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-7336-26aa6caa70067ffaf4493b798460878bada226b5
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 5265f207065dd06f6fd70467402c7843d9379e02
Author: Meng Wang <[email protected]>
AuthorDate: Thu Aug 6 17:07:18 2026 -0700

    test(frontend): extend CardItemComponent template coverage (#7336)
    
    ### What changes were proposed in this PR?
    
    Extends `card-item.component.spec.ts` to render the
    previously-unexercised half
    of `card-item.component.html`. The class was already ~96% covered but
    the
    template sat at ~50% — the existing tests call the handlers directly
    rather than
    rendering the branches. This adds 8 tests that drive each template arm
    through
    the DOM (`detectChanges()` +
    `debugElement.query(By.css(...)).triggerEventHandler(...)`):
    
    - **Private-search mode** (workflow) — the checkbox overlay, edit-name
    button, and
    the Detail / Share / Copy / Download / Delete actions render and their
    clicks
      reach the right handler or `@Output`; the like button is absent here.
    - **Cover-image controls** (`canEditCover`) — the camera and reset
    buttons render
      and wire up, and the hidden file input's `(change)` fires.
    - **Name editing** — the edit button swaps the name display for the
    input, and
      Enter confirms the rename.
    - **Non-private mode** — the like button renders, toggles on click,
    shows the
      `liked` class, and is disabled without a current user.
    - **Dataset in private mode** — Download shows while Detail / Copy /
    checkbox are
      hidden (per-type `*ngIf` gating).
    - **Size row + cover-image error** handler.
    
    This takes the template from ~50% to **105/106 lines (99%)**. The one
    remaining
    line is the `(keydown.enter)` binding on the rename input: the handler
    is
    exercised (the test asserts the rename fires on Enter), but the coverage
    tool
    does not credit that key-filtered event line. No production code was
    changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7330.
    
    ### How was this PR tested?
    
    `ng test --watch=false --include
    
src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts`
    — 72 passed. `eslint` and `prettier --check` clean. Coverage report
    confirms the template at 105/106 lines. Failure path verified by
    breaking an assertion (→ non-zero exit) and restoring. Per the issue's
    determinism note, no `vi.useFakeTimers()` — `onEditName`'s `setTimeout`
    focus never runs in the synchronous test body.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../card-item/card-item.component.spec.ts          | 156 +++++++++++++++++++++
 1 file changed, 156 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts
index 3e6b654804..fbc393d345 100644
--- 
a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts
@@ -28,6 +28,7 @@ import { HttpClientTestingModule } from 
"@angular/common/http/testing";
 import { NzModalService } from "ng-zorro-antd/modal";
 import { of, throwError, Subject } from "rxjs";
 import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
+import { By } from "@angular/platform-browser";
 import { RouterTestingModule } from "@angular/router/testing";
 import { StubUserService } from 
"../../../../../common/service/user/stub-user.service";
 import { UserService } from "../../../../../common/service/user/user.service";
@@ -894,4 +895,159 @@ describe("CardItemComponent", () => {
       expect(component.likeCount).toBe(0);
     });
   });
+
+  describe("template rendering", () => {
+    // Query, assert the element is present, then dispatch the event — a real
+    // MouseEvent for clicks so handlers calling 
stopPropagation()/preventDefault() work.
+    const fire = (css: string, event: string, payload: unknown): void => {
+      const el = fixture.debugElement.query(By.css(css));
+      expect(el).toBeTruthy();
+      el.triggerEventHandler(event, payload);
+    };
+
+    it("renders the full private-search action set for an owned workflow (and 
no like button)", () => {
+      component.entry = makeWorkflowEntry();
+      component.isPrivateSearch = true;
+      component.currentUid = 1;
+      fixture.detectChanges();
+
+      const de = fixture.debugElement;
+      expect(de.query(By.css(".card-checkbox"))).toBeTruthy();
+      expect(de.query(By.css(".edit-btn"))).toBeTruthy();
+      expect(de.query(By.css('button[title="Detail"]'))).toBeTruthy();
+      expect(de.query(By.css('button[title="Share"]'))).toBeTruthy();
+      expect(de.query(By.css('button[title="Copy"]'))).toBeTruthy();
+      expect(de.query(By.css('button[title="Download"]'))).toBeTruthy();
+      expect(de.query(By.css(".delete-btn"))).toBeTruthy();
+      // the like button is only rendered in non-private mode
+      expect(de.query(By.css(".like-btn"))).toBeNull();
+    });
+
+    it("wires each private-search action click to its handler / output", () => 
{
+      component.entry = makeWorkflowEntry();
+      component.isPrivateSearch = true;
+      component.currentUid = 1;
+      fixture.detectChanges();
+
+      const detailSpy = vi.spyOn(component, 
"openDetailModal").mockImplementation(() => {});
+      const shareSpy = vi.spyOn(component, 
"onClickOpenShareAccess").mockImplementation(async () => {});
+      const downloadSpy = vi.spyOn(component, 
"onClickDownload").mockImplementation(async () => {});
+      let duplicated = false;
+      component.duplicated.subscribe(() => (duplicated = true));
+      let deleted = false;
+      component.deleted.subscribe(() => (deleted = true));
+
+      fire('button[title="Detail"]', "click", new MouseEvent("click"));
+      fire('button[title="Share"]', "click", new MouseEvent("click"));
+      fire('button[title="Download"]', "click", new MouseEvent("click"));
+      fire('button[title="Copy"]', "click", new MouseEvent("click"));
+      fire(".delete-btn", "nzOnConfirm", undefined);
+
+      expect(detailSpy).toHaveBeenCalled();
+      expect(shareSpy).toHaveBeenCalled();
+      expect(downloadSpy).toHaveBeenCalled();
+      expect(duplicated).toBe(true);
+      expect(deleted).toBe(true);
+    });
+
+    it("enters name-editing mode from the edit button and swaps the display 
for the input", () => {
+      component.entry = makeWorkflowEntry();
+      component.isPrivateSearch = true;
+      fixture.detectChanges();
+
+      // Trigger via the DOM; onEditName sets editingName synchronously (its 
setTimeout
+      // focus callback never runs in this synchronous test — no fake timers 
needed).
+      fire(".edit-btn", "click", new MouseEvent("click"));
+      expect(component.editingName).toBe(true);
+
+      fixture.detectChanges();
+      
expect(fixture.debugElement.query(By.css(".resource-name-edit-input"))).toBeTruthy();
+      expect(fixture.debugElement.query(By.css(".resource-name"))).toBeNull();
+
+      // pressing Enter in the edit input confirms the rename (real key event 
so the
+      // Angular keydown.enter binding fires through its event plugin)
+      const confirmSpy = vi.spyOn(component, 
"confirmUpdateCustomName").mockImplementation(() => {});
+      const editInput = 
fixture.debugElement.query(By.css(".resource-name-edit-input"))
+        .nativeElement as HTMLInputElement;
+      editInput.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
+      expect(confirmSpy).toHaveBeenCalled();
+    });
+
+    it("renders and wires the cover-image controls when the cover is 
editable", () => {
+      // A workflow entry with a cover url makes the component compute 
hasCustomImage = true
+      // through its public entry input, so we don't reach into the private 
customImage field.
+      component.entry = makeWorkflowEntry({ coverImageUrl: 
"http://example.com/cover.png"; });
+      component.isPrivateSearch = true;
+      component.initializeEntry(); // process the entry input (mirrors the 
ngOnChanges path)
+      fixture.detectChanges();
+      expect(component.canEditCover).toBe(true);
+      expect(component.hasCustomImage).toBe(true);
+
+      const cameraSpy = vi.spyOn(component, 
"openImagePicker").mockImplementation(() => {});
+      const resetSpy = vi.spyOn(component, "resetImage").mockImplementation(() 
=> {});
+      fire('button[title="Change cover image"]', "click", new 
MouseEvent("click"));
+      fire('button[title="Reset to default image"]', "click", new 
MouseEvent("click"));
+
+      expect(cameraSpy).toHaveBeenCalled();
+      expect(resetSpy).toHaveBeenCalled();
+
+      // selecting a file fires the hidden input's (change) handler
+      const imageSelectedSpy = vi.spyOn(component, 
"onImageSelected").mockImplementation(async () => {});
+      fire('input[type="file"]', "change", { target: { files: [] } });
+      expect(imageSelectedSpy).toHaveBeenCalled();
+    });
+
+    it("renders the like button in non-private mode and toggles like on 
click", () => {
+      component.entry = makeWorkflowEntry();
+      component.isPrivateSearch = false;
+      component.currentUid = 1;
+      component.isLiked = false;
+      fixture.detectChanges();
+
+      const likeBtn = fixture.debugElement.query(By.css(".like-btn"));
+      expect(likeBtn).toBeTruthy();
+      expect(fixture.debugElement.query(By.css(".card-checkbox"))).toBeNull();
+      
expect(fixture.debugElement.query(By.css(".private-actions"))).toBeNull();
+
+      const toggleSpy = vi.spyOn(component, 
"toggleLike").mockImplementation(() => {});
+      likeBtn.triggerEventHandler("click", new MouseEvent("click"));
+      expect(toggleSpy).toHaveBeenCalled();
+    });
+
+    it("reflects liked state and disables the like button without a current 
user", () => {
+      component.entry = makeWorkflowEntry();
+      component.isPrivateSearch = false;
+      component.isLiked = true;
+      component.currentUid = undefined;
+      fixture.detectChanges();
+
+      const likeBtn = 
fixture.debugElement.query(By.css(".like-btn")).nativeElement as 
HTMLButtonElement;
+      expect(likeBtn.classList.contains("liked")).toBe(true);
+      expect(likeBtn.disabled).toBe(true);
+    });
+
+    it("shows Download but hides Detail/Copy/checkbox for a dataset in private 
mode", () => {
+      component.entry = makeDatasetEntry();
+      component.isPrivateSearch = true;
+      fixture.detectChanges();
+
+      const de = fixture.debugElement;
+      expect(de.query(By.css('button[title="Download"]'))).toBeTruthy();
+      expect(de.query(By.css('button[title="Share"]'))).toBeTruthy();
+      expect(de.query(By.css('button[title="Detail"]'))).toBeNull();
+      expect(de.query(By.css('button[title="Copy"]'))).toBeNull();
+      expect(de.query(By.css(".card-checkbox"))).toBeNull();
+    });
+
+    it("renders the size row when a size is set and handles a cover-image load 
error", () => {
+      component.entry = makeWorkflowEntry();
+      component.size = 2048;
+      fixture.detectChanges();
+      
expect(fixture.debugElement.query(By.css('span[title="Size"]'))).toBeTruthy();
+
+      const errorSpy = vi.spyOn(component, 
"onCoverError").mockImplementation(() => {});
+      fire(".card-preview-image", "error", {});
+      expect(errorSpy).toHaveBeenCalled();
+    });
+  });
 });

Reply via email to