You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/23 01:07:57 UTC
(texera) 01/01: test(frontend): extend CardItemComponent unit test coverage (#6780)
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-cf52d7cf319fd6248314410f679a8dd45a03d5d9
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 9ffcd749417738b7e21fc0f591aa9a498794a441
Author: Xinyuan Lin <xi...@uci.edu>
AuthorDate: Wed Jul 22 17:30:17 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);
+ });
+ });
});