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 <[email protected]>
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 <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../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");
+    });
+  });
 });

Reply via email to