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-7443-08a2eac8eb7151eaac33b18cd8a47a20599e5fa7 in repository https://gitbox.apache.org/repos/asf/texera.git
commit ae3ad45c45712e22625a5e30516cc1a31df8c5de Author: Xinyuan Lin <[email protected]> AuthorDate: Sun Aug 9 16:57:35 2026 -0700 test(frontend): render the dataset list item's permission gates (#7443) ### What changes were proposed in this PR? Whether this row offers any editing is decided in the template by **two** conditions, not one: ```html *ngIf="editable && entry.accessPrivilege === 'WRITE'" ``` The list being editable is not on its own permission to change someone else's dataset. The existing suite exercises the component's methods and never renders, so neither condition was pinned. Adds 8 tests covering both halves independently — a reader on an editable list gets no rename or add-description control, and neither does a writer on a non-editable list — plus the same pair guarding the inline description, the owner and shared-access markers being mutually exclusive, the shared marker naming the privilege held, and the rename input being seeded from the dataset's name. **Verified by mutation**, all reverted (template diff empty): | Mutation | Result | |---|---| | rename gate drops the WRITE check | red | | rename gate drops the editable check | red | | description gate drops the WRITE check | red | | inline description gate drops the WRITE check | red | | show the owner marker to everyone | red | | show the shared marker to the owner too | red | | invert the name / edit-input branch | red | | seed the rename input from the description | red | Testing both halves separately is the point: dropping either condition alone still leaves a single-condition test passing. The shared-marker mutation **survived its first run** — the owner test asserted its own marker was present but not that the shared one was absent. It is now exclusive. No production file is touched. ### Any related issues, documentation, discussions? Closes #7440 ### How was this PR tested? ``` npx ng test --watch=false --include="**/user-dataset-list-item.component.spec.ts" ``` ``` Test Files 1 passed (1) Tests 28 passed (28) ``` 8 new on top of the existing 20. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --- .../user-dataset-list-item.component.spec.ts | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts index 513d803b87..b023f10f3f 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts @@ -19,10 +19,12 @@ import { Component, EventEmitter, ViewChild } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { provideRouter } from "@angular/router"; import { NzListComponent } from "ng-zorro-antd/list"; import { NzModalService } from "ng-zorro-antd/modal"; +import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { of, throwError } from "rxjs"; import type { Mocked } from "vitest"; import { UserDatasetListItemComponent } from "./user-dataset-list-item.component"; @@ -308,4 +310,115 @@ describe("UserDatasetListItemComponent", () => { expect(component.refresh).toBeInstanceOf(EventEmitter); }); }); + /** + * Whether this row offers any editing is decided in the template, and by TWO conditions rather + * than one: the list must be editable AND the viewer must hold WRITE on the dataset. The suite + * above exercises the component's methods and never renders, so neither condition was pinned. + */ + describe("rendered row", () => { + /** Re-renders the host with the given entry and list-level editability. */ + function render(over: Partial<DashboardDataset> = {}, editable = true): HTMLElement { + fixture.componentInstance.entry = makeEntry(over); + fixture.componentInstance.editable = editable; + fixture.detectChanges(); + component = fixture.componentInstance.inner; + return fixture.nativeElement as HTMLElement; + } + + /** Titles of every tooltip on the row; interpolated ones never reach the DOM as attributes. */ + function tooltipTitles(): unknown[] { + return fixture.debugElement + .queryAll(By.directive(NzTooltipDirective)) + .map(d => (d.injector.get(NzTooltipDirective) as NzTooltipDirective).directiveTitle); + } + + function hasTooltip(pred: (t: string) => boolean): boolean { + return tooltipTitles().some(t => typeof t === "string" && pred(t)); + } + + it("offers the editing controls to a writer on an editable list", () => { + render({ accessPrivilege: "WRITE" }, true); + + expect(hasTooltip(t => t === "Customize Dataset Name")).toBe(true); + expect(hasTooltip(t => t === "Add Description")).toBe(true); + }); + + it("withholds them from a reader, even on an editable list", () => { + // READ access must not be offered a rename it cannot persist; the list being editable is not + // on its own permission to change someone else's dataset. + render({ accessPrivilege: "READ" }, true); + + expect(hasTooltip(t => t === "Customize Dataset Name")).toBe(false); + expect(hasTooltip(t => t === "Add Description")).toBe(false); + }); + + it("withholds them on a non-editable list, even from a writer", () => { + // Both controls carry the same pair of conditions, so both are asserted: checking only the + // rename would let the add-description button lose its `editable` half unnoticed. + render({ accessPrivilege: "WRITE" }, false); + + expect(hasTooltip(t => t === "Customize Dataset Name")).toBe(false); + expect(hasTooltip(t => t === "Add Description")).toBe(false); + }); + + it("applies the same pair of conditions to the inline description", () => { + render({ accessPrivilege: "READ" }, true); + + (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>(".dataset-description-label")?.click(); + fixture.detectChanges(); + + expect(component.editingDescription).toBe(false); + }); + + it("keeps the inline description shut on a non-editable list, even for a writer", () => { + // The other half of the same `&&`. Without this case the gate could be reduced to the + // privilege check alone and every remaining test would still pass. + render({ accessPrivilege: "WRITE" }, false); + + (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>(".dataset-description-label")?.click(); + fixture.detectChanges(); + + expect(component.editingDescription).toBe(false); + }); + + it("opens the inline description for a writer", () => { + render({ accessPrivilege: "WRITE" }, true); + + (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>(".dataset-description-label")?.click(); + fixture.detectChanges(); + + expect(component.editingDescription).toBe(true); + }); + + it("marks a dataset the viewer owns, and only that marker", () => { + // The two markers are mutually exclusive; showing both would tell an owner their own dataset + // had been shared with them. + render({ isOwner: true, accessPrivilege: "WRITE" }); + + expect(hasTooltip(t => t === "You are the owner")).toBe(true); + expect(hasTooltip(t => t.endsWith(" Access"))).toBe(false); + }); + + it("tells a non-owner what access they hold instead", () => { + // The marker interpolates the privilege, so a reader and a writer are told different things. + render({ isOwner: false, accessPrivilege: "READ" }); + + expect(hasTooltip(t => t === "You are the owner")).toBe(false); + expect(hasTooltip(t => t === "READ Access")).toBe(true); + }); + + it("swaps the name for an input once renaming starts", () => { + const el = render({ accessPrivilege: "WRITE" }, true); + expect(el.querySelector("nz-list-item-meta-title input")).toBeNull(); + + component.editingName = true; + fixture.detectChanges(); + + const input = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>( + "nz-list-item-meta-title input" + )!; + expect(input).not.toBeNull(); + expect(input.value).toBe(component.dataset.name); + }); + }); });
