Ma77Ball commented on code in PR #5236:
URL: https://github.com/apache/texera/pull/5236#discussion_r3344963820


##########
frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts:
##########
@@ -0,0 +1,417 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, Input } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ActivatedRoute, Router } from "@angular/router";
+import { NzIconModule } from "ng-zorro-antd/icon";
+import { NZ_MODAL_DATA } from "ng-zorro-antd/modal";
+import { ArrowLeftOutline, EyeOutline, LikeOutline, UserOutline } from 
"@ant-design/icons-angular/icons";
+import { of, throwError } from "rxjs";
+import { vi } from "vitest";
+
+import { HubWorkflowDetailComponent, THROTTLE_TIME_MS } from 
"./hub-workflow-detail.component";
+import { ActionType, EntityType, HubService } from 
"../../../service/hub.service";
+import { UserService } from "../../../../common/service/user/user.service";
+import { StubUserService, MOCK_USER } from 
"../../../../common/service/user/stub-user.service";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { WorkflowActionService } from 
"../../../../workspace/service/workflow-graph/model/workflow-action.service";
+import { WorkflowPersistService } from 
"../../../../common/service/workflow-persist/workflow-persist.service";
+import { Role } from "../../../../common/type/user";
+import { Workflow } from "../../../../common/type/workflow";
+import { DASHBOARD_HUB_WORKFLOW_RESULT, DASHBOARD_USER_WORKSPACE } from 
"../../../../app-routing.constant";
+import { MarkdownDescriptionComponent } from 
"../../../../dashboard/component/user/markdown-description/markdown-description.component";
+import { WorkflowEditorComponent } from 
"../../../../workspace/component/workflow-editor/workflow-editor.component";
+import { MiniMapComponent } from 
"../../../../workspace/component/workflow-editor/mini-map/mini-map.component";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
+
+@Component({ selector: "texera-markdown-description", standalone: true, 
template: "" })
+class StubMarkdownDescriptionComponent {
+  @Input() description?: string;
+  @Input() enableViewMore?: boolean;
+}
+
+@Component({ selector: "texera-workflow-editor", standalone: true, template: 
"" })
+class StubWorkflowEditorComponent {}
+
+@Component({ selector: "texera-mini-map", standalone: true, template: "" })
+class StubMiniMapComponent {}
+
+describe("HubWorkflowDetailComponent", () => {
+  let fixture: ComponentFixture<HubWorkflowDetailComponent>;
+  let component: HubWorkflowDetailComponent;
+
+  let hubServiceMock: any;
+  let workflowPersistServiceMock: any;
+  let workflowActionServiceMock: any;
+  let notificationServiceMock: any;
+  let routerMock: any;
+  let stubGraph: { triggerCenterEvent: ReturnType<typeof vi.fn> };
+
+  function makeMocks() {
+    stubGraph = { triggerCenterEvent: vi.fn() };
+
+    hubServiceMock = {
+      getCounts: vi.fn().mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, counts: {} }])),
+      postView: vi.fn().mockReturnValue(of(7)),
+      isLiked: vi.fn().mockReturnValue(of([])),
+      postLike: vi.fn().mockReturnValue(of(true)),
+      postUnlike: vi.fn().mockReturnValue(of(true)),
+      cloneWorkflow: vi.fn().mockReturnValue(of(99)),
+    };
+
+    workflowPersistServiceMock = {
+      retrieveWorkflow: vi.fn().mockReturnValue(of({} as Workflow)),
+      retrievePublicWorkflow: vi.fn().mockReturnValue(of({} as Workflow)),
+      getOwnerName: vi.fn().mockReturnValue(of("owner")),
+      getWorkflowName: vi.fn().mockReturnValue(of("name")),
+      getWorkflowDescription: vi.fn().mockReturnValue(of("desc")),
+    };
+
+    workflowActionServiceMock = {
+      disableWorkflowModification: vi.fn(),
+      reloadWorkflow: vi.fn(),
+      clearWorkflow: vi.fn(),
+      getTexeraGraph: vi.fn().mockReturnValue(stubGraph),
+    };
+
+    notificationServiceMock = { success: vi.fn(), error: vi.fn(), info: 
vi.fn() };
+
+    routerMock = {
+      navigateByUrl: vi.fn().mockResolvedValue(true),
+      navigate: vi.fn().mockResolvedValue(true),
+    };
+  }
+
+  function configure(opts: { modalData?: { wid: number } | undefined; 
routeId?: number; userOverride?: any }) {
+    TestBed.overrideComponent(HubWorkflowDetailComponent, {
+      remove: { imports: [WorkflowEditorComponent, MiniMapComponent, 
MarkdownDescriptionComponent] },
+      add: { imports: [StubWorkflowEditorComponent, StubMiniMapComponent, 
StubMarkdownDescriptionComponent] },
+    });
+
+    TestBed.configureTestingModule({
+      imports: [
+        HubWorkflowDetailComponent,
+        NzIconModule.forChild([ArrowLeftOutline, EyeOutline, LikeOutline, 
UserOutline]),
+      ],
+      providers: [
+        { provide: NZ_MODAL_DATA, useValue: opts.modalData },
+        {
+          provide: ActivatedRoute,
+          useValue: { snapshot: { params: opts.routeId !== undefined ? { id: 
opts.routeId } : {} } },

Review Comment:
   Good catch — fixed. The route mock now supplies `id` as a string 
(`"11"`/`"9"`), matching what Angular produces at runtime. Since the 
persist/hub services require a numeric `wid`, I also added coercion in the 
component constructor (`this.wid = isDefined(routeId) ? Number(routeId) : 
undefined`), so the numeric assertions still hold and the production code no 
longer forwards a string to `retrievePublicWorkflow(wid: number)`.



##########
frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts:
##########
@@ -0,0 +1,417 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, Input } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ActivatedRoute, Router } from "@angular/router";
+import { NzIconModule } from "ng-zorro-antd/icon";
+import { NZ_MODAL_DATA } from "ng-zorro-antd/modal";
+import { ArrowLeftOutline, EyeOutline, LikeOutline, UserOutline } from 
"@ant-design/icons-angular/icons";
+import { of, throwError } from "rxjs";
+import { vi } from "vitest";
+
+import { HubWorkflowDetailComponent, THROTTLE_TIME_MS } from 
"./hub-workflow-detail.component";
+import { ActionType, EntityType, HubService } from 
"../../../service/hub.service";
+import { UserService } from "../../../../common/service/user/user.service";
+import { StubUserService, MOCK_USER } from 
"../../../../common/service/user/stub-user.service";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { WorkflowActionService } from 
"../../../../workspace/service/workflow-graph/model/workflow-action.service";
+import { WorkflowPersistService } from 
"../../../../common/service/workflow-persist/workflow-persist.service";
+import { Role } from "../../../../common/type/user";
+import { Workflow } from "../../../../common/type/workflow";
+import { DASHBOARD_HUB_WORKFLOW_RESULT, DASHBOARD_USER_WORKSPACE } from 
"../../../../app-routing.constant";
+import { MarkdownDescriptionComponent } from 
"../../../../dashboard/component/user/markdown-description/markdown-description.component";
+import { WorkflowEditorComponent } from 
"../../../../workspace/component/workflow-editor/workflow-editor.component";
+import { MiniMapComponent } from 
"../../../../workspace/component/workflow-editor/mini-map/mini-map.component";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
+
+@Component({ selector: "texera-markdown-description", standalone: true, 
template: "" })
+class StubMarkdownDescriptionComponent {
+  @Input() description?: string;
+  @Input() enableViewMore?: boolean;
+}
+
+@Component({ selector: "texera-workflow-editor", standalone: true, template: 
"" })
+class StubWorkflowEditorComponent {}
+
+@Component({ selector: "texera-mini-map", standalone: true, template: "" })
+class StubMiniMapComponent {}
+
+describe("HubWorkflowDetailComponent", () => {
+  let fixture: ComponentFixture<HubWorkflowDetailComponent>;
+  let component: HubWorkflowDetailComponent;
+
+  let hubServiceMock: any;
+  let workflowPersistServiceMock: any;
+  let workflowActionServiceMock: any;
+  let notificationServiceMock: any;
+  let routerMock: any;
+  let stubGraph: { triggerCenterEvent: ReturnType<typeof vi.fn> };
+
+  function makeMocks() {
+    stubGraph = { triggerCenterEvent: vi.fn() };
+
+    hubServiceMock = {
+      getCounts: vi.fn().mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, counts: {} }])),
+      postView: vi.fn().mockReturnValue(of(7)),
+      isLiked: vi.fn().mockReturnValue(of([])),
+      postLike: vi.fn().mockReturnValue(of(true)),
+      postUnlike: vi.fn().mockReturnValue(of(true)),
+      cloneWorkflow: vi.fn().mockReturnValue(of(99)),
+    };
+
+    workflowPersistServiceMock = {
+      retrieveWorkflow: vi.fn().mockReturnValue(of({} as Workflow)),
+      retrievePublicWorkflow: vi.fn().mockReturnValue(of({} as Workflow)),
+      getOwnerName: vi.fn().mockReturnValue(of("owner")),
+      getWorkflowName: vi.fn().mockReturnValue(of("name")),
+      getWorkflowDescription: vi.fn().mockReturnValue(of("desc")),
+    };
+
+    workflowActionServiceMock = {
+      disableWorkflowModification: vi.fn(),
+      reloadWorkflow: vi.fn(),
+      clearWorkflow: vi.fn(),
+      getTexeraGraph: vi.fn().mockReturnValue(stubGraph),
+    };
+
+    notificationServiceMock = { success: vi.fn(), error: vi.fn(), info: 
vi.fn() };
+
+    routerMock = {
+      navigateByUrl: vi.fn().mockResolvedValue(true),
+      navigate: vi.fn().mockResolvedValue(true),
+    };
+  }
+
+  function configure(opts: { modalData?: { wid: number } | undefined; 
routeId?: number; userOverride?: any }) {
+    TestBed.overrideComponent(HubWorkflowDetailComponent, {
+      remove: { imports: [WorkflowEditorComponent, MiniMapComponent, 
MarkdownDescriptionComponent] },
+      add: { imports: [StubWorkflowEditorComponent, StubMiniMapComponent, 
StubMarkdownDescriptionComponent] },
+    });
+
+    TestBed.configureTestingModule({
+      imports: [
+        HubWorkflowDetailComponent,
+        NzIconModule.forChild([ArrowLeftOutline, EyeOutline, LikeOutline, 
UserOutline]),
+      ],
+      providers: [
+        { provide: NZ_MODAL_DATA, useValue: opts.modalData },
+        {
+          provide: ActivatedRoute,
+          useValue: { snapshot: { params: opts.routeId !== undefined ? { id: 
opts.routeId } : {} } },
+        },
+        { provide: Router, useValue: routerMock },
+        { provide: HubService, useValue: hubServiceMock },
+        { provide: WorkflowPersistService, useValue: 
workflowPersistServiceMock },
+        { provide: WorkflowActionService, useValue: workflowActionServiceMock 
},
+        { provide: NotificationService, useValue: notificationServiceMock },
+        { provide: UserService, useClass: StubUserService },
+        ...commonTestProviders,
+      ],
+    });
+
+    if ("userOverride" in opts) {
+      (TestBed.inject(UserService) as unknown as StubUserService).user = 
opts.userOverride;
+    }
+  }
+
+  function build(opts: {
+    modalData?: { wid: number } | undefined;
+    routeId?: number;
+    userOverride?: any;
+    detectChanges?: boolean;
+  }) {
+    configure(opts);
+    fixture = TestBed.createComponent(HubWorkflowDetailComponent);
+    component = fixture.componentInstance;
+    if (opts.detectChanges ?? true) {
+      fixture.detectChanges();
+    }
+  }
+
+  beforeEach(() => {
+    makeMocks();
+  });
+
+  describe("constructor / wid resolution", () => {
+    it("uses NZ_MODAL_DATA wid and leaves isHub false", () => {
+      build({ modalData: { wid: 42 }, routeId: 11 });
+      expect(component.wid).toBe(42);
+      expect(component.isHub).toBe(false);
+    });
+
+    it("falls back to route.snapshot.params.id and sets isHub true", () => {
+      build({ modalData: undefined, routeId: 11 });
+      expect(component.wid).toBe(11);
+      expect(component.isHub).toBe(true);
+    });
+
+    it("sets isActivatedUser true for REGULAR", () => {
+      build({ modalData: { wid: 1 } });
+      expect(component.isActivatedUser).toBe(true);
+    });
+
+    it("sets isActivatedUser true for ADMIN", () => {
+      build({
+        modalData: { wid: 1 },
+        userOverride: { ...MOCK_USER, role: Role.ADMIN },
+      });
+      expect(component.isActivatedUser).toBe(true);
+    });
+
+    it("leaves isActivatedUser false for non-activated roles", () => {
+      build({
+        modalData: { wid: 1 },
+        userOverride: { ...MOCK_USER, role: Role.INACTIVE },
+      });
+      expect(component.isActivatedUser).toBe(false);
+    });
+
+    it("disables workflow modification", () => {
+      build({ modalData: { wid: 1 } });
+      
expect(workflowActionServiceMock.disableWorkflowModification).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe("ngOnInit", () => {
+    it("early-returns when wid is undefined", () => {
+      build({ modalData: undefined, routeId: undefined, detectChanges: false 
});
+      expect(component.wid).toBeUndefined();
+      component.ngOnInit();
+      expect(hubServiceMock.getCounts).not.toHaveBeenCalled();
+      expect(hubServiceMock.postView).not.toHaveBeenCalled();
+      expect(hubServiceMock.isLiked).not.toHaveBeenCalled();
+    });
+
+    it("assigns likeCount and cloneCount from getCounts", () => {
+      hubServiceMock.getCounts.mockReturnValue(
+        of([{ entityId: 1, entityType: EntityType.Workflow, counts: { like: 5, 
clone: 3 } }])
+      );
+      build({ modalData: { wid: 1 } });
+      expect(hubServiceMock.getCounts).toHaveBeenCalledWith(
+        [EntityType.Workflow],
+        [1],
+        [ActionType.Like, ActionType.Clone]
+      );
+      expect(component.likeCount).toBe(5);
+      expect(component.cloneCount).toBe(3);
+    });
+
+    it("defaults likeCount and cloneCount to 0 when counts are missing", () => 
{
+      hubServiceMock.getCounts.mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, counts: {} }]));
+      build({ modalData: { wid: 1 } });
+      expect(component.likeCount).toBe(0);
+      expect(component.cloneCount).toBe(0);
+    });
+
+    it("pipes postView through throttleTime and assigns viewCount", () => {
+      expect(THROTTLE_TIME_MS).toBe(1000);
+      hubServiceMock.postView.mockReturnValue(of(12));
+      build({ modalData: { wid: 1 } });
+      expect(hubServiceMock.postView).toHaveBeenCalledWith(1, MOCK_USER.uid, 
EntityType.Workflow);
+      expect(component.viewCount).toBe(12);
+    });
+
+    it("passes 0 as userId to postView when there is no current user", () => {
+      build({ modalData: { wid: 1 }, userOverride: undefined });
+      expect(hubServiceMock.postView).toHaveBeenCalledWith(1, 0, 
EntityType.Workflow);
+    });
+
+    it("sets isLiked from the isLiked response when a user is logged in", () 
=> {
+      hubServiceMock.isLiked.mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, isLiked: true }]));
+      build({ modalData: { wid: 1 } });
+      expect(hubServiceMock.isLiked).toHaveBeenCalledWith([1], 
[EntityType.Workflow]);
+      expect(component.isLiked).toBe(true);
+    });
+
+    it("falls back to isLiked = false when the response is empty", () => {
+      hubServiceMock.isLiked.mockReturnValue(of([]));
+      build({ modalData: { wid: 1 } });
+      expect(component.isLiked).toBe(false);
+    });
+
+    it("does not call isLiked when there is no current user", () => {
+      build({ modalData: { wid: 1 }, userOverride: undefined });
+      expect(hubServiceMock.isLiked).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("ngAfterViewInit / loadWorkflowWithId", () => {
+    it("uses retrieveWorkflow when not in hub mode and triggers center event", 
() => {
+      const wf = {} as Workflow;
+      workflowPersistServiceMock.retrieveWorkflow.mockReturnValue(of(wf));
+      build({ modalData: { wid: 5 } });
+      
expect(workflowPersistServiceMock.retrieveWorkflow).toHaveBeenCalledWith(5);
+      
expect(workflowPersistServiceMock.retrievePublicWorkflow).not.toHaveBeenCalled();
+      
expect(workflowActionServiceMock.reloadWorkflow).toHaveBeenCalledWith(wf);
+      expect(stubGraph.triggerCenterEvent).toHaveBeenCalledTimes(1);
+    });
+
+    it("uses retrievePublicWorkflow when in hub mode and triggers center 
event", () => {
+      const wf = {} as Workflow;
+      
workflowPersistServiceMock.retrievePublicWorkflow.mockReturnValue(of(wf));
+      build({ modalData: undefined, routeId: 9 });
+      
expect(workflowPersistServiceMock.retrievePublicWorkflow).toHaveBeenCalledWith(9);
+      
expect(workflowPersistServiceMock.retrieveWorkflow).not.toHaveBeenCalled();
+      
expect(workflowActionServiceMock.reloadWorkflow).toHaveBeenCalledWith(wf);
+      expect(stubGraph.triggerCenterEvent).toHaveBeenCalledTimes(1);
+    });
+
+    it("does not reload or trigger center event when retrieveWorkflow errors", 
() => {
+      
workflowPersistServiceMock.retrieveWorkflow.mockReturnValue(throwError(() => 
new Error("boom")));
+      build({ modalData: { wid: 5 } });
+      expect(workflowActionServiceMock.reloadWorkflow).not.toHaveBeenCalled();
+      expect(stubGraph.triggerCenterEvent).not.toHaveBeenCalled();
+    });
+
+    it("does not reload or trigger center event when retrievePublicWorkflow 
errors", () => {
+      
workflowPersistServiceMock.retrievePublicWorkflow.mockReturnValue(throwError(() 
=> new Error("boom")));
+      build({ modalData: undefined, routeId: 9 });

Review Comment:
   Fixed. Both error-path tests now assert the documented `Failed to load 
workflow with id ...` behavior. The handler throws inside the subscribe error 
callback, which RxJS surfaces asynchronously via `config.onUnhandledError`; the 
new `captureUnhandledRxjsError` helper installs that hook under fake timers and 
flushes the report, so the test asserts `unhandled?.message` equals `Failed to 
load workflow with id 5`. If the error handler were removed, the message would 
differ and the test would fail.



##########
frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts:
##########
@@ -0,0 +1,417 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, Input } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ActivatedRoute, Router } from "@angular/router";
+import { NzIconModule } from "ng-zorro-antd/icon";
+import { NZ_MODAL_DATA } from "ng-zorro-antd/modal";
+import { ArrowLeftOutline, EyeOutline, LikeOutline, UserOutline } from 
"@ant-design/icons-angular/icons";
+import { of, throwError } from "rxjs";
+import { vi } from "vitest";
+
+import { HubWorkflowDetailComponent, THROTTLE_TIME_MS } from 
"./hub-workflow-detail.component";
+import { ActionType, EntityType, HubService } from 
"../../../service/hub.service";
+import { UserService } from "../../../../common/service/user/user.service";
+import { StubUserService, MOCK_USER } from 
"../../../../common/service/user/stub-user.service";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { WorkflowActionService } from 
"../../../../workspace/service/workflow-graph/model/workflow-action.service";
+import { WorkflowPersistService } from 
"../../../../common/service/workflow-persist/workflow-persist.service";
+import { Role } from "../../../../common/type/user";
+import { Workflow } from "../../../../common/type/workflow";
+import { DASHBOARD_HUB_WORKFLOW_RESULT, DASHBOARD_USER_WORKSPACE } from 
"../../../../app-routing.constant";
+import { MarkdownDescriptionComponent } from 
"../../../../dashboard/component/user/markdown-description/markdown-description.component";
+import { WorkflowEditorComponent } from 
"../../../../workspace/component/workflow-editor/workflow-editor.component";
+import { MiniMapComponent } from 
"../../../../workspace/component/workflow-editor/mini-map/mini-map.component";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
+
+@Component({ selector: "texera-markdown-description", standalone: true, 
template: "" })
+class StubMarkdownDescriptionComponent {
+  @Input() description?: string;
+  @Input() enableViewMore?: boolean;
+}
+
+@Component({ selector: "texera-workflow-editor", standalone: true, template: 
"" })
+class StubWorkflowEditorComponent {}
+
+@Component({ selector: "texera-mini-map", standalone: true, template: "" })
+class StubMiniMapComponent {}
+
+describe("HubWorkflowDetailComponent", () => {
+  let fixture: ComponentFixture<HubWorkflowDetailComponent>;
+  let component: HubWorkflowDetailComponent;
+
+  let hubServiceMock: any;
+  let workflowPersistServiceMock: any;
+  let workflowActionServiceMock: any;
+  let notificationServiceMock: any;
+  let routerMock: any;
+  let stubGraph: { triggerCenterEvent: ReturnType<typeof vi.fn> };
+
+  function makeMocks() {
+    stubGraph = { triggerCenterEvent: vi.fn() };
+
+    hubServiceMock = {
+      getCounts: vi.fn().mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, counts: {} }])),
+      postView: vi.fn().mockReturnValue(of(7)),
+      isLiked: vi.fn().mockReturnValue(of([])),
+      postLike: vi.fn().mockReturnValue(of(true)),
+      postUnlike: vi.fn().mockReturnValue(of(true)),
+      cloneWorkflow: vi.fn().mockReturnValue(of(99)),
+    };
+
+    workflowPersistServiceMock = {
+      retrieveWorkflow: vi.fn().mockReturnValue(of({} as Workflow)),
+      retrievePublicWorkflow: vi.fn().mockReturnValue(of({} as Workflow)),
+      getOwnerName: vi.fn().mockReturnValue(of("owner")),
+      getWorkflowName: vi.fn().mockReturnValue(of("name")),
+      getWorkflowDescription: vi.fn().mockReturnValue(of("desc")),
+    };
+
+    workflowActionServiceMock = {
+      disableWorkflowModification: vi.fn(),
+      reloadWorkflow: vi.fn(),
+      clearWorkflow: vi.fn(),
+      getTexeraGraph: vi.fn().mockReturnValue(stubGraph),
+    };
+
+    notificationServiceMock = { success: vi.fn(), error: vi.fn(), info: 
vi.fn() };
+
+    routerMock = {
+      navigateByUrl: vi.fn().mockResolvedValue(true),
+      navigate: vi.fn().mockResolvedValue(true),
+    };
+  }
+
+  function configure(opts: { modalData?: { wid: number } | undefined; 
routeId?: number; userOverride?: any }) {
+    TestBed.overrideComponent(HubWorkflowDetailComponent, {
+      remove: { imports: [WorkflowEditorComponent, MiniMapComponent, 
MarkdownDescriptionComponent] },
+      add: { imports: [StubWorkflowEditorComponent, StubMiniMapComponent, 
StubMarkdownDescriptionComponent] },
+    });
+
+    TestBed.configureTestingModule({
+      imports: [
+        HubWorkflowDetailComponent,
+        NzIconModule.forChild([ArrowLeftOutline, EyeOutline, LikeOutline, 
UserOutline]),
+      ],
+      providers: [
+        { provide: NZ_MODAL_DATA, useValue: opts.modalData },
+        {
+          provide: ActivatedRoute,
+          useValue: { snapshot: { params: opts.routeId !== undefined ? { id: 
opts.routeId } : {} } },
+        },
+        { provide: Router, useValue: routerMock },
+        { provide: HubService, useValue: hubServiceMock },
+        { provide: WorkflowPersistService, useValue: 
workflowPersistServiceMock },
+        { provide: WorkflowActionService, useValue: workflowActionServiceMock 
},
+        { provide: NotificationService, useValue: notificationServiceMock },
+        { provide: UserService, useClass: StubUserService },
+        ...commonTestProviders,
+      ],
+    });
+
+    if ("userOverride" in opts) {
+      (TestBed.inject(UserService) as unknown as StubUserService).user = 
opts.userOverride;
+    }
+  }
+
+  function build(opts: {
+    modalData?: { wid: number } | undefined;
+    routeId?: number;
+    userOverride?: any;
+    detectChanges?: boolean;
+  }) {
+    configure(opts);
+    fixture = TestBed.createComponent(HubWorkflowDetailComponent);
+    component = fixture.componentInstance;
+    if (opts.detectChanges ?? true) {
+      fixture.detectChanges();
+    }
+  }
+
+  beforeEach(() => {
+    makeMocks();
+  });
+
+  describe("constructor / wid resolution", () => {
+    it("uses NZ_MODAL_DATA wid and leaves isHub false", () => {
+      build({ modalData: { wid: 42 }, routeId: 11 });
+      expect(component.wid).toBe(42);
+      expect(component.isHub).toBe(false);
+    });
+
+    it("falls back to route.snapshot.params.id and sets isHub true", () => {
+      build({ modalData: undefined, routeId: 11 });
+      expect(component.wid).toBe(11);
+      expect(component.isHub).toBe(true);
+    });
+
+    it("sets isActivatedUser true for REGULAR", () => {
+      build({ modalData: { wid: 1 } });
+      expect(component.isActivatedUser).toBe(true);
+    });
+
+    it("sets isActivatedUser true for ADMIN", () => {
+      build({
+        modalData: { wid: 1 },
+        userOverride: { ...MOCK_USER, role: Role.ADMIN },
+      });
+      expect(component.isActivatedUser).toBe(true);
+    });
+
+    it("leaves isActivatedUser false for non-activated roles", () => {
+      build({
+        modalData: { wid: 1 },
+        userOverride: { ...MOCK_USER, role: Role.INACTIVE },
+      });
+      expect(component.isActivatedUser).toBe(false);
+    });
+
+    it("disables workflow modification", () => {
+      build({ modalData: { wid: 1 } });
+      
expect(workflowActionServiceMock.disableWorkflowModification).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe("ngOnInit", () => {
+    it("early-returns when wid is undefined", () => {
+      build({ modalData: undefined, routeId: undefined, detectChanges: false 
});
+      expect(component.wid).toBeUndefined();
+      component.ngOnInit();
+      expect(hubServiceMock.getCounts).not.toHaveBeenCalled();
+      expect(hubServiceMock.postView).not.toHaveBeenCalled();
+      expect(hubServiceMock.isLiked).not.toHaveBeenCalled();
+    });
+
+    it("assigns likeCount and cloneCount from getCounts", () => {
+      hubServiceMock.getCounts.mockReturnValue(
+        of([{ entityId: 1, entityType: EntityType.Workflow, counts: { like: 5, 
clone: 3 } }])
+      );
+      build({ modalData: { wid: 1 } });
+      expect(hubServiceMock.getCounts).toHaveBeenCalledWith(
+        [EntityType.Workflow],
+        [1],
+        [ActionType.Like, ActionType.Clone]
+      );
+      expect(component.likeCount).toBe(5);
+      expect(component.cloneCount).toBe(3);
+    });
+
+    it("defaults likeCount and cloneCount to 0 when counts are missing", () => 
{
+      hubServiceMock.getCounts.mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, counts: {} }]));
+      build({ modalData: { wid: 1 } });
+      expect(component.likeCount).toBe(0);
+      expect(component.cloneCount).toBe(0);
+    });
+
+    it("pipes postView through throttleTime and assigns viewCount", () => {
+      expect(THROTTLE_TIME_MS).toBe(1000);
+      hubServiceMock.postView.mockReturnValue(of(12));
+      build({ modalData: { wid: 1 } });
+      expect(hubServiceMock.postView).toHaveBeenCalledWith(1, MOCK_USER.uid, 
EntityType.Workflow);
+      expect(component.viewCount).toBe(12);
+    });
+
+    it("passes 0 as userId to postView when there is no current user", () => {
+      build({ modalData: { wid: 1 }, userOverride: undefined });
+      expect(hubServiceMock.postView).toHaveBeenCalledWith(1, 0, 
EntityType.Workflow);
+    });
+
+    it("sets isLiked from the isLiked response when a user is logged in", () 
=> {
+      hubServiceMock.isLiked.mockReturnValue(of([{ entityId: 1, entityType: 
EntityType.Workflow, isLiked: true }]));
+      build({ modalData: { wid: 1 } });
+      expect(hubServiceMock.isLiked).toHaveBeenCalledWith([1], 
[EntityType.Workflow]);
+      expect(component.isLiked).toBe(true);
+    });
+
+    it("falls back to isLiked = false when the response is empty", () => {
+      hubServiceMock.isLiked.mockReturnValue(of([]));
+      build({ modalData: { wid: 1 } });
+      expect(component.isLiked).toBe(false);
+    });
+
+    it("does not call isLiked when there is no current user", () => {
+      build({ modalData: { wid: 1 }, userOverride: undefined });
+      expect(hubServiceMock.isLiked).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("ngAfterViewInit / loadWorkflowWithId", () => {
+    it("uses retrieveWorkflow when not in hub mode and triggers center event", 
() => {
+      const wf = {} as Workflow;
+      workflowPersistServiceMock.retrieveWorkflow.mockReturnValue(of(wf));
+      build({ modalData: { wid: 5 } });
+      
expect(workflowPersistServiceMock.retrieveWorkflow).toHaveBeenCalledWith(5);
+      
expect(workflowPersistServiceMock.retrievePublicWorkflow).not.toHaveBeenCalled();
+      
expect(workflowActionServiceMock.reloadWorkflow).toHaveBeenCalledWith(wf);
+      expect(stubGraph.triggerCenterEvent).toHaveBeenCalledTimes(1);
+    });
+
+    it("uses retrievePublicWorkflow when in hub mode and triggers center 
event", () => {
+      const wf = {} as Workflow;
+      
workflowPersistServiceMock.retrievePublicWorkflow.mockReturnValue(of(wf));
+      build({ modalData: undefined, routeId: 9 });
+      
expect(workflowPersistServiceMock.retrievePublicWorkflow).toHaveBeenCalledWith(9);
+      
expect(workflowPersistServiceMock.retrieveWorkflow).not.toHaveBeenCalled();
+      
expect(workflowActionServiceMock.reloadWorkflow).toHaveBeenCalledWith(wf);
+      expect(stubGraph.triggerCenterEvent).toHaveBeenCalledTimes(1);
+    });
+
+    it("does not reload or trigger center event when retrieveWorkflow errors", 
() => {
+      
workflowPersistServiceMock.retrieveWorkflow.mockReturnValue(throwError(() => 
new Error("boom")));
+      build({ modalData: { wid: 5 } });
+      expect(workflowActionServiceMock.reloadWorkflow).not.toHaveBeenCalled();
+      expect(stubGraph.triggerCenterEvent).not.toHaveBeenCalled();
+    });
+
+    it("does not reload or trigger center event when retrievePublicWorkflow 
errors", () => {
+      
workflowPersistServiceMock.retrievePublicWorkflow.mockReturnValue(throwError(() 
=> new Error("boom")));
+      build({ modalData: undefined, routeId: 9 });

Review Comment:
   Fixed alongside the private-workflow case — the public-workflow error test 
now asserts `unhandled?.message` equals `Failed to load workflow with id 9` via 
the same `captureUnhandledRxjsError` helper, so removing or altering the error 
handler fails the test rather than passing silently.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to