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-7968-c22f6172d93326cbb4d13b30c4178805e98e7e1f in repository https://gitbox.apache.org/repos/asf/texera.git
commit 996fe6ea16b7a409cb5adfbbb206801fe28dd9e2 Author: Meng Wang <[email protected]> AuthorDate: Tue Aug 25 22:15:41 2026 +0000 test(frontend): drive the dashboard list item's controls from the DOM (#7968) ### What changes were proposed in this PR? Extends `list-item.component.spec.ts` so the card's controls are fired from the rendered markup instead of by calling the handlers directly. This takes `list-item.component.html` to full coverage. Measured locally with `--coverage` over the 15 specs that render `ListItemComponent`, so the figures line up with codecov's aggregate rather than with a single-spec run: | `list-item.component.html` | Before | After | | --- | --- | --- | | lines | 88/96 (91.67 %) | **96/96 (100 %)** | | branches | 7/10 | **10/10** | | functions | 3/17 | **17/17** | The eight lines the issue named — 99-105, 192, 201, 219/221, 231 — were exactly the ones uncovered before, and none is left. - **The inline rename input.** Clicking *Rename* renders it; typing writes back through `[(ngModel)]="entry.name"`; `(blur)` and `(keydown.enter)` each call `confirmUpdateCustomName` with the new name. Its `(click)="$event.stopPropagation()"` is fired too, so typing in the box cannot bubble to the row's `routerLink`. - **The action buttons.** *Detail*, *Share*, *Copy* and *Delete* are each fired from the rendered control. The delete confirmation is the popconfirm's `(nzOnConfirm)` output, which is bound on the button itself — the popup would need a CDK overlay, which jsdom never attaches, so the output is triggered directly and the comment says why. - **The download guard.** `*ngIf="entry.type === 'workflow' || entry.type === 'dataset'"` is rendered for a workflow (and clicked), for a dataset, and for a third type where the button must be absent. - Also covered while the block was open: row hover, the per-row checkbox (its `(change)` and its `stopPropagation` guard), the description editor from both its button and the description line, and the public card's like button in both its disabled and enabled states. Every entry fixture is rebuilt per render, because `confirmUpdateCustomName` mutates `entry.name` in place. No production code was changed. ### Any related issues, documentation, discussions? Closes #7963. The issue originally also covered `filters.component.html`; that half was dropped and the issue trimmed — all nine of its uncovered statements sit inside `<nz-dropdown-menu>`, whose content only exists once a CDK overlay attaches, and the overlay does not attach under jsdom (probed both by clicking the trigger and by driving `nzVisible` through `ngOnChanges` in `fakeAsync`). `filters.component.ts` is already at 100 %, and #7463 deletes the project dropdown that made up part of that half. The reasoning is recorded on the issue. ### How was this PR tested? `ng test --watch=false --include src/app/dashboard/component/user/list-item/list-item.component.spec.ts` — 45 passed (38 before, 7 new), repeated 3× for stability. The 15 specs that render `ListItemComponent` go from 435 to 442 passing, all green. `yarn format:ci` clean. Failure path verified by breaking one assertion in each of the 7 new tests: 7 failed / 38 passed, non-zero exit, no pre-existing test disturbed, then restored to green. Determinism: every control is driven with `triggerEventHandler`, so nothing waits on a real event loop or an overlay; the modal-opening handlers are spied rather than allowed to open; `fixture.destroy()` and `vi.restoreAllMocks()` run in the block's `afterEach`; and no assertion touches rendered dates or geometry. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../user/list-item/list-item.component.spec.ts | 161 +++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts index bf13d35514..9c6211ef0e 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts @@ -18,6 +18,7 @@ */ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { ListItemComponent } from "./list-item.component"; import { DEFAULT_WORKFLOW_NAME, @@ -593,4 +594,164 @@ describe("ListItemComponent", () => { }); }); }); + + /** + * The suites above call the handlers directly; these fire them from the rendered + * markup, so a control that loses its binding fails here. + */ + describe("rendered controls", () => { + const q = (selector: string) => fixture.debugElement.query(By.css(selector)); + const button = (title: string) => q(`button[title="${title}"]`); + + /** + * Renders the card for one entry. The entry is rebuilt per call because the + * rename path mutates `entry.name` in place. + */ + function render(overrides: Record<string, unknown> = {}, isPrivateSearch = true): void { + component.entry = { + id: 7, + name: "item", + description: "", + type: "workflow", + workflow: { isOwner: true }, + dataset: { isOwner: true }, + accessibleUserIds: [], + likeCount: 0, + viewCount: 0, + isLiked: false, + size: 0, + ...overrides, + } as unknown as DashboardEntry; + component.isPrivateSearch = isPrivateSearch; + component.ngOnChanges({ entry: {} as any }); + fixture.detectChanges(); + } + + afterEach(() => { + fixture.destroy(); + vi.restoreAllMocks(); + }); + + it("renames through the inline input, confirming on blur and on enter", () => { + const confirm = vi.spyOn(component, "confirmUpdateCustomName").mockImplementation(() => {}); + render(); + + expect(q("input.resource-name-edit-input")).toBeNull(); + button("Rename").triggerEventHandler("click", new MouseEvent("click")); + fixture.detectChanges(); + + const input = q("input.resource-name-edit-input"); + expect(input).not.toBeNull(); + input.nativeElement.value = "renamed"; + input.nativeElement.dispatchEvent(new Event("input")); + fixture.detectChanges(); + // The two-way binding writes straight back onto the entry. + expect(component.entry.name).toBe("renamed"); + + input.triggerEventHandler("blur", null); + expect(confirm).toHaveBeenLastCalledWith("renamed"); + + input.triggerEventHandler("keydown.enter", null); + expect(confirm).toHaveBeenCalledTimes(2); + + // Clicking inside the input must not bubble to the row's routerLink. + const click = { stopPropagation: vi.fn() }; + input.triggerEventHandler("click", click); + expect(click.stopPropagation).toHaveBeenCalledTimes(1); + }); + + it("opens the description editor from its button and from the description line", () => { + const edit = vi.spyOn(component, "onEditDescription").mockImplementation(() => {}); + render({ description: "hello" }); + + button("Edit Description").triggerEventHandler("click", new MouseEvent("click")); + q(".resource-description").triggerEventHandler("click", new MouseEvent("click")); + + expect(edit).toHaveBeenCalledTimes(2); + }); + + it("tracks hover over the row", () => { + render(); + const row = q("div[nz-row]"); + + row.triggerEventHandler("mouseenter", null); + expect(component.hovering).toBe(true); + + row.triggerEventHandler("mouseleave", null); + expect(component.hovering).toBe(false); + }); + + it("toggles the row checkbox of a private workflow entry", () => { + let changes = 0; + component.checkboxChanged.subscribe(() => changes++); + render(); + + const checkbox = q("input.large-checkbox"); + expect(checkbox).not.toBeNull(); + checkbox.triggerEventHandler("change", null); + fixture.detectChanges(); + + expect(component.entry.checked).toBe(true); + expect(changes).toBe(1); + expect(q("input.large-checkbox").nativeElement.checked).toBe(true); + + // Ticking the box must not bubble to the row's routerLink. + const click = { stopPropagation: vi.fn() }; + q("input.large-checkbox").triggerEventHandler("click", click); + expect(click.stopPropagation).toHaveBeenCalledTimes(1); + }); + + it("wires the detail, share, copy and delete controls", () => { + const detail = vi.spyOn(component, "openDetailModal").mockImplementation(() => {}); + const share = vi.spyOn(component, "onClickOpenShareAccess").mockResolvedValue(undefined); + let duplicated = 0; + let deleted = 0; + component.duplicated.subscribe(() => duplicated++); + component.deleted.subscribe(() => deleted++); + render(); + + button("Detail").triggerEventHandler("click", null); + button("Share").triggerEventHandler("click", null); + button("Copy").triggerEventHandler("click", null); + // The popconfirm popup itself needs a CDK overlay, which jsdom never attaches; + // the confirmation output is bound on the button, so it is fired directly. + button("Delete").triggerEventHandler("nzOnConfirm", null); + + expect(detail).toHaveBeenCalledWith(7); + expect(share).toHaveBeenCalledTimes(1); + expect(duplicated).toBe(1); + expect(deleted).toBe(1); + }); + + it("offers the download button to workflows and datasets only", () => { + const download = vi.spyOn(component, "onClickDownload").mockImplementation(() => {}); + + render(); + button("Download").triggerEventHandler("click", null); + expect(download).toHaveBeenCalledTimes(1); + + render({ type: "dataset" }); + expect(button("Download")).not.toBeNull(); + + render({ type: "file" }); + expect(button("Download")).toBeNull(); + }); + + it("likes from the public card and disables the button without a signed-in user", () => { + const like = vi.spyOn(component, "toggleLike").mockImplementation(() => {}); + render({ likeCount: 12 }, false); + + // No current user: the control renders but is disabled. + expect(q("button.like-button").nativeElement.disabled).toBe(true); + + component.currentUid = 1; + fixture.detectChanges(); + const likeButton = q("button.like-button"); + expect(likeButton.nativeElement.disabled).toBe(false); + likeButton.triggerEventHandler("click", new MouseEvent("click")); + + expect(like).toHaveBeenCalledTimes(1); + expect(likeButton.nativeElement.textContent).toContain("12"); + }); + }); });
