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: Fixed. The route mock now uses a string `id` (runtime reality), and the constructor coerces via `Number(routeId)` so the numeric `wid` the services expect is preserved. ########## 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. The test now asserts the thrown `Failed to load workflow with id 5` message (captured via `config.onUnhandledError`), so removing the error handler fails the test. ########## 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: Same fix here, asserting `Failed to load workflow with id 9`. -- 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]
