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");
+    });
+  });
 });

Reply via email to