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

commit 6b1a1e8691de7588b93013dbe1df6f46d408a68d
Author: Meng Wang <[email protected]>
AuthorDate: Sun Aug 23 08:04:59 2026 +0000

    test(frontend): cover the dashboard list item's per-entry-type dispatch 
(#7884)
    
    ### What changes were proposed in this PR?
    
    The spec fed the component one workflow entry; the component dispatches
    on `entry.type` in
    half a dozen places. 18 new tests hand it the other kinds, taking
    `list-item.component.ts` to
    **177/177 statements** (from 147/177) and its branches from 68/110 to
    95/110.
    
    - **The required-input guard** — reading `entry` before one is supplied
    throws.
    - **Icon dispatch** — `project` for a workflow, `database` for a
    dataset, `folder-open` for a
    file, the `Unexpected type in DashboardEntry.` throw for a kind the
    chain does not know, and
      a dataset without a numeric id, which is left unrouted.
    - **`renderMarkdownPreview`** — an absent description, a whitespace-only
    one, and a real one
    reduced to plain text, so both the `??` fallback and the empty-guard arm
    run.
    - **Share access** — the workflow and dataset dialogs assert their own
    configuration, the
    modal's `refresh` is re-emitted through the component's own output, and
    an entry kind that
      cannot be shared opens nothing.
    - **Download** — workflow by id and name, dataset by id and name, and
    the early exit for an
      entry that was never persisted.
    - **Editing** — entering name-edit mode with and without the view child
    present (the focus
    callback is invoked directly rather than waited on), the missing-id
    rejection, the per-kind
    default name when a rename is blank, an absent description treated as
    empty, and a dataset
      description routed to the dataset service.
    - **`ngOnChanges`** with a change that is not the entry, which must not
    re-initialise.
    
    Each test builds its own entry — several handlers mutate it in place, so
    a shared fixture
    would leak a rename into a later assertion. `vi.restoreAllMocks()` runs
    in the block's own
    `afterEach`. No assertions on layout or geometry, and no project-type
    entries, so #7463 does
    not disturb this.
    
    No production code was changed.
    
    ### The 15 branch lines left
    
    They are the remaining `else if` type arms and `??` fallbacks in the
    like/view counters
    (`toggleLike`, `openDetailModal`'s count refresh) — reachable, but each
    needs another
    hub-service response shape rather than another entry kind, which is a
    different seam from the
    dispatch this issue is about. Statements are complete, so nothing is
    unexercised; these are the
    unlit halves of value-level guards.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7880.
    
    ### How was this PR tested?
    
    `ng test --watch=false --include
    src/app/dashboard/component/user/list-item/list-item.component.spec.ts`
    — 38 passed (20 existing + 18 new), run 3x for determinism. Coverage
    (`--coverage`) gives the
    numbers above. The failure path was verified by breaking an assertion
    (red, non-zero exit) and
    restoring it. `yarn --cwd frontend format:ci`, the repo's own lint step,
    is clean.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../user/list-item/list-item.component.spec.ts     | 247 ++++++++++++++++++++-
 1 file changed, 245 insertions(+), 2 deletions(-)

diff --git 
a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts
index eca823a4ee..bf13d35514 100644
--- 
a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts
@@ -19,7 +19,10 @@
 
 import { ComponentFixture, TestBed } from "@angular/core/testing";
 import { ListItemComponent } from "./list-item.component";
-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, Subject, throwError } from "rxjs";
@@ -31,7 +34,7 @@ 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 { DatasetService } from "../../../service/user/dataset/dataset.service";
+import { DatasetService, DEFAULT_DATASET_NAME } from 
"../../../service/user/dataset/dataset.service";
 import { NotificationService } from 
"../../../../common/service/notification/notification.service";
 import {
   HUB_DATASET_RESULT_DETAIL,
@@ -350,4 +353,244 @@ describe("ListItemComponent", () => {
       expect(component.viewCount).toBe(5); // 4 + 1
     });
   });
+
+  /**
+   * The suite above feeds one workflow entry; the component dispatches on 
`entry.type` in
+   * several places, so these hand it the other kinds. Each test builds its 
own entry — the
+   * rename and description handlers mutate it in place.
+   */
+  describe("per-entry-type dispatch", () => {
+    function entryOf(overrides: Partial<Record<string, unknown>>): 
DashboardEntry {
+      return {
+        id: 7,
+        name: "item",
+        description: "",
+        accessibleUserIds: [],
+        likeCount: 0,
+        viewCount: 0,
+        isLiked: false,
+        size: 0,
+        ...overrides,
+      } as unknown as DashboardEntry;
+    }
+
+    /** Re-runs the input pipeline the way an @Input change would. */
+    function feed(entry: DashboardEntry): void {
+      component.entry = entry;
+      component.ngOnChanges({ entry: {} as any });
+    }
+
+    afterEach(() => {
+      vi.restoreAllMocks();
+    });
+
+    it("refuses to be read before an entry is supplied", () => {
+      const bare = 
TestBed.createComponent(ListItemComponent).componentInstance;
+
+      expect(() => bare.entry).toThrowError("entry property must be 
provided.");
+    });
+
+    it("picks an icon per entry kind", () => {
+      feed(entryOf({ type: "workflow", workflow: { isOwner: true } }));
+      expect(component.iconType).toBe("project");
+
+      feed(entryOf({ type: "dataset", dataset: { isOwner: true } }));
+      expect(component.iconType).toBe("database");
+
+      feed(entryOf({ type: "file" }));
+      expect(component.iconType).toBe("folder-open");
+    });
+
+    it("refuses an entry kind it does not know", () => {
+      expect(() => feed(entryOf({ type: "quantum" 
}))).toThrowError("Unexpected type in DashboardEntry.");
+    });
+
+    it("leaves a dataset without a numeric id unrouted", () => {
+      // The dataset arm reads isOwner and the link only for a persisted entry.
+      feed(entryOf({ type: "dataset", id: undefined, dataset: { isOwner: false 
} }));
+
+      expect(component.entryLink).toEqual([]);
+      expect(component.iconType).not.toBe("database");
+    });
+
+    it("reduces a description to a plain preview, and blanks an empty one", () 
=> {
+      feed(entryOf({ type: "file", description: undefined }));
+      expect(component.renderedDescription).toBe("");
+
+      feed(entryOf({ type: "file", description: "   " }));
+      expect(component.renderedDescription).toBe("");
+
+      feed(entryOf({ type: "file", description: "# Title with [a 
link](http://x)  and\n*emphasis*" }));
+      expect(component.renderedDescription).toBe("Title with a link and 
emphasis");
+    });
+
+    describe("share access", () => {
+      /** A modal handle whose componentInstance re-emits on demand. */
+      function modalReturning(refresh: Subject<void> | undefined) {
+        return {
+          componentInstance: refresh === undefined ? undefined : { refresh },
+        } as any;
+      }
+
+      it("opens the workflow share dialog and re-emits its refresh", async () 
=> {
+        const refresh = new Subject<void>();
+        const create = vi.spyOn(modalService, 
"create").mockReturnValue(modalReturning(refresh));
+        // The shared stub carries only the update methods; the share dialog 
also asks for
+        // the owner list.
+        (workflowPersistService as any).retrieveOwners = 
vi.fn().mockReturnValue(of([]));
+        let refreshed = false;
+        component.refresh.subscribe(() => (refreshed = true));
+        feed(entryOf({ type: "workflow", workflow: { isOwner: true, 
accessLevel: "WRITE" } }));
+
+        await component.onClickOpenShareAccess();
+
+        expect(create).toHaveBeenCalledWith(
+          expect.objectContaining({
+            nzTitle: "Share this workflow with others",
+            nzData: expect.objectContaining({ type: "workflow", writeAccess: 
true, id: 7 }),
+          })
+        );
+
+        refresh.next();
+        expect(refreshed).toBe(true);
+      });
+
+      it("opens the dataset share dialog with the dataset's owners", async () 
=> {
+        const create = vi.spyOn(modalService, 
"create").mockReturnValue(modalReturning(new Subject<void>()));
+        (datasetService as any).retrieveOwners = 
vi.fn().mockReturnValue(of([]));
+        feed(entryOf({ type: "dataset", dataset: { isOwner: true }, 
accessLevel: "READ" }));
+
+        await component.onClickOpenShareAccess();
+
+        expect(create).toHaveBeenCalledWith(
+          expect.objectContaining({
+            nzTitle: "Share this dataset with others",
+            nzData: expect.objectContaining({ type: "dataset", writeAccess: 
false }),
+          })
+        );
+      });
+
+      it("opens nothing for an entry kind that cannot be shared", async () => {
+        const create = vi.spyOn(modalService, "create");
+        feed(entryOf({ type: "file" }));
+
+        await component.onClickOpenShareAccess();
+
+        expect(create).not.toHaveBeenCalled();
+      });
+    });
+
+    describe("editing", () => {
+      it("focuses the name box once it exists, and copes when it does not", () 
=> {
+        vi.useRealTimers();
+        feed(entryOf({ type: "workflow", workflow: { isOwner: true }, name: 
"before" }));
+
+        // No view child yet: entering edit mode must not throw.
+        (component as any).nameInput = undefined;
+        expect(() => component.onEditName()).not.toThrow();
+        expect(component.originalName).toBe("before");
+        expect(component.editingName).toBe(true);
+
+        const input = { value: "before", focus: vi.fn(), setSelectionRange: 
vi.fn() };
+        (component as any).nameInput = { nativeElement: input };
+        component.onEditName();
+
+        // The focus is scheduled on a task the component owns; run it 
directly rather than
+        // waiting on a timer.
+        const scheduled = vi.spyOn(globalThis, "setTimeout");
+        component.onEditName();
+        const callback = scheduled.mock.calls.at(-1)?.[0] as () => void;
+        callback();
+
+        expect(input.focus).toHaveBeenCalled();
+        expect(input.setSelectionRange).toHaveBeenCalledWith("before".length, 
"before".length);
+      });
+
+      it("reports a missing id instead of updating", () => {
+        const notify = vi.spyOn((component as any).notificationService, 
"error").mockImplementation(() => {});
+        feed(entryOf({ type: "workflow", id: 0, workflow: { isOwner: true } 
}));
+
+        component.confirmUpdateCustomName("renamed");
+
+        expect(notify).toHaveBeenCalledWith("Id is missing");
+        
expect(workflowPersistService.updateWorkflowName).not.toHaveBeenCalled();
+      });
+
+      it("falls back to the default name per entry kind when the new name is 
empty", () => {
+        workflowPersistService.updateWorkflowName.mockReturnValue(of({} as 
Response));
+        feed(entryOf({ type: "workflow", workflow: { isOwner: true } }));
+
+        component.confirmUpdateCustomName("");
+
+        
expect(workflowPersistService.updateWorkflowName).toHaveBeenCalledWith(7, 
DEFAULT_WORKFLOW_NAME);
+      });
+
+      it("treats an absent description as an empty one", () => {
+        workflowPersistService.updateWorkflowDescription.mockReturnValue(of({} 
as Response));
+        feed(entryOf({ type: "workflow", workflow: { isOwner: true } }));
+
+        component.confirmUpdateCustomDescription(undefined);
+
+        
expect(workflowPersistService.updateWorkflowDescription).toHaveBeenCalledWith(7,
 "");
+      });
+
+      it("sends a dataset description to the dataset service", () => {
+        (datasetService as any).updateDatasetDescription = 
vi.fn().mockReturnValue(of(undefined));
+        feed(entryOf({ type: "dataset", dataset: { isOwner: true } }));
+
+        component.confirmUpdateCustomDescription("about this set");
+
+        expect((datasetService as 
any).updateDatasetDescription).toHaveBeenCalledWith(7, "about this set");
+      });
+    });
+
+    it("ignores a change that is not the entry", () => {
+      feed(entryOf({ type: "file", description: "kept" }));
+      const before = component.renderedDescription;
+
+      component.ngOnChanges({ editable: {} as any });
+
+      expect(component.renderedDescription).toBe(before);
+    });
+
+    it("falls back to the dataset default name when a dataset rename is 
blank", () => {
+      (datasetService as any).updateDatasetName = 
vi.fn().mockReturnValue(of({} as Response));
+      feed(entryOf({ type: "dataset", dataset: { isOwner: true } }));
+
+      component.confirmUpdateCustomName("");
+
+      expect((datasetService as 
any).updateDatasetName).toHaveBeenCalledWith(7, DEFAULT_DATASET_NAME);
+    });
+
+    describe("download", () => {
+      it("downloads a workflow by id and name", () => {
+        const download = vi
+          .spyOn((component as any).downloadService, "downloadWorkflow")
+          .mockReturnValue(of(undefined));
+        feed(entryOf({ type: "workflow", workflow: { isOwner: true, workflow: 
{ name: "flow" } } }));
+
+        component.onClickDownload();
+
+        expect(download).toHaveBeenCalledWith(7, "flow");
+      });
+
+      it("downloads a dataset by id and name", () => {
+        const download = vi.spyOn((component as any).downloadService, 
"downloadDataset").mockReturnValue(of(undefined));
+        feed(entryOf({ type: "dataset", dataset: { isOwner: true }, name: 
"set" }));
+
+        component.onClickDownload();
+
+        expect(download).toHaveBeenCalledWith(7, "set");
+      });
+
+      it("downloads nothing for an entry that was never persisted", () => {
+        const workflow = vi.spyOn((component as any).downloadService, 
"downloadWorkflow");
+        feed(entryOf({ type: "file", id: 0 }));
+
+        component.onClickDownload();
+
+        expect(workflow).not.toHaveBeenCalled();
+      });
+    });
+  });
 });

Reply via email to