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-6780-aba6e6e162dc9da8fe23c41b4063253403a98c3d in repository https://gitbox.apache.org/repos/asf/texera.git
commit b405528cc4000e69ec1473b08f288b3a1c46ae88 Author: Xinyuan Lin <[email protected]> AuthorDate: Wed Jul 22 20:11:33 2026 -0700 test(frontend): extend CardItemComponent unit test coverage (#6780) ### What changes were proposed in this PR? Extends `card-item.component.spec.ts` for `CardItemComponent`, covering the entry-type render branches and the open/duplicate/delete/share/like action handlers, including the workflow and dataset share-access modal flows (with the modal `refresh` stream). Coverage 83% -> high. No existing tests modified. ### Any related issues, documentation, discussions? Closes #6774. ### How was this PR tested? `ng test --include='**/card-item.component.spec.ts'` -> 64/64 passing. `yarn format:ci` passes. ### 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 | 267 ++++++++++++++++++++- 1 file changed, 263 insertions(+), 4 deletions(-) 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 0ed28e6fd9..3e6b654804 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 @@ -20,10 +20,13 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; import { CardItemComponent } from "./card-item.component"; import { ActionType, EntityType, HubService } from "src/app/hub/service/hub.service"; -import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; +import { + DEFAULT_WORKFLOW_NAME, + WorkflowPersistService, +} from "src/app/common/service/workflow-persist/workflow-persist.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzModalService } from "ng-zorro-antd/modal"; -import { of, throwError } from "rxjs"; +import { of, throwError, Subject } from "rxjs"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterTestingModule } from "@angular/router/testing"; import { StubUserService } from "../../../../../common/service/user/stub-user.service"; @@ -31,10 +34,17 @@ import { UserService } from "../../../../../common/service/user/user.service"; import { commonTestProviders } from "../../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; import { DashboardEntry } from "src/app/dashboard/type/dashboard-entry"; -import { HUB_WORKFLOW_RESULT_DETAIL, USER_PROJECT, USER_WORKSPACE } from "../../../../../app-routing.constant"; +import { + HUB_DATASET_RESULT_DETAIL, + HUB_WORKFLOW_RESULT_DETAIL, + USER_DATASET, + USER_PROJECT, + USER_WORKSPACE, +} from "../../../../../app-routing.constant"; import { WorkflowCoverService } from "src/app/dashboard/service/user/workflow-cover/workflow-cover.service"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; -import { DatasetService } from "../../../../service/user/dataset/dataset.service"; +import { DatasetService, DEFAULT_DATASET_NAME } from "../../../../service/user/dataset/dataset.service"; +import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; function makeWorkflowEntry(overrides: Partial<DashboardEntry> = {}): DashboardEntry { return { @@ -635,4 +645,253 @@ describe("CardItemComponent", () => { expect(postLikeSpy).not.toHaveBeenCalled(); }); + + describe("extended coverage", () => { + it("entry getter throws when no entry has been provided", () => { + component.entry = undefined as any; + expect(() => component.entry).toThrow("entry property must be provided."); + }); + + it("initializeEntry routes an owning dataset user to the user dataset view", () => { + component.currentUid = 42; + component.entry = makeDatasetEntry({ + id: 5, + dataset: { isOwner: true }, + accessibleUserIds: [42], + coverImageUrl: undefined, // skips the cover fetch + size: 55, + } as any); + + component.initializeEntry(); + + expect(component.entryLink).toEqual([USER_DATASET, "5"]); + expect(component.iconType).toBe("database"); + expect(component.disableDelete).toBe(false); // owner + expect(component.size).toBe(55); + }); + + it("initializeEntry routes a non-owning dataset user to the hub dataset detail view", () => { + component.currentUid = 42; + component.entry = makeDatasetEntry({ + id: 5, + dataset: { isOwner: false }, + accessibleUserIds: [99], + coverImageUrl: undefined, + } as any); + + component.initializeEntry(); + + expect(component.entryLink).toEqual([HUB_DATASET_RESULT_DETAIL, "5"]); + expect(component.disableDelete).toBe(true); // !isOwner + }); + + it("onClickDownload downloads a workflow via the download service", () => { + const downloadService = TestBed.inject(DownloadService); + const downloadWorkflowSpy = vi.spyOn(downloadService, "downloadWorkflow").mockReturnValue(of({} as any)); + component.entry = makeWorkflowEntry({ id: 7, workflow: { isOwner: true, workflow: { name: "myflow" } } } as any); + + component.onClickDownload(); + + expect(downloadWorkflowSpy).toHaveBeenCalledWith(7, "myflow"); + }); + + it("onClickDownload downloads a dataset via the download service", () => { + const downloadService = TestBed.inject(DownloadService); + const downloadDatasetSpy = vi.spyOn(downloadService, "downloadDataset").mockReturnValue(of(new Blob())); + component.entry = makeDatasetEntry({ id: 5, name: "mydataset", coverImageUrl: undefined }); + + component.onClickDownload(); + + expect(downloadDatasetSpy).toHaveBeenCalledWith(5, "mydataset"); + }); + + it("onClickDownload is a no-op when the entry has no id", () => { + const downloadService = TestBed.inject(DownloadService); + const downloadWorkflowSpy = vi.spyOn(downloadService, "downloadWorkflow"); + const downloadDatasetSpy = vi.spyOn(downloadService, "downloadDataset"); + component.entry = makeWorkflowEntry({ id: undefined }); + + component.onClickDownload(); + + expect(downloadWorkflowSpy).not.toHaveBeenCalled(); + expect(downloadDatasetSpy).not.toHaveBeenCalled(); + }); + + it("onClickOpenShareAccess opens the workflow share modal and forwards refresh events", async () => { + const modalService = TestBed.inject(NzModalService); + const refresh$ = new Subject<void>(); + const createSpy = vi + .spyOn(modalService, "create") + .mockReturnValue({ componentInstance: { refresh: refresh$ } } as any); + (workflowPersistService as any).retrieveOwners = vi.fn().mockReturnValue(of(["alice", "bob"])); + component.entry = makeWorkflowEntry({ id: 7, workflow: { isOwner: true, accessLevel: "WRITE" } } as any); + + await component.onClickOpenShareAccess(); + + expect(createSpy).toHaveBeenCalledTimes(1); + const cfg = createSpy.mock.calls[0][0]; + expect(cfg.nzData).toEqual({ + writeAccess: true, + type: "workflow", + id: 7, + allOwners: ["alice", "bob"], + inWorkspace: false, + }); + expect(cfg.nzTitle).toBe("Share this workflow with others"); + + const refreshSpy = vi.fn(); + const refreshSub = component.refresh.subscribe(refreshSpy); + refresh$.next(); + expect(refreshSpy).toHaveBeenCalledTimes(1); + refreshSub.unsubscribe(); + }); + + it("onClickOpenShareAccess opens the dataset share modal with dataset-specific data", async () => { + const modalService = TestBed.inject(NzModalService); + const createSpy = vi + .spyOn(modalService, "create") + .mockReturnValue({ componentInstance: { refresh: new Subject<void>() } } as any); + (datasetService as any).retrieveOwners = vi.fn().mockReturnValue(of(["carol"])); + component.entry = makeDatasetEntry({ id: 5, accessLevel: "READ", coverImageUrl: undefined } as any); + + await component.onClickOpenShareAccess(); + + expect(createSpy).toHaveBeenCalledTimes(1); + const cfg = createSpy.mock.calls[0][0]; + expect(cfg.nzData).toEqual({ + writeAccess: false, // accessLevel is READ, not WRITE + type: "dataset", + id: 5, + allOwners: ["carol"], + }); + expect(cfg.nzTitle).toBe("Share this dataset with others"); + }); + + it("onClickOpenShareAccess does not open a modal for a non-shareable entry type", async () => { + const modalService = TestBed.inject(NzModalService); + const createSpy = vi.spyOn(modalService, "create"); + component.entry = { + id: 3, + name: "proj", + type: "project", + likeCount: 0, + viewCount: 0, + isLiked: false, + } as unknown as DashboardEntry; + + await component.onClickOpenShareAccess(); + + expect(createSpy).not.toHaveBeenCalled(); + }); + + it("confirmUpdateCustomName surfaces a missing-id error and skips the update", () => { + const notificationService = TestBed.inject(NotificationService); + const errorSpy = vi.spyOn(notificationService, "error"); + component.entry = makeWorkflowEntry({ id: undefined, name: "current" }); + component.originalName = "old"; // differs from current, so the update path runs + + component.confirmUpdateCustomName("new-name"); + + expect(errorSpy).toHaveBeenCalledWith("Id is missing"); + expect(workflowPersistService.updateWorkflowName).not.toHaveBeenCalled(); + }); + + it("confirmUpdateCustomName falls back to the default workflow name when the new name is blank", () => { + component.entry = makeWorkflowEntry({ id: 1, name: "current" }); + component.originalName = "old"; + workflowPersistService.updateWorkflowName.mockReturnValue(of({} as Response)); + + component.confirmUpdateCustomName(""); + + expect(workflowPersistService.updateWorkflowName).toHaveBeenCalledWith(1, DEFAULT_WORKFLOW_NAME); + }); + + it("confirmUpdateCustomName falls back to the default dataset name when the new name is blank", () => { + component.entry = makeDatasetEntry({ id: 5, name: "current" }); + component.originalName = "old"; + datasetService.updateDatasetName.mockReturnValue(of({} as any)); + + component.confirmUpdateCustomName(""); + + expect(datasetService.updateDatasetName).toHaveBeenCalledWith(5, DEFAULT_DATASET_NAME); + }); + + it("confirmUpdateCustomDescription updates a dataset description via the dataset service", () => { + (datasetService as any).updateDatasetDescription = vi.fn().mockReturnValue(of({} as any)); + component.entry = makeDatasetEntry({ id: 5, description: "current" }); + component.originalDescription = "old"; + + component.confirmUpdateCustomDescription("new description"); + + expect((datasetService as any).updateDatasetDescription).toHaveBeenCalledWith(5, "new description"); + expect(component.entry.description).toBe("new description"); + expect(component.editingDescription).toBe(false); + }); + + it("confirmUpdateCustomDescription writes an empty string when the description is undefined", () => { + component.entry = makeWorkflowEntry({ id: 1, description: "current" }); + component.originalDescription = "old"; + workflowPersistService.updateWorkflowDescription.mockReturnValue(of({} as Response)); + + component.confirmUpdateCustomDescription(undefined); + + expect(workflowPersistService.updateWorkflowDescription).toHaveBeenCalledWith(1, ""); + }); + + it("confirmUpdateCustomDescription falls back to an empty string when the update fails without an original", () => { + component.entry = makeWorkflowEntry({ id: 1, description: "current" }); + component.originalDescription = undefined; + workflowPersistService.updateWorkflowDescription.mockReturnValue(throwError(() => new Error("boom"))); + + component.confirmUpdateCustomDescription("new description"); + + expect(component.entry.description).toBe(""); // originalValue undefined -> "" + expect(component.editingDescription).toBe(false); + }); + + it("openDetailModal defaults the bumped view count to 1 when no counts are returned", () => { + const modalService = TestBed.inject(NzModalService); + const hubService = TestBed.inject(HubService); + vi.spyOn(modalService, "create").mockReturnValue({ componentInstance: {} } as any); + vi.spyOn(hubService, "getCounts").mockReturnValue(of([] as any)); + + component.entry = makeWorkflowEntry({ id: 7 }); + component.viewCount = 99; + component.openDetailModal(7); + + expect(component.viewCount).toBe(1); // (undefined ?? 0) + 1 + }); + + it("toggleLike defaults the like count to 0 when the refreshed count is missing", () => { + const hubService = TestBed.inject(HubService); + vi.spyOn(hubService, "postLike").mockReturnValue(of(true)); + vi.spyOn(hubService, "getCounts").mockReturnValue(of([{ counts: {} }] as any)); + + component.currentUid = 42; + component.entry = makeWorkflowEntry({ id: 7 }); + component.isLiked = false; + component.likeCount = 5; + + component.toggleLike(); + + expect(component.isLiked).toBe(true); + expect(component.likeCount).toBe(0); + }); + + it("toggleLike defaults the like count to 0 after an unlike when the refreshed count is missing", () => { + const hubService = TestBed.inject(HubService); + vi.spyOn(hubService, "postUnlike").mockReturnValue(of(true)); + vi.spyOn(hubService, "getCounts").mockReturnValue(of([{ counts: {} }] as any)); + + component.currentUid = 42; + component.entry = makeWorkflowEntry({ id: 7 }); + component.isLiked = true; + component.likeCount = 5; + + component.toggleLike(); + + expect(component.isLiked).toBe(false); + expect(component.likeCount).toBe(0); + }); + }); });
