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-6604-3a671c31ffe7f5a48da5b2e98841b0d55e176acc
in repository https://gitbox.apache.org/repos/asf/texera.git

commit b6cf569c312ff0013bc47e3401be85e1bee05f7f
Author: Meng Wang <[email protected]>
AuthorDate: Mon Jul 20 12:39:25 2026 -0700

    test(frontend): extend UserWorkflowListItemComponent and 
BrowseSectionComponent coverage (#6604)
    
    ### What changes were proposed in this PR?
    
    Extends the existing specs for two components (both asserted little past
    creation) with unit tests for their previously-untested methods, per
    `frontend/TESTING.md`.
    
    **`UserWorkflowListItemComponent`** (codecov ~62%) — 11 new tests:
    - `getProjectIds` returns the entry's project-id set; `isLightColor`
    reports
      light vs dark hex colors.
    - `confirmUpdateWorkflowCustomName` /
    `confirmUpdateWorkflowCustomDescription`
    persist via `WorkflowPersistService`, update the workflow, and stop
    editing;
    the name falls back to the default when empty; both no-op when the
    workflow
      has no id.
    - `removeWorkflowFromProject` calls `UserProjectService` and prunes the
    entry's
    project ids; `onClickGetWorkflowExecutions` opens the execution-history
    modal;
    `onClickDownloadWorkfllow` delegates to `DownloadService` (and no-ops
    without
      an id).
    
    **`BrowseSectionComponent`** (codecov ~72%) — 4 new tests:
    - `initializeEntry` skips entries whose id is not a number and throws on
    an
      unexpected entity type.
    - `loadCoverImages` / `getCoverImage` build and cache
    `${apiEndpoint}/dataset/{id}/cover`
    for a dataset with a cover image, and fall back to the default
    background otherwise.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6590
    
    ### How was this PR tested?
    
    Extended Vitest specs, run locally in `frontend/` (each whole file
    green, run 4×
    to confirm determinism; failure paths verified by breaking an assertion
    in each):
    
    ```
    ng test --watch=false --include 
.../user-workflow-list-item.component.spec.ts   # 15 passed
    ng test --watch=false --include .../browse-section.component.spec.ts        
    # 9 passed
    eslint <specs>     # clean
    prettier --check   # clean
    ```
    
    Services are stubbed (`vi.spyOn` / `useValue`), so the specs make no
    network
    calls. The list-item tests that mutate a workflow (rename,
    remove-from-project)
    use a freshly-constructed `DashboardEntry` per test so mutations cannot
    leak into
    the shared `testWorkflowEntries` fixture.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../user-workflow-list-item.component.spec.ts      | 154 ++++++++++++++++++++-
 .../browse-section.component.spec.ts               |  38 +++++
 2 files changed, 189 insertions(+), 3 deletions(-)

diff --git 
a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.spec.ts
index 494c079308..4174dc05d4 100644
--- 
a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.spec.ts
@@ -21,14 +21,21 @@ import { Component, ViewChild } from "@angular/core";
 import { ComponentFixture, TestBed, waitForAsync } from 
"@angular/core/testing";
 import { UserWorkflowListItemComponent } from 
"./user-workflow-list-item.component";
 import { FileSaverService } from 
"../../../../service/user/file/file-saver.service";
-import { testWorkflowEntries } from "../../../user-dashboard-test-fixtures";
+import { testWorkflow1, testWorkflowEntries } from 
"../../../user-dashboard-test-fixtures";
 import { By } from "@angular/platform-browser";
 import { StubWorkflowPersistService } from 
"../../../../../common/service/workflow-persist/stub-workflow-persist.service";
-import { WorkflowPersistService } from 
"../../../../../common/service/workflow-persist/workflow-persist.service";
+import {
+  DEFAULT_WORKFLOW_NAME,
+  WorkflowPersistService,
+} from 
"../../../../../common/service/workflow-persist/workflow-persist.service";
 import { UserProjectService } from 
"../../../../service/user/project/user-project.service";
 import { StubUserProjectService } from 
"../../../../service/user/project/stub-user-project.service";
+import { DownloadService } from 
"../../../../service/user/download/download.service";
+import { WorkflowExecutionHistoryComponent } from 
"../ngbd-modal-workflow-executions/workflow-execution-history.component";
+import { Workflow } from "../../../../../common/type/workflow";
+import { of } from "rxjs";
 import { NzListComponent } from "ng-zorro-antd/list";
-import { NzModalModule } from "ng-zorro-antd/modal";
+import { NzModalModule, NzModalService } from "ng-zorro-antd/modal";
 import { HttpClientTestingModule } from "@angular/common/http/testing";
 import { provideRouter } from "@angular/router";
 import { DashboardEntry } from "../../../../type/dashboard-entry";
@@ -55,6 +62,20 @@ class TestHostComponent {
   @ViewChild(UserWorkflowListItemComponent, { static: true }) inner!: 
UserWorkflowListItemComponent;
 }
 
+// A fresh DashboardEntry per call so methods that mutate the workflow (rename,
+// remove-from-project) cannot leak into the shared testWorkflowEntries 
fixture.
+function makeWorkflowEntry(workflowOverrides: Partial<Workflow> = {}, 
projectIDs: number[] = [1]): DashboardEntry {
+  return new DashboardEntry({
+    workflow: { ...testWorkflow1, ...workflowOverrides },
+    isOwner: true,
+    ownerName: "Texera",
+    accessLevel: "Write",
+    projectIDs: [...projectIDs],
+    ownerId: 1,
+    coverImage: null,
+  });
+}
+
 describe("UserWorkflowListItemComponent", () => {
   let component: UserWorkflowListItemComponent;
   let fixture: ComponentFixture<TestHostComponent>;
@@ -133,6 +154,133 @@ describe("UserWorkflowListItemComponent", () => {
     });
   }));
 
+  describe("method coverage", () => {
+    afterEach(() => {
+      vi.restoreAllMocks();
+    });
+
+    it("getProjectIds returns the set of the entry's project ids", () => {
+      component.entry = makeWorkflowEntry({}, [1, 2, 3]);
+      expect(component.getProjectIds()).toEqual(new Set([1, 2, 3]));
+    });
+
+    it("isLightColor reports light vs dark hex colors", () => {
+      expect(component.isLightColor("ffffff")).toBe(true);
+      expect(component.isLightColor("000000")).toBe(false);
+    });
+
+    describe("confirmUpdateWorkflowCustomName", () => {
+      it("persists the new name, updates the workflow, and stops editing", () 
=> {
+        const persist = TestBed.inject(WorkflowPersistService);
+        // The stub lacks updateWorkflowName; give this fresh-per-test 
instance a spy.
+        const spy = ((persist as any).updateWorkflowName = 
vi.fn().mockReturnValue(of(undefined)));
+        component.entry = makeWorkflowEntry({ wid: 5, name: "old" });
+        component.editingName = true;
+
+        component.confirmUpdateWorkflowCustomName("new name");
+
+        expect(spy).toHaveBeenCalledWith(5, "new name");
+        expect(component.workflow.name).toBe("new name");
+        expect(component.editingName).toBe(false);
+      });
+
+      it("falls back to the default name when the input is empty", () => {
+        const persist = TestBed.inject(WorkflowPersistService);
+        const spy = ((persist as any).updateWorkflowName = 
vi.fn().mockReturnValue(of(undefined)));
+        component.entry = makeWorkflowEntry({ wid: 5 });
+
+        component.confirmUpdateWorkflowCustomName("");
+
+        expect(spy).toHaveBeenCalledWith(5, DEFAULT_WORKFLOW_NAME);
+        expect(component.workflow.name).toBe(DEFAULT_WORKFLOW_NAME);
+      });
+
+      it("is a no-op when the workflow has no id", () => {
+        const persist = TestBed.inject(WorkflowPersistService);
+        const spy = ((persist as any).updateWorkflowName = vi.fn());
+        component.entry = makeWorkflowEntry({ wid: undefined });
+
+        component.confirmUpdateWorkflowCustomName("x");
+
+        expect(spy).not.toHaveBeenCalled();
+      });
+    });
+
+    describe("confirmUpdateWorkflowCustomDescription", () => {
+      it("persists the new description and stops editing", () => {
+        const persist = TestBed.inject(WorkflowPersistService);
+        const spy = ((persist as any).updateWorkflowDescription = 
vi.fn().mockReturnValue(of(undefined)));
+        component.entry = makeWorkflowEntry({ wid: 5 });
+        component.editingDescription = true;
+
+        component.confirmUpdateWorkflowCustomDescription("new desc");
+
+        expect(spy).toHaveBeenCalledWith(5, "new desc");
+        expect(component.workflow.description).toBe("new desc");
+        expect(component.editingDescription).toBe(false);
+      });
+
+      it("is a no-op when the workflow has no id", () => {
+        const persist = TestBed.inject(WorkflowPersistService);
+        const spy = ((persist as any).updateWorkflowDescription = vi.fn());
+        component.entry = makeWorkflowEntry({ wid: undefined });
+
+        component.confirmUpdateWorkflowCustomDescription("x");
+
+        expect(spy).not.toHaveBeenCalled();
+      });
+    });
+
+    it("removeWorkflowFromProject calls the service and prunes the entry's 
project ids", () => {
+      const userProject = TestBed.inject(UserProjectService);
+      const spy = vi.spyOn(userProject, 
"removeWorkflowFromProject").mockReturnValue(of({} as Response));
+      component.entry = makeWorkflowEntry({ wid: 9 }, [1, 2]);
+
+      component.removeWorkflowFromProject(1);
+
+      expect(spy).toHaveBeenCalledWith(1, 9);
+      expect([...component.entry.workflow.projectIDs]).toEqual([2]);
+    });
+
+    it("onClickGetWorkflowExecutions opens the execution-history modal for the 
workflow", () => {
+      const modal = TestBed.inject(NzModalService);
+      const spy = vi.spyOn(modal, "create").mockReturnValue({} as any);
+      component.entry = makeWorkflowEntry({ wid: 9, name: "wf" });
+
+      component.onClickGetWorkflowExecutions();
+
+      expect(spy).toHaveBeenCalledWith(
+        expect.objectContaining({
+          nzContent: WorkflowExecutionHistoryComponent,
+          nzData: { wid: 9 },
+          nzTitle: "Execution results of Workflow: wf",
+        })
+      );
+    });
+
+    describe("onClickDownloadWorkfllow", () => {
+      it("delegates to the download service with the workflow id and name", () 
=> {
+        const download = TestBed.inject(DownloadService);
+        const spy = vi.spyOn(download, 
"downloadWorkflow").mockReturnValue(of(undefined) as any);
+        component.entry = makeWorkflowEntry({ wid: 9, name: "wf" });
+
+        component.onClickDownloadWorkfllow();
+
+        expect(spy).toHaveBeenCalledWith(9, "wf");
+      });
+
+      it("does nothing when the workflow has no id", () => {
+        const download = TestBed.inject(DownloadService);
+        const spy = vi.spyOn(download, "downloadWorkflow");
+        component.entry = makeWorkflowEntry({ wid: undefined });
+
+        component.onClickDownloadWorkfllow();
+
+        expect(spy).not.toHaveBeenCalled();
+      });
+    });
+  });
+
   function sendInput(editableDescriptionInput: HTMLInputElement, text: string) 
{
     // Helper function to change the workflow description textbox.
     editableDescriptionInput.value = text;
diff --git 
a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
 
b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
index 504101e32e..8207eeab5f 100644
--- 
a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
+++ 
b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
@@ -24,6 +24,7 @@ import { DatasetService } from 
"../../../dashboard/service/user/dataset/dataset.
 import { ChangeDetectorRef } from "@angular/core";
 import { commonTestProviders } from "../../../common/testing/test-utils";
 import { DashboardEntry } from "../../../dashboard/type/dashboard-entry";
+import { AppSettings } from "../../../common/app-setting";
 import {
   HUB_DATASET_RESULT_DETAIL,
   HUB_WORKFLOW_RESULT_DETAIL,
@@ -83,4 +84,41 @@ describe("BrowseSectionComponent", () => {
       expect(component.entityRoutes[201]).toEqual([HUB_DATASET_RESULT_DETAIL, 
"201"]);
     });
   });
+
+  describe("initializeEntry edge cases", () => {
+    it("skips entries whose id is not a number", () => {
+      component.entities = [{ id: undefined, type: "dataset", 
accessibleUserIds: [] } as unknown as DashboardEntry];
+      component.ngOnInit();
+      expect(Object.keys(component.entityRoutes)).toHaveLength(0);
+    });
+
+    it("throws on an unexpected entity type", () => {
+      const bad = { id: 7, type: "project", accessibleUserIds: [] } as unknown 
as DashboardEntry;
+      expect(() => (component as 
any).initializeEntry(bad)).toThrowError("Unexpected type in DashboardEntry.");
+    });
+  });
+
+  describe("cover images", () => {
+    it("builds and caches the cover URL for a dataset that has a cover image", 
() => {
+      const entity = {
+        id: 5,
+        type: "dataset",
+        coverImageUrl: "has-cover",
+        accessibleUserIds: [],
+      } as unknown as DashboardEntry;
+      component.entities = [entity];
+      component.ngOnInit();
+
+      
expect(component.getCoverImage(entity)).toBe(`${AppSettings.getApiEndpoint()}/dataset/5/cover`);
+    });
+
+    it("falls back to the default background when no cover was cached", () => {
+      // No coverImageUrl -> loadCoverImages skips it -> getCoverImage returns 
the default.
+      const entity = { id: 6, type: "dataset", accessibleUserIds: [] } as 
unknown as DashboardEntry;
+      component.entities = [entity];
+      component.ngOnInit();
+
+      
expect(component.getCoverImage(entity)).toBe(component.defaultBackground);
+    });
+  });
 });

Reply via email to