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-7419-97dac3c2db0055a5bb98a23d6741ace3fc106742 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 96ed553760a299f2031f4cd83c024e9327cb5346 Author: Meng Wang <[email protected]> AuthorDate: Sat Aug 8 04:13:59 2026 -0700 test(frontend): render DatasetDetailComponent template branches for coverage (#7419) ### What changes were proposed in this PR? Extends the existing `DatasetDetailComponent` spec so the detail view's markup actually renders, covering template branches that were previously never executed (`frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html`). The spec previously drove the class directly and never queried the DOM. No production code was changed. 9 tests drive the template through the DOM: - **Like tag** — likes when logged in, unlikes when already liked, and stays inert (with the `disabled` class) when logged out, exercising the `(click)="isLogin && toggleLike()"` guard. - **Cover image** — the `*ngIf` omits the `<img>` without a cover URL and the `[src]` binding renders it when one is present. - **Right bar** — both arms of the collapse/restore `*ngIf` pair are clicked. - **Settings tab** — the dataset-name `[(ngModel)]` input renders and its Save button routes to the service; both `nz-switch` toggles are present and their change handlers reach `updateDatasetPublicity` / `updateDatasetDownloadable`. - **Contributors** — the `*ngFor` renders the seeded contributor rows. Three component behaviours the tests had to account for, noted in comments so the setup isn't mistaken for boilerplate: - `toggleLike()` early-returns unless `currentUid` is set — the spec's existing `login()` helper supplies it (the stub user service emits before the component subscribes). - `ngOnInit`'s subscriptions reset fields such as `coverImageUrl`, so the helper runs one change-detection pass first, then applies the test state, then renders. - `nz-tabs` only renders the active tab, and the Settings tab is additionally behind `*ngIf="userHasWriteAccess()"`, so an `openTab()` helper switches tabs and the access level is seeded. Per the issue's determinism constraints: no fake timers are introduced, no date/time string is asserted, and no layout or geometry is asserted. ### Any related issues, documentation, discussions? Closes #7409 ### How was this PR tested? Extended unit tests, run locally in `frontend/` (all green; the failure path was verified by breaking an assertion to confirm the suite goes red): ``` ng test --watch=false --include src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts # Test Files 1 passed (1) | Tests 104 passed (104) prettier --write <spec> # clean eslint <spec> # clean ``` ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../dataset-detail.component.spec.ts | 155 +++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts index 43d9481167..63223d125f 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts @@ -18,6 +18,7 @@ */ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { of, Subject, throwError } from "rxjs"; import { NzModalService } from "ng-zorro-antd/modal"; @@ -1673,4 +1674,158 @@ describe("DatasetDetailComponent behavior", () => { expect(datasetServiceStub.updateDatasetContributors).not.toHaveBeenCalled(); }); }); + + // ─── template rendering ──────────────────────────────────────────────────── + // These drive the markup through the DOM (rather than calling handlers directly) + // so the template's bindings and conditional blocks actually execute. + describe("template rendering", () => { + // Renders the component and applies the given state, so each *ngIf arm is exercised. + // The first detectChanges() lets ngOnInit's subscriptions settle — they reset fields + // such as coverImageUrl — so the state is applied afterwards and rendered by a + // second change-detection pass. + const renderWith = (state: Partial<DatasetDetailComponent> = {}): void => { + createComponent(); + fixture.detectChanges(); + Object.assign(component, state); + fixture.detectChanges(); + }; + + const clickByCss = (selector: string): void => { + const el = fixture.debugElement.query(By.css(selector)); + expect(el).toBeTruthy(); + el.triggerEventHandler("click", null); + fixture.detectChanges(); + }; + + // nz-tabs renders only the active tab's content, so a tab must be opened by its + // title before the markup inside it can be queried. + const openTab = (title: string): void => { + const tab = fixture.debugElement + .queryAll(By.css(".ant-tabs-tab")) + .find(el => (el.nativeElement.textContent ?? "").includes(title)); + expect(tab).toBeTruthy(); + tab!.nativeElement.click(); + fixture.detectChanges(); + }; + + it("toggles the like through the like tag when logged in", () => { + // toggleLike() early-returns unless currentUid is set, which login() supplies + createComponent(); + fixture.detectChanges(); + login(); + Object.assign(component, { isLogin: true, did: 5, isLiked: false, likeCount: 1 }); + fixture.detectChanges(); + + clickByCss(".like-tag"); + + expect(hubServiceStub.postLike).toHaveBeenCalled(); + }); + + it("unlikes through the same tag when the dataset is already liked", () => { + createComponent(); + fixture.detectChanges(); + login(); + Object.assign(component, { isLogin: true, did: 5, isLiked: true, likeCount: 2 }); + fixture.detectChanges(); + + clickByCss(".like-tag"); + + expect(hubServiceStub.postUnlike).toHaveBeenCalled(); + }); + + it("does not toggle the like when logged out", () => { + renderWith({ isLogin: false, did: 5, isLiked: false, likeCount: 1 }); + + const likeTag = fixture.debugElement.query(By.css(".like-tag")); + expect(likeTag).toBeTruthy(); + // the template guards the handler with `isLogin &&` + expect(likeTag.nativeElement.classList).toContain("disabled"); + + likeTag.triggerEventHandler("click", null); + + expect(hubServiceStub.postLike).not.toHaveBeenCalled(); + }); + + it("omits the cover image when there is no cover URL", () => { + renderWith({ coverImageUrl: null }); + expect(fixture.debugElement.query(By.css(".dataset-cover-image"))).toBeNull(); + }); + + it("renders the cover image bound to the cover URL", () => { + renderWith({ coverImageUrl: "blob:cover" }); + const img = fixture.debugElement.query(By.css(".dataset-cover-image")); + expect(img).toBeTruthy(); + expect(img.nativeElement.getAttribute("src")).toBe("blob:cover"); + }); + + it("collapses the right bar from the template, then renders the restore control", () => { + renderWith({ isRightBarCollapsed: false }); + openTab("Versions & Files"); + + // both arms of the *ngIf pair are exercised: hide first, then the show button + clickByCss("button[nz-tooltip='Hide the right bar']"); + expect(component.isRightBarCollapsed).toBe(true); + + clickByCss("button[nz-tooltip='Show Tree']"); + expect(component.isRightBarCollapsed).toBe(false); + }); + + it("binds the dataset name input and saves it from the template", () => { + // the Settings tab is behind *ngIf="userHasWriteAccess()" + renderWith({ did: 5, editedDatasetName: "renamed", userDatasetAccessLevel: "WRITE" }); + openTab("Settings"); + + const input = fixture.debugElement.query(By.css(".settings-name-controls input[nz-input]")); + expect(input).toBeTruthy(); + + // drive the [(ngModel)] update path through the DOM + input.nativeElement.value = "typed-name"; + input.nativeElement.dispatchEvent(new Event("input")); + fixture.detectChanges(); + expect(component.editedDatasetName).toBe("typed-name"); + + const saveBtn = fixture.debugElement + .queryAll(By.css("button")) + .find(btn => (btn.nativeElement.textContent ?? "").trim() === "Save"); + expect(saveBtn).toBeTruthy(); + saveBtn!.triggerEventHandler("click", null); + + expect(datasetServiceStub.updateDatasetName).toHaveBeenCalledWith(5, "typed-name"); + }); + + it("renders every contributor row from the list", () => { + renderWith({ + did: 5, + datasetContributors: [ + { name: "Ada", email: "[email protected]", affiliation: "" } as Contributor, + { name: "Grace", email: "[email protected]", affiliation: "" } as Contributor, + ], + }); + + const rendered = fixture.debugElement.nativeElement.textContent ?? ""; + expect(rendered).toContain("Ada"); + expect(rendered).toContain("Grace"); + }); + + it("routes the settings switches' ngModelChange bindings to the service", () => { + renderWith({ + did: 5, + datasetIsPublic: false, + datasetIsDownloadable: true, + userDatasetAccessLevel: "WRITE", + isOwner: true, // the downloadable switch is [nzDisabled]="!isOwner" + }); + openTab("Settings"); + + const switches = fixture.debugElement.queryAll(By.css("nz-switch")); + expect(switches.length).toBeGreaterThanOrEqual(2); + + // fire the template's (ngModelChange) handlers rather than calling the methods + switches[0].triggerEventHandler("ngModelChange", true); + expect(datasetServiceStub.updateDatasetPublicity).toHaveBeenCalledWith(5); + + switches[1].triggerEventHandler("ngModelChange", false); + expect(datasetServiceStub.updateDatasetDownloadable).toHaveBeenCalledWith(5); + }); + }); });
