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