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 03:11:55 UTC
(texera) branch gh-readonly-queue/main/pr-6780-aba6e6e162dc9da8fe23c41b4063253403a98c3d created (now b405528cc4)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-6780-aba6e6e162dc9da8fe23c41b4063253403a98c3d
in repository https://gitbox.apache.org/repos/asf/texera.git
at b405528cc4 test(frontend): extend CardItemComponent unit test coverage (#6780)
This branch includes the following new commits:
new b405528cc4 test(frontend): extend CardItemComponent unit test coverage (#6780)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(frontend): extend CardItemComponent unit test coverage (#6780)
Posted by gi...@apache.org.
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 <xi...@uci.edu>
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);
+ });
+ });
});