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

Reply via email to