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-7627-caf3d3e62abb4e4e6dd214aa17dbe5700ee798ba
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 95b2e5c10688a9319aef73073bae584615d03a30
Author: Xinyuan Lin <[email protected]>
AuthorDate: Fri Aug 14 06:04:05 2026 +0000

    test(frontend): render the dashboard search with its real children (#7627)
    
    ### What changes were proposed in this PR?
    
    `search.component.html` reported **0 of 35 lines** covered while its own
    `.ts` sat at 93%. That is the attribution loss from #7458, not an
    untested template: the spec stubs its children out through
    `TestBed.overrideComponent`, and any override re-JITs the component from
    its decorator metadata, so the re-compiled template has no source map
    back to the `.html` and its bindings execute uncounted.
    
    Adds a `describe` block that renders the component with its **real**
    children, which restores attribution:
    
    | | Before | After |
    |---|---|---|
    | `search.component.html` lines | 0/35 | **35/35** |
    | `search.component.html` functions | 0/6 | **6/6** |
    | `search.component.ts` lines | 52/56 | 55/56 |
    | `search.component.ts` branches | 19/23 | 21/23 |
    
    The block keeps its own `TestBed`, so the 15 existing tests keep their
    stubs and assertions untouched. Same remedy as merged PR #7535.
    
    ### Verification
    
    16 mutations applied and reverted, production diff empty each time. Five
    were then run independently, chosen against the failure modes rather
    than repeating the list:
    
    | Probe | Result |
    |---|---|
    | drop `[currentUid]` from the child binding | red — so the
    child-instance assertion is not the "collaborator also sets it" trap |
    | `(sortMethodChange)` no longer assigns | red |
    | the All-type button unwired | red |
    | delete the Project button's `nz-icon` span | red — which also proves
    `nz-icon` really derives its `anticon-*` class under jsdom |
    | `[showResourceTypes]="true"` → `"false"` | **survives**, and was
    reported as such |
    
    I ran a sixth of my own afterwards — unwiring the All-type button — and
    it is red.
    
    ### Deliberately not included
    
    `[showResourceTypes]` is reported rather than pinned: repo-wide grep
    shows the consuming component never varies it, so the flag has one value
    in practice and no assertion can distinguish the two.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7626
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/search.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  22 passed (22)
    ```
    
    7 new on top of the existing 15. Coverage re-measured from scratch by
    reverting the spec, running with `--coverage`, then restoring. `yarn
    format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
    
    Co-authored-by: Meng Wang <[email protected]>
---
 .../component/user/search/search.component.spec.ts | 131 +++++++++++++++++++++
 1 file changed, 131 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/search/search.component.spec.ts 
b/frontend/src/app/dashboard/component/user/search/search.component.spec.ts
index f8e0673d67..2b100a02c9 100644
--- a/frontend/src/app/dashboard/component/user/search/search.component.spec.ts
+++ b/frontend/src/app/dashboard/component/user/search/search.component.spec.ts
@@ -30,6 +30,21 @@ import { SearchService } from 
"../../../service/user/search.service";
 import { UserService } from "../../../../common/service/user/user.service";
 import { SortMethod } from "../../../type/sort-method";
 import { commonTestProviders } from "../../../../common/testing/test-utils";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { FormsModule } from "@angular/forms";
+import { JWT_OPTIONS, JwtHelperService } from "@auth0/angular-jwt";
+import { NzDropDownModule } from "ng-zorro-antd/dropdown";
+import { NzModalModule } from "ng-zorro-antd/modal";
+import { en_US, provideNzI18n } from "ng-zorro-antd/i18n";
+import { By } from "@angular/platform-browser";
+import { MOCK_USER_ID, StubUserService } from 
"../../../../common/service/user/stub-user.service";
+import { OperatorMetadataService } from 
"src/app/workspace/service/operator-metadata/operator-metadata.service";
+import { StubOperatorMetadataService } from 
"src/app/workspace/service/operator-metadata/stub-operator-metadata.service";
+import { UserProjectService } from 
"../../../service/user/project/user-project.service";
+import { StubUserProjectService } from 
"../../../service/user/project/stub-user-project.service";
+import { WorkflowPersistService } from 
"src/app/common/service/workflow-persist/workflow-persist.service";
+import { StubWorkflowPersistService } from 
"src/app/common/service/workflow-persist/stub-workflow-persist.service";
+import { SortButtonComponent } from "../sort-button/sort-button.component";
 
 // Lightweight stand-in for FiltersComponent. It registers itself under the 
real
 // FiltersComponent token so SearchComponent's `@ViewChild(FiltersComponent)`
@@ -240,3 +255,119 @@ describe("SearchComponent", () => {
     expect(component.sortMethod).toBe(SortMethod.EditTimeDesc);
   });
 });
+
+// The suite above stubs the children out, and *any* `overrideComponent` makes
+// Angular re-JIT SearchComponent from its retained decorator metadata; the
+// recompiled template loses its source map back to search.component.html, so
+// every binding still runs but none of it is attributed (issue #7458). This
+// suite therefore stands up its own TestBed with the REAL children and asserts
+// on the rendered DOM, leaving the stubbed tests above untouched.
+describe("SearchComponent rendered template", () => {
+  let fixture: ComponentFixture<SearchComponent>;
+  let locationStub: { back: ReturnType<typeof vi.fn> };
+  let executeSearch: ReturnType<typeof vi.fn>;
+
+  const host = (): HTMLElement => fixture.nativeElement as HTMLElement;
+
+  /** The four resource-type buttons — direct children of .left-controls, 
which excludes the sort button. */
+  const typeButtons = (): HTMLButtonElement[] =>
+    Array.from(host().querySelectorAll<HTMLButtonElement>(".left-controls > 
button"));
+
+  const labels = (): string[] => typeButtons().map(button => 
button.textContent!.trim());
+
+  const selectedFlags = (): boolean[] => typeButtons().map(button => 
button.classList.contains("selected"));
+
+  /** The `type` argument of the most recent SearchService.executeSearch call. 
*/
+  const lastSearchedType = (): unknown => 
executeSearch.mock.calls[executeSearch.mock.calls.length - 1][4];
+
+  beforeEach(async () => {
+    TestBed.resetTestingModule();
+    locationStub = { back: vi.fn() };
+    executeSearch = vi.fn(() => of({ entries: [], more: false }));
+
+    await TestBed.configureTestingModule({
+      imports: [SearchComponent, NzModalModule, NzDropDownModule, FormsModule, 
HttpClientTestingModule],
+      providers: [
+        JwtHelperService,
+        { provide: JWT_OPTIONS, useValue: {} },
+        { provide: SearchService, useValue: { executeSearch } },
+        { provide: UserService, useClass: StubUserService },
+        { provide: OperatorMetadataService, useClass: 
StubOperatorMetadataService },
+        { provide: UserProjectService, useClass: StubUserProjectService },
+        { provide: WorkflowPersistService, useValue: new 
StubWorkflowPersistService([]) },
+        { provide: ActivatedRoute, useValue: { queryParams: new 
Subject<Params>() } },
+        { provide: Location, useValue: locationStub },
+        provideNzI18n(en_US),
+        ...commonTestProviders,
+      ],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SearchComponent);
+    fixture.detectChanges();
+  });
+
+  it("renders the four resource-type buttons, each with its own label and 
icon", () => {
+    expect(labels()).toEqual(["All", "Project", "Workflow", "Dataset"]);
+    // nz-icon turns nzType into an `anticon-<type>` class, so this pins the 
icon
+    // each button asks for — and that "All" asks for none.
+    const icons = typeButtons().map(button => {
+      const icon = button.querySelector("span[nz-icon]");
+      return icon && Array.from(icon.classList).find(name => 
name.startsWith("anticon-"));
+    });
+    expect(icons).toEqual([null, "anticon-container", "anticon-project", 
"anticon-database"]);
+  });
+
+  it("highlights only the All button before a resource type is chosen", () => {
+    expect(selectedFlags()).toEqual([true, false, false, false]);
+  });
+
+  it("moves the highlight onto whichever resource-type button is clicked", () 
=> {
+    // Click each typed button in turn; the highlight must be one-hot on that
+    // same button, which pins both its (click) argument and its [ngClass] 
test.
+    typeButtons().forEach((button, index) => {
+      button.click();
+      fixture.detectChanges();
+      expect(selectedFlags()).toEqual(labels().map((_, i) => i === index));
+    });
+  });
+
+  it("scopes the search to the clicked resource type", () => {
+    typeButtons()[3].click();
+    expect(lastSearchedType()).toBe("dataset");
+
+    // Back to All: the type filter is cleared rather than left on "dataset".
+    typeButtons()[0].click();
+    expect(lastSearchedType()).toBeNull();
+  });
+
+  it("re-runs the search with the sort method the sort button emits", () => {
+    const sortButton = 
fixture.debugElement.query(By.directive(SortButtonComponent))
+      .componentInstance as SortButtonComponent;
+
+    sortButton.dateSort();
+
+    // Kills both halves of `sortMethod = $event; search()`: drop the 
assignment
+    // and the search runs with the EditTimeDesc default; drop the call and
+    // executeSearch is never reached at all.
+    expect(executeSearch).toHaveBeenCalledTimes(1);
+    expect(executeSearch.mock.calls[0][5]).toBe(SortMethod.CreateTimeDesc);
+  });
+
+  it("renders a back arrow that returns to the previous page", () => {
+    const goBack = host().querySelector<HTMLButtonElement>(".go-back-button")!;
+    expect(goBack.querySelector("span.anticon-arrow-left")).not.toBeNull();
+
+    goBack.click();
+
+    expect(locationStub.back).toHaveBeenCalledTimes(1);
+  });
+
+  it("hands the signed-in user's uid to the results list", () => {
+    const results = 
fixture.debugElement.query(By.directive(SearchResultsComponent))
+      .componentInstance as SearchResultsComponent;
+    // Asserted on the child input rather than the DOM because currentUid only
+    // reaches the markup through texera-list-item, which is not rendered while
+    // the result list is empty.
+    expect(results.currentUid).toBe(MOCK_USER_ID);
+  });
+});

Reply via email to