You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/23 23:20:36 UTC
(texera) 01/01: test(frontend): extend HuggingFaceComponent coverage for template bindings and fallback branches (#6831)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6831-3149eb5ce55de0db029bdba2c3106684e6c65a74
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 362e6baca217b29d6df411d669e029ab5bc1af7d
Author: Meng Wang <me...@uci.edu>
AuthorDate: Thu Jul 23 16:20:14 2026 -0700
test(frontend): extend HuggingFaceComponent coverage for template bindings and fallback branches (#6831)
### What changes were proposed in this PR?
Extends the `HuggingFaceComponent` spec to close the coverage gaps that
remain
after #6773.
**Note on the issue's premise:** #6828 cites ~80% / 57 uncovered lines,
but that
is pre-#6773 codecov data — the component's **statement/line coverage is
already
100%**. Measuring locally (`ng test --include <spec> --coverage`) showed
the real
remaining gaps are the **template's event bindings** (the existing spec
never
rendered/clicked anything) and a handful of **fallback branches**. This
PR
targets exactly those:
| | before | after |
|---|---|---|
| `hugging-face.component.html` statements | 84.67% | **97.58%** |
| `hugging-face.component.html` branches | 77.77% | **94.44%** |
| `hugging-face.component.ts` branches | 91.04% | **94.78%** |
14 added tests:
- **Rendered-template interactions** (new `By.css` +
`triggerEventHandler` block — the existing spec called methods directly,
so none of the template's `(click)`/`(ngModelChange)` bindings were
exercised): selecting a model item, clearing the selected model, the
Prev/Next pagination buttons, both Retry buttons (models-load and
tasks-load failures), the search input, the search clear icon, the task
dropdown, and the search-specific empty-state message.
- **Fallback branches**: the empty-tag `|| "text-generation"` fallback
in `loadAllModels` / `retryLoad` / the server-side search, and
persisting a task when the field has no `parent` and the form has no
`task` control.
**Determinism:** the added tests reuse the spec's existing hygiene
(`invalidateHuggingFaceModelCache()` per test, `fixture.destroy()` +
`http.verify()` in `afterEach`, `fakeAsync`/`tick` for the 300 ms
debounce). I
deliberately left three candidate branches uncovered rather than ship a
fragile
test: the two "in-flight fetch canceled" poll branches (couldn't be
driven
deterministically) and the formly validation-message block (needs extra
formly
config). Two others (`snapshot` hasOwnProperty, `defaults[key] ?? ""`)
are
unreachable — every key is always populated.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6828
### How was this PR tested?
Extended unit tests, run locally in `frontend/`:
```
ng test --watch=false --include src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
# Test Files 1 passed (1) | Tests 85 passed (85) — 3 consecutive runs, 0 flakes
prettier --write <spec> # unchanged
eslint <spec> # clean
```
The failure path was verified by deliberately breaking a new assertion
and
confirming the suite exits non-zero.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---------
Signed-off-by: Meng Wang <me...@uci.edu>
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
---
.../hugging-face/hugging-face.component.spec.ts | 189 +++++++++++++++++++++
1 file changed, 189 insertions(+)
diff --git a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
index ad31f0ca6c..201361767d 100644
--- a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
+++ b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
@@ -19,6 +19,7 @@
import { ComponentFixture, TestBed, discardPeriodicTasks, fakeAsync, tick } from "@angular/core/testing";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
+import { By } from "@angular/platform-browser";
import { FormControl, FormGroup } from "@angular/forms";
import { FieldTypeConfig } from "@ngx-formly/core";
import { AppSettings } from "../../../common/app-setting";
@@ -1384,4 +1385,192 @@ describe("HuggingFaceComponent (TestBed)", () => {
expect(component.loading).toBe(false);
});
});
+
+ // ── Rendered-template interactions (the (click)/(ngModelChange) bindings) ──
+
+ describe("template interactions", () => {
+ it("clicking a model item selects it", () => {
+ initComponent("text-generation", buildModels(3));
+ fixture.detectChanges();
+
+ const item = fixture.debugElement.query(By.css(".hf-model-item"));
+ expect(item).toBeTruthy();
+ item.triggerEventHandler("click", null);
+
+ expect(component.formControl.value).toBe("model/model-0");
+ });
+
+ it("clicking the close icon on the selected model clears the value", () => {
+ initComponent("text-generation", buildModels(3));
+ component.formControl.setValue("model/model-0");
+ fixture.detectChanges();
+
+ const closeIcon = fixture.debugElement.query(By.css(".hf-selected-model i"));
+ expect(closeIcon).toBeTruthy();
+ closeIcon.triggerEventHandler("click", null);
+
+ expect(component.formControl.value).toBe("");
+ });
+
+ it("the pagination buttons move to the next and previous page", () => {
+ initComponent("text-generation", buildModels(120)); // 3 pages of 50
+ fixture.detectChanges();
+
+ const buttons = fixture.debugElement.queryAll(By.css(".hf-pagination button"));
+ expect(buttons.length).toBe(2);
+
+ buttons[1].triggerEventHandler("click", null);
+ expect(component.currentPage).toBe(1);
+
+ fixture.detectChanges();
+ buttons[0].triggerEventHandler("click", null);
+ expect(component.currentPage).toBe(0);
+ });
+
+ it("clicking Retry re-fetches the models after a load failure", () => {
+ const { field } = buildFieldWithFormGroup("text-generation");
+ component.field = field;
+ fixture.detectChanges();
+ flushIconRequests();
+ http.expectOne(`${API}/huggingface/tasks`).flush(buildTaskResponse());
+ http
+ .expectOne(req => req.url.startsWith(`${API}/huggingface/models`))
+ .flush("boom", { status: 500, statusText: "Server Error" });
+ fixture.detectChanges();
+
+ const retry = fixture.debugElement.query(By.css(".hf-error button"));
+ expect(retry).toBeTruthy();
+ retry.triggerEventHandler("click", null);
+
+ http.expectOne(req => req.url.startsWith(`${API}/huggingface/models`)).flush(buildModels(2));
+ expect(component.errorMessage).toBeNull();
+ });
+
+ it("clicking Retry re-fetches the tasks after a tasks-load failure", () => {
+ const { field } = buildFieldWithFormGroup("text-generation");
+ component.field = field;
+ fixture.detectChanges();
+ flushIconRequests();
+ http.expectOne(`${API}/huggingface/tasks`).flush("boom", { status: 500, statusText: "Server Error" });
+ http.expectOne(req => req.url.startsWith(`${API}/huggingface/models`)).flush(buildModels(2));
+ fixture.detectChanges();
+
+ const retry = fixture.debugElement.query(By.css(".hf-error button"));
+ expect(retry).toBeTruthy();
+ retry.triggerEventHandler("click", null);
+
+ http.expectOne(`${API}/huggingface/tasks`).flush(buildTaskResponse());
+ expect(component.tasksError).toBeNull();
+ });
+
+ it("the search box forwards typed text to onSearchInput", () => {
+ initComponent("text-generation", buildModels(3));
+ fixture.detectChanges();
+
+ const input = fixture.debugElement.query(By.css("input[nz-input]"));
+ expect(input).toBeTruthy();
+ input.triggerEventHandler("ngModelChange", "model-1");
+
+ expect(component.searchText).toBe("model-1");
+ });
+
+ it("clicking the clear icon resets the search", () => {
+ initComponent("text-generation", buildModels(3));
+ component.onSearchInput("model-1");
+ fixture.detectChanges();
+
+ const clear = fixture.debugElement.query(By.css("i[nztype='close-circle']"));
+ expect(clear).toBeTruthy();
+ clear.triggerEventHandler("click", null);
+
+ expect(component.searchText).toBe("");
+ });
+
+ it("changing the task dropdown loads the models for the new task", () => {
+ initComponent("text-generation", buildModels(3));
+ fixture.detectChanges();
+
+ const select = fixture.debugElement.query(By.css("nz-select"));
+ expect(select).toBeTruthy();
+ select.triggerEventHandler("ngModelChange", "image-classification");
+
+ http.expectOne(`${API}/huggingface/models?task=image-classification`).flush(buildModels(2));
+ expect(component.selectedTaskTag).toBe("image-classification");
+ });
+
+ it("shows the search-specific empty message when a search matches nothing", () => {
+ initComponent("text-generation", buildModels(3));
+ component.onSearchInput("no-such-model");
+ fixture.detectChanges();
+
+ const empty = fixture.debugElement.query(By.css(".hf-empty"));
+ expect(empty).toBeTruthy();
+ expect(empty.nativeElement.textContent).toContain('No models found for "no-such-model".');
+ });
+ });
+
+ // ── Fallback branches ──
+
+ describe("fallback branches", () => {
+ it("falls back to the text-generation tag when the selected tag is empty", () => {
+ initComponent("text-generation", buildModels(2));
+ invalidateHuggingFaceModelCache();
+ component.selectedTaskTag = "";
+
+ component.retryLoad();
+
+ http.expectOne(`${API}/huggingface/models?task=text-generation`).flush(buildModels(1));
+ expect(component.pagedModels.length).toBe(1);
+ });
+
+ it("server-side search falls back to the text-generation tag when the tag is empty", fakeAsync(() => {
+ initComponent("text-generation", buildModels(2));
+ component.truncated = true;
+ component.selectedTaskTag = "";
+
+ component.onSearchInput("query");
+ tick(300);
+
+ http.expectOne(`${API}/huggingface/models?task=text-generation&search=query`).flush(buildModels(1));
+ expect(component.pagedModels.length).toBe(1);
+ }));
+
+ it("uses the current selection when no task is present on the model or form", fakeAsync(() => {
+ // taskValue "" -> getCurrentTaskTag() returns undefined, so the ?? fallback is taken.
+ const { field } = buildFieldWithFormGroup("");
+ component.field = field;
+ fixture.detectChanges();
+ flushIconRequests();
+ http.expectOne(`${API}/huggingface/tasks`).flush(buildTaskResponse());
+ http.expectOne(req => req.url.startsWith(`${API}/huggingface/models`)).flush(buildModels(2));
+ tick(0); // the ngOnInit setTimeout re-sync, which also takes the ?? fallback
+
+ component.onTaskSelected("summarization");
+
+ http.expectOne(`${API}/huggingface/models?task=summarization`).flush(buildModels(1));
+ expect(component.selectedTaskTag).toBe("summarization");
+ }));
+
+ it("persists the task when the field has no parent and the form has no task control", () => {
+ const formGroup = new FormGroup({ modelId: new FormControl("") });
+ const field = {
+ key: "modelId",
+ formControl: formGroup.get("modelId")! as FormControl,
+ form: formGroup,
+ model: { modelId: "" } as Record<string, unknown>,
+ props: {},
+ options: { detectChanges: vi.fn() },
+ } as unknown as FieldTypeConfig; // no `parent` -> the `field.parent ?? field` fallback
+ component.field = field;
+ fixture.detectChanges();
+ flushIconRequests();
+ http.expectOne(`${API}/huggingface/tasks`).flush(buildTaskResponse());
+ http.expectOne(req => req.url.startsWith(`${API}/huggingface/models`)).flush(buildModels(2));
+
+ expect(() => component.onTaskSelected("translation")).not.toThrow();
+
+ http.expectOne(`${API}/huggingface/models?task=translation`).flush(buildModels(1));
+ expect(component.selectedTaskTag).toBe("translation");
+ });
+ });
});