This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 3b19f76cc3 test(frontend): render the hub search result with its real 
children (#7661)
3b19f76cc3 is described below

commit 3b19f76cc357788c5dd47f2f87aa50ce9e476fe6
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sat Aug 15 01:39:05 2026 +0000

    test(frontend): render the hub search result with its real children (#7661)
    
    ### What changes were proposed in this PR?
    
    `hub-search-result.component.html` reported **0 of 23 lines, 0 of 8
    branches and 0 of 3 functions** covered, behind a 17-test spec. That is
    the attribution loss from #7458, not an untested template: the spec
    swaps its four children for same-selector stubs via
    `TestBed.overrideComponent`, and any override re-JITs the component from
    its decorator metadata, leaving the recompiled template with no source
    map back to the `.html`.
    
    Adds a `describe` block that renders the component with its **real**
    children:
    
    | | Before | After |
    |---|---|---|
    | lines | 0/23 | **23/23** |
    | branches | 0/8 | **8/8** |
    | functions | 0/3 | **3/3** |
    
    The block keeps its own `TestBed`, so the 17 existing tests keep their
    stubs and assertions untouched. Same remedy as merged PRs #7535, #7627
    and #7629 — this is the last of the six templates #7458 identified, bar
    `workspace` and the `user-project` one that the project-feature removal
    deletes.
    
    Covered: the real children resolving rather than the stub selectors, the
    dataset-only view toggle and its absence for workflows, which toggle
    button is highlighted, the sort options shown and hidden per search
    type, the sort handler's two halves, the card template and its
    `viewMode` guard, and the three inputs handed to the results list.
    
    ### Verification
    
    23 mutations applied and reverted, production diff empty each time.
    
    **Two then turned out to survive, and the cause is worth recording.**
    The view-toggle helper read each button as
    `classList.contains("ant-btn-primary")`, which pins a ternary's *true*
    leg only — so changing the false leg from `'default'` to `'dashed'` or
    `'link'` shipped green. The build's own mutations replaced the whole
    ternary with a constant, which flips the primary leg and dies, which is
    exactly why the gap was missed.
    
    The helper now reads the type by **name**, matched against the nzType
    names rather than any `ant-btn-*` class, since the buttons also carry
    `ant-btn-icon-only` — my first attempt at the fix picked that modifier
    up and failed loudly, which is how I found it.
    
    All three are now red, each on the intended test rather than merely
    somewhere in the suite:
    
    | Mutation | Result | Failing test |
    |---|---|---|
    | list button's false leg → `dashed` | red | "highlights whichever
    view-toggle button matches…" |
    | card button's false leg → `link` | red | same |
    | list button collapsed to a constant (control) | red | same |
    
    ### Deliberately not included
    
    A static `nzTheme="outline"` attribute carries no lcov line and is not
    asserted.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7660
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/hub-search-result.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  27 passed (27)
    ```
    
    10 new on top of the existing 17. Coverage re-measured by reverting the
    spec, running with `--coverage`, and restoring. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../hub-search-result.component.spec.ts            | 255 ++++++++++++++++++++-
 1 file changed, 252 insertions(+), 3 deletions(-)

diff --git 
a/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.spec.ts
 
b/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.spec.ts
index 59aa576d10..cfc5a4936a 100644
--- 
a/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.spec.ts
+++ 
b/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.spec.ts
@@ -18,9 +18,14 @@
  */
 
 import { Component, EventEmitter, forwardRef, Input, Output, TemplateRef } 
from "@angular/core";
-import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { Router } from "@angular/router";
+import { ComponentFixture, fakeAsync, TestBed, tick } from 
"@angular/core/testing";
+import { provideRouter, Router } from "@angular/router";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { By } from "@angular/platform-browser";
 import { NzIconModule } from "ng-zorro-antd/icon";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { en_US, provideNzI18n } from "ng-zorro-antd/i18n";
 import { AppstoreOutline, BarsOutline } from "@ant-design/icons-angular/icons";
 import { of, Subject } from "rxjs";
 import { vi } from "vitest";
@@ -31,10 +36,19 @@ import { FiltersComponent } from 
"../../../dashboard/component/user/filters/filt
 import { CardItemComponent } from 
"../../../dashboard/component/user/list-item/card-item/card-item.component";
 import { SortButtonComponent } from 
"../../../dashboard/component/user/sort-button/sort-button.component";
 import { SortMethod } from "../../../dashboard/type/sort-method";
+import { DashboardEntry } from "../../../dashboard/type/dashboard-entry";
 import { UserService } from "../../../common/service/user/user.service";
-import { StubUserService } from 
"../../../common/service/user/stub-user.service";
+import { MOCK_USER_ID, StubUserService } from 
"../../../common/service/user/stub-user.service";
 import { SearchService } from "../../../dashboard/service/user/search.service";
 import { commonTestProviders } from "../../../common/testing/test-utils";
+import { OperatorMetadataService } from 
"../../../workspace/service/operator-metadata/operator-metadata.service";
+import { StubOperatorMetadataService } from 
"../../../workspace/service/operator-metadata/stub-operator-metadata.service";
+import { UserProjectService } from 
"../../../dashboard/service/user/project/user-project.service";
+import { StubUserProjectService } from 
"../../../dashboard/service/user/project/stub-user-project.service";
+import { WorkflowPersistService } from 
"../../../common/service/workflow-persist/workflow-persist.service";
+import { StubWorkflowPersistService } from 
"../../../common/service/workflow-persist/stub-workflow-persist.service";
+import { DatasetService } from 
"../../../dashboard/service/user/dataset/dataset.service";
+import { WorkflowCoverService } from 
"../../../dashboard/service/user/workflow-cover/workflow-cover.service";
 
 const VIEW_MODE_STORAGE_KEY = "texera.hub.dataset.viewMode";
 
@@ -377,3 +391,238 @@ describe("HubSearchResultComponent", () => {
     });
   });
 });
+
+// The suite above stubs the children out, and *any* `overrideComponent` makes
+// Angular re-JIT HubSearchResultComponent from its retained decorator 
metadata;
+// the recompiled template loses its source map back to
+// hub-search-result.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("HubSearchResultComponent rendered template", () => {
+  let fixture: ComponentFixture<HubSearchResultComponent>;
+  let executeSearch: ReturnType<typeof vi.fn>;
+  let entries: DashboardEntry[];
+
+  const host = (): HTMLElement => fixture.nativeElement as HTMLElement;
+
+  const toggleButtons = (): HTMLButtonElement[] =>
+    Array.from(host().querySelectorAll<HTMLButtonElement>(".view-toggle 
button"));
+
+  /**
+   * nz-button renders nzType as an `ant-btn-<type>` class, so this reads the 
[nzType] ternaries
+   * back off the DOM. It reports the type NAME rather than a 
primary/not-primary boolean on
+   * purpose: a boolean read pins each ternary's false leg only as "not 
primary", so changing
+   * `'default'` to `'dashed'` or `'link'` would ship green.
+   */
+  const toggleTypes = (): string[] => {
+    // Matched against the nzType names rather than any `ant-btn-*` class, 
because the buttons also
+    // carry modifier classes such as `ant-btn-icon-only`.
+    const names = ["primary", "default", "dashed", "link", "text"];
+    return toggleButtons().map(button => names.find(name => 
button.classList.contains(`ant-btn-${name}`)) ?? "none");
+  };
+
+  const cardItems = (): CardItemComponent[] =>
+    fixture.debugElement.queryAll(By.directive(CardItemComponent)).map(item => 
item.componentInstance);
+
+  const cardNames = (): string[] =>
+    Array.from(host().querySelectorAll(".card-grid texera-card-item 
.resource-name")).map(name =>
+      name.textContent!.trim()
+    );
+
+  const results = (): SearchResultsComponent =>
+    
fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance;
+
+  /** The sort options the real sort button offers, read out of the cdk 
overlay it opens on hover. */
+  const sortMenuLabels = (): string[] =>
+    Array.from(document.querySelectorAll(".cdk-overlay-container 
li[nz-menu-item]")).map(item =>
+      item.textContent!.trim()
+    );
+
+  function openSortMenu(): void {
+    host().querySelector("texera-sort-button a")!.dispatchEvent(new 
MouseEvent("mouseenter"));
+    tick(500);
+    fixture.detectChanges();
+  }
+
+  function makeDatasetEntry(id: number, name: string): DashboardEntry {
+    return {
+      id,
+      name,
+      description: "",
+      type: "dataset",
+      dataset: { isOwner: true },
+      accessibleUserIds: [],
+      likeCount: 0,
+      viewCount: 0,
+      isLiked: false,
+      size: 0,
+    } as unknown as DashboardEntry;
+  }
+
+  function render(url: string, storedViewMode?: string): void {
+    TestBed.resetTestingModule();
+    localStorage.clear();
+    if (storedViewMode !== undefined) {
+      localStorage.setItem(VIEW_MODE_STORAGE_KEY, storedViewMode);
+    }
+    executeSearch = vi.fn(() => of({ entries, more: false }));
+
+    TestBed.configureTestingModule({
+      imports: [
+        HubSearchResultComponent,
+        NzIconModule.forChild([BarsOutline, AppstoreOutline]),
+        HttpClientTestingModule,
+        NoopAnimationsModule,
+      ],
+      providers: [
+        provideRouter([]),
+        { provide: SearchService, useValue: { executeSearch } },
+        { provide: UserService, useClass: StubUserService },
+        { provide: OperatorMetadataService, useClass: 
StubOperatorMetadataService },
+        { provide: UserProjectService, useClass: StubUserProjectService },
+        { provide: WorkflowPersistService, useValue: new 
StubWorkflowPersistService([]) },
+        { provide: DatasetService, useValue: { getDatasetCoverUrl: vi.fn(() => 
of({ url: undefined })) } },
+        { provide: WorkflowCoverService, useValue: { getCover: vi.fn(() => 
of(undefined)) } },
+        NzModalService,
+        provideNzI18n(en_US),
+        ...commonTestProviders,
+      ],
+    });
+
+    // ngOnInit derives searchType from Router.url; an own property shadows 
the real getter.
+    Object.defineProperty(TestBed.inject(Router), "url", { get: () => url });
+
+    fixture = TestBed.createComponent(HubSearchResultComponent);
+    fixture.detectChanges();
+  }
+
+  /** Entries only reach the DOM through a search, which is how the card 
template gets instantiated. */
+  async function loadEntries(list: DashboardEntry[]): Promise<void> {
+    entries = list;
+    await fixture.componentInstance.search(true);
+    fixture.detectChanges();
+  }
+
+  beforeEach(() => {
+    entries = [];
+  });
+
+  afterEach(() => {
+    fixture?.destroy();
+    localStorage.clear();
+    document.querySelectorAll(".cdk-overlay-container").forEach(el => 
el.remove());
+  });
+
+  it("renders the real children, not the stubbed selectors", () => {
+    // If these resolve to empty stub templates the component was re-JITed and 
the
+    // template's coverage has silently gone back to zero.
+    render("/dashboard/dataset");
+
+    expect(host().querySelector("texera-sort-button 
button#sortDropdown")).not.toBeNull();
+    expect(host().querySelector("texera-filters button")).not.toBeNull();
+    expect(host().querySelector("texera-search-results 
nz-card")).not.toBeNull();
+  });
+
+  it("renders both dataset view-toggle buttons, each with its own label and 
icon", () => {
+    render("/dashboard/dataset");
+
+    expect(toggleButtons().map(button => button.title)).toEqual(["List view", 
"Card view"]);
+    // nz-icon turns nzType into an `anticon-<type>` class, so this pins which 
icon each button asks for.
+    expect(toggleButtons().map(button => 
button.querySelector("i[nz-icon]")!.className)).toEqual([
+      expect.stringContaining("anticon-bars"),
+      expect.stringContaining("anticon-appstore"),
+    ]);
+  });
+
+  it("omits the view toggle entirely when the search type is workflow", () => {
+    render("/dashboard/workflow");
+
+    expect(host().querySelector(".view-toggle")).toBeNull();
+    expect(toggleButtons()).toEqual([]);
+    // The rest of the filter bar is unaffected.
+    expect(host().querySelector("texera-sort-button 
button#sortDropdown")).not.toBeNull();
+  });
+
+  it("highlights whichever view-toggle button matches the current view mode", 
() => {
+    render("/dashboard/dataset");
+    expect(toggleTypes()).toEqual(["primary", "default"]);
+
+    toggleButtons()[1].click();
+    fixture.detectChanges();
+    expect(toggleTypes()).toEqual(["default", "primary"]);
+
+    toggleButtons()[0].click();
+    fixture.detectChanges();
+    expect(toggleTypes()).toEqual(["primary", "default"]);
+  });
+
+  it("hides the edit-time and execution-time sort options for datasets", 
fakeAsync(() => {
+    render("/dashboard/dataset");
+
+    openSortMenu();
+
+    expect(sortMenuLabels()).toEqual(["By Create Time", "A -> Z", "Z -> A"]);
+  }));
+
+  it("offers the edit-time and execution-time sort options for workflows", 
fakeAsync(() => {
+    render("/dashboard/workflow");
+
+    openSortMenu();
+
+    expect(sortMenuLabels()).toEqual(["By Edit Time", "By Create Time", "By 
Execution Time", "A -> Z", "Z -> A"]);
+  }));
+
+  it("re-runs the search with the sort method the sort button emits", () => {
+    render("/dashboard/workflow");
+    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.
+    expect(executeSearch).toHaveBeenCalledTimes(1);
+    expect(executeSearch.mock.calls[0][5]).toBe(SortMethod.CreateTimeDesc);
+  });
+
+  it("renders every dataset entry through the card template in card mode", 
async () => {
+    render("/dashboard/dataset", "card");
+
+    await loadEntries([makeDatasetEntry(7, "alpha"), makeDatasetEntry(8, 
"beta")]);
+
+    expect(cardNames()).toEqual(["alpha", "beta"]);
+    // The like button is disabled while currentUid is undefined, so an 
enabled one
+    // is the card template's [currentUid] binding arriving in the DOM.
+    const likeButtons = 
Array.from(host().querySelectorAll<HTMLButtonElement>(".card-grid .like-btn"));
+    expect(likeButtons.map(button => button.disabled)).toEqual([false, false]);
+    expect(cardItems().map(item => item.currentUid)).toEqual([MOCK_USER_ID, 
MOCK_USER_ID]);
+  });
+
+  it("keeps the workflow search type on the list view even when card mode is 
stored", () => {
+    render("/dashboard/workflow", "card");
+    expect(fixture.componentInstance.viewMode).toBe("card");
+
+    expect(host().querySelector("cdk-virtual-scroll-viewport")).not.toBeNull();
+    expect(host().querySelector(".card-scroll-container")).toBeNull();
+    // The card template itself is withheld too, which the DOM cannot show 
while
+    // the results list is already pinned to the list view.
+    expect(results().cardTemplate).toBeUndefined();
+  });
+
+  it("hands the resource types, the filter keywords and the signed-in uid to 
the results list", () => {
+    render("/dashboard/workflow");
+    const filters = 
fixture.debugElement.query(By.directive(FiltersComponent)).componentInstance as 
FiltersComponent;
+
+    // Committing a filter list is what the real filter bar does on every 
change, and it
+    // is what makes the component republish its keywords.
+    filters.masterFilterList = ["alpha"];
+    fixture.detectChanges();
+
+    // Asserted on the child inputs rather than the DOM because all three only 
reach the
+    // markup through texera-list-item, which is not rendered while the result 
list is empty.
+    expect(results().showResourceTypes).toBe(true);
+    expect(results().searchKeywords).toEqual(["alpha"]);
+    expect(results().currentUid).toBe(MOCK_USER_ID);
+  });
+});

Reply via email to