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-7417-fe89db4315c4b23c04ae06160030566ce8410281 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 69c7391a5f613b165ce7276f81811a98de48c442 Author: Xinyuan Lin <[email protected]> AuthorDate: Sat Aug 8 00:24:55 2026 -0700 test(frontend): render the dataset page's view toggle and results wiring (#7417) ### What changes were proposed in this PR? `UserDatasetComponent`'s spec constructs the component with `new`, so its template had never been rendered and sat at 0%. That 0% is simply "never mounted" — not the instrumentation problem some other templates have. Adds 8 tests that mount it for real: - the card/list toggle, and which button carries the `primary` highlight — the only way the user can tell which view they are in - the preference surviving into a freshly created component through `localStorage` - the view mode, `editable` and `isPrivateSearch` flags handed down to `texera-search-results` - `(sortMethodChange)="sortMethod = $event; search()"` — an inline statement doing two things, where dropping either leaves the list in the previous order - the execution-time sort options being withheld on a page whose entries never execute - the create button being wired **Verified by mutation**, all reverted (production diff empty): | Mutation | Result | |---|---| | pin the list button's highlight on | red | | key the card highlight off the wrong mode | red | | stop passing the view mode down | red | | mark the results list read-only | red | | turn off the private-search scope | red | | drop `search()` from the sort handler | red | | drop the assignment from the sort handler | red | | offer the execution-time sort option | red | | unwire the create button | red | | stop persisting the view preference | red | Three findings from getting it to run, all commented in the spec: - `viewType` defaults to **card**, not list, and is seeded from `localStorage` at construction — so it is cleared per test, or a view chosen by one test leaks into the next. That persistence turned out to be worth a test of its own. - The sort button renders its own `<button>` into the same `nz-space-compact`, so a positional selector picks that up first. The view buttons are selected by their `title` attribute instead. - ng-zorro defaults to `zh-cn` and throws `NG0701` without locale data; the TestBed provides `{ provide: NZ_I18N, useValue: en_US }`, matching the sibling dashboard specs. No production file is touched. ### Any related issues, documentation, discussions? Closes #7414 ### How was this PR tested? ``` npx ng test --watch=false --include="**/user-dataset.component.spec.ts" ``` ``` Test Files 1 passed (1) Tests 26 passed (26) ``` 8 new on top of the existing 18. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --- .../user-dataset/user-dataset.component.spec.ts | 153 +++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts index 1806e51735..1ccd5918fb 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts @@ -18,6 +18,20 @@ */ import { of, Subject } from "rxjs"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideRouter } from "@angular/router"; +import { NzModalService } from "ng-zorro-antd/modal"; +import { NzMessageService } from "ng-zorro-antd/message"; +import { en_US, NZ_I18N } from "ng-zorro-antd/i18n"; +import { UserService } from "../../../../common/service/user/user.service"; +import { StubUserService } from "../../../../common/service/user/stub-user.service"; +import { SearchService } from "../../../service/user/search.service"; +import { DatasetService } from "../../../service/user/dataset/dataset.service"; +import { SearchResultsComponent } from "../search-results/search-results.component"; +import { SortButtonComponent } from "../sort-button/sort-button.component"; +import { commonTestImports, commonTestProviders } from "../../../../common/testing/test-utils"; + import { UserDatasetComponent } from "./user-dataset.component"; import { USER_DATASET } from "../../../../app-routing.constant"; import { UserDatasetVersionCreatorComponent } from "./user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component"; @@ -368,3 +382,142 @@ describe("UserDatasetComponent", () => { }); }); }); +/** + * The existing suite constructs the component directly, so its template has never been rendered. + * These tests mount it for real: the view toggle, the sort wiring and the bindings handed to the + * results list all live only in the template. + */ +/** Mirrors UserDatasetComponent's private static VIEW_MODE_STORAGE_KEY. */ +const VIEW_MODE_STORAGE_KEY = "texera.userDataset.viewMode"; + +describe("UserDatasetComponent rendering", () => { + let fixture: ComponentFixture<UserDatasetComponent>; + let component: UserDatasetComponent; + let searchSpy: ReturnType<typeof vi.fn>; + + beforeEach(async () => { + // viewType is seeded from localStorage at construction, so a view chosen by an earlier test + // would leak into this one. Only this component's key is removed, so nothing else in the + // shared jsdom store is disturbed. + localStorage.removeItem(VIEW_MODE_STORAGE_KEY); + searchSpy = vi.fn(() => of({ entries: [], more: false, hasMismatch: false })); + await TestBed.configureTestingModule({ + imports: [UserDatasetComponent, ...commonTestImports], + providers: [ + { provide: NzModalService, useValue: { create: vi.fn() } }, + { provide: UserService, useClass: StubUserService }, + { provide: SearchService, useValue: { executeSearch: searchSpy } }, + { provide: DatasetService, useValue: { deleteDatasets: vi.fn(() => of({} as Response)) } }, + { provide: NzMessageService, useValue: { warning: vi.fn() } }, + // ng-zorro defaults to zh-cn and throws NG0701 without locale data; the app registers en_US. + { provide: NZ_I18N, useValue: en_US }, + provideRouter([]), + ...commonTestProviders, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(UserDatasetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + // Also on the way out: the persistence test leaves a chosen view behind, and this key is + // shared with the suite above. + localStorage.removeItem(VIEW_MODE_STORAGE_KEY); + vi.restoreAllMocks(); + }); + + /** + * The two view-mode buttons, found by title: the sort button renders its own <button> into the + * same nz-space-compact, so a positional selector picks that up first. + */ + function viewButtons(): { list: HTMLButtonElement; card: HTMLButtonElement } { + const host = fixture.nativeElement as HTMLElement; + const list = host.querySelector<HTMLButtonElement>('button[title="List View"]'); + const card = host.querySelector<HTMLButtonElement>('button[title="Card View"]'); + // Named up front so a renamed title fails as a missing button rather than a null dereference + // three lines later. + expect(list, 'no button titled "List View"').not.toBeNull(); + expect(card, 'no button titled "Card View"').not.toBeNull(); + return { list: list!, card: card! }; + } + + it("starts in card view with only that button highlighted", () => { + // nz-button renders nzType="primary" as ant-btn-primary; the highlight is how the user can tell + // which view they are in, so it has to follow viewType rather than being fixed. + const { list, card } = viewButtons(); + + expect(component.viewType).toBe("card"); + expect(card.classList).toContain("ant-btn-primary"); + expect(list.classList).not.toContain("ant-btn-primary"); + }); + + it("moves the highlight when the list view is chosen", () => { + viewButtons().list.click(); + fixture.detectChanges(); + + const { list, card } = viewButtons(); + expect(component.viewType).toBe("list"); + expect(list.classList).toContain("ant-btn-primary"); + expect(card.classList).not.toContain("ant-btn-primary"); + }); + + it("remembers the chosen view for the next visit", () => { + // The preference is persisted, so a fresh component picks it back up. + viewButtons().list.click(); + + const reopened = TestBed.createComponent(UserDatasetComponent); + expect(reopened.componentInstance.viewType).toBe("list"); + }); + + it("passes the chosen view down to the results list", () => { + const results = fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance; + expect(results.viewMode).toBe("card"); + + component.setViewType("list"); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance.viewMode).toBe("list"); + }); + + it("marks the results list as editable and private", () => { + // This is the user's own dataset page, so entries are editable and the search is scoped to them. + const results = fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance; + + expect(results.editable).toBe(true); + expect(results.isPrivateSearch).toBe(true); + }); + + it("re-runs the search when the sort method changes", () => { + // The template statement does two things — assign then search — and dropping either leaves the + // list showing results in the previous order. + const sortButton = fixture.debugElement.query(By.directive(SortButtonComponent)); + searchSpy.mockClear(); + + sortButton.componentInstance.sortMethodChange.emit(SortMethod.NameAsc); + fixture.detectChanges(); + + expect(component.sortMethod).toBe(SortMethod.NameAsc); + expect(searchSpy).toHaveBeenCalled(); + }); + + it("opens the create-dataset flow from the toolbar button", () => { + const spy = vi.spyOn(component, "onClickOpenDatasetAddComponent").mockImplementation(() => {}); + const host = fixture.nativeElement as HTMLElement; + const create = host.querySelector<HTMLButtonElement>("button.create-btn"); + expect(create, "no button matching .create-btn").not.toBeNull(); + + create!.click(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it("hides the sort button's execution-time options on the dataset page", () => { + // Datasets have no executions, so those sort options must not be offered. + const sortButton = fixture.debugElement.query(By.directive(SortButtonComponent)).componentInstance; + + expect(sortButton.showEditTime).toBe(false); + expect(sortButton.showExecutionTime).toBe(false); + }); +});
