This is an automated email from the ASF dual-hosted git repository.
aicam pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new c86dc15f3a test: replace placeholder spec with real MenuComponent
tests (#4993)
c86dc15f3a is described below
commit c86dc15f3aa5a8ce39c0dce9a63fff9ee77f519e
Author: Matthew B. <[email protected]>
AuthorDate: Sat May 9 16:40:21 2026 -0700
test: replace placeholder spec with real MenuComponent tests (#4993)
### What changes were proposed in this PR?
Replace the placeholder/commented-out body of menu.component.spec.ts
with a real spec for MenuComponent, covering the top-bar menu's main
actions: run/pause/resume, save, share, version history, and other menu
handlers.
### Any related issues, documentation, or discussions?
Closes: #4966
### How was this PR tested?
npx ng test --watch=false
--include='src/app/workspace/component/menu/menu.component.spec.ts' — 35
tests pass locally under the Vitest runner.
### Was this PR authored or co-authored using generative AI tooling?
Co-Authored with Claude Opus 4.7 in Compliance with ASF
---------
Signed-off-by: Matthew B. <[email protected]>
Co-authored-by: Yicong Huang
<[email protected]>
---
frontend/angular.json | 1 -
.../component/menu/menu.component.spec.ts | 812 +++++++++++++--------
frontend/src/tsconfig.spec.json | 1 -
3 files changed, 504 insertions(+), 310 deletions(-)
diff --git a/frontend/angular.json b/frontend/angular.json
index f5617ecf46..c2f9362329 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -96,7 +96,6 @@
"exclude": [
"**/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts",
"**/app/common/service/user/config/user-config.service.spec.ts",
- "**/app/workspace/component/menu/menu.component.spec.ts",
"**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts",
"**/app/workspace/component/workspace.component.spec.ts"
]
diff --git a/frontend/src/app/workspace/component/menu/menu.component.spec.ts
b/frontend/src/app/workspace/component/menu/menu.component.spec.ts
index c8bce52277..ee46f368a6 100644
--- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts
+++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts
@@ -17,311 +17,507 @@
* under the License.
*/
-// import { RouterTestingModule } from '@angular/router/testing';
-// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-// import { By } from '@angular/platform-browser';
-
-// import { NavigationComponent } from './navigation.component';
-// import { ExecuteWorkflowService } from
'./../../service/execute-workflow/execute-workflow.service';
-// import { WorkflowActionService } from
'./../../service/workflow-graph/model/workflow-action.service';
-// import { UndoRedoService } from
'./../../service/undo-redo/undo-redo.service';
-// import { ValidationWorkflowService } from
'../../service/validation/validation-workflow.service';
-
-// import { CustomNgMaterialModule } from
'../../../common/custom-ng-material.module';
-
-// import { StubOperatorMetadataService } from
'../../service/operator-metadata/stub-operator-metadata.service';
-// import { OperatorMetadataService } from
'../../service/operator-metadata/operator-metadata.service';
-// import { JointUIService } from '../../service/joint-ui/joint-ui.service';
-
-// import { Observable } from 'rxjs';
-// import { marbles } from 'rxjs-marbles';
-// import { HttpClient } from '@angular/common/http';
-// import { mockExecutionResult } from
'../../service/execute-workflow/mock-result-data';
-// import { JointGraphWrapper } from
'../../service/workflow-graph/model/joint-graph-wrapper';
-// import { WorkflowUtilService } from
'../../service/workflow-graph/util/workflow-util.service';
-// import { mockScanPredicate, mockPoint } from
'../../service/workflow-graph/model/mock-workflow-data';
-// import { WorkflowStatusService } from
'../../service/workflow-status/workflow-status.service';
-// import { environment } from '../../../../environments/environment';
-
-// class StubHttpClient {
-
-// public post<T>(): Observable<string> { return Observable.of('a'); }
-// public get<T>(): Observable<string> { return Observable.of('a'); }
-
-// }
-
-// describe('NavigationComponent', () => {
-// let component: NavigationComponent;
-// let fixture: ComponentFixture<NavigationComponent>;
-// let executeWorkFlowService: ExecuteWorkflowService;
-// let workflowActionService: WorkflowActionService;
-// let workflowStatusService: WorkflowStatusService;
-// let undoRedoService: UndoRedoService;
-// let validationWorkflowService: ValidationWorkflowService;
-// beforeEach(async(() => {
-// TestBed.configureTestingModule({
-// declarations: [NavigationComponent],
-// imports: [
-// CustomNgMaterialModule,
-// RouterTestingModule.withRoutes([]),
-// ],
-// providers: [
-// WorkflowActionService,
-// WorkflowUtilService,
-// JointUIService,
-// ExecuteWorkflowService,
-// UndoRedoService,
-// ValidationWorkflowService,
-// { provide: OperatorMetadataService, useClass:
StubOperatorMetadataService },
-// { provide: HttpClient, useClass: StubHttpClient },
-// WorkflowStatusService
-// ]
-// }).compileComponents();
-// }));
-
-// beforeEach(() => {
-// fixture = TestBed.createComponent(NavigationComponent);
-// component = fixture.componentInstance;
-// executeWorkFlowService = TestBed.get(ExecuteWorkflowService);
-// workflowActionService = TestBed.get(WorkflowActionService);
-// workflowStatusService = TestBed.get(WorkflowStatusService);
-// undoRedoService = TestBed.get(UndoRedoService);
-// validationWorkflowService = TestBed.get(ValidationWorkflowService);
-// fixture.detectChanges();
-// });
-
-// it('should create', () => {
-// expect(component).toBeTruthy();
-// });
-
-// // it('should execute the workflow when run button is clicked',
marbles((m) => {
-
-// // const httpClient: HttpClient = TestBed.get(HttpClient);
-// // vi.spyOn(httpClient, 'post').mockReturnValue(
-// // Observable.of(mockExecutionResult)
-// // );
-
-// // const runButtonElement =
fixture.debugElement.query(By.css('.texera-navigation-run-button'));
-// // m.hot('-e-').do(event =>
runButtonElement.triggerEventHandler('click', null)).subscribe();
-
-// // const executionEndStream =
executeWorkFlowService.getExecuteEndedStream().map(value => 'e');
-
-// // const expectedStream = '-e-';
-// // m.expect(executionEndStream).toBeObservable(expectedStream);
-
-// // }));
-
-// // it('should show pause/resume button when the workflow execution begins
and hide the button when execution ends', marbles((m) => {
-
-// // const httpClient: HttpClient = TestBed.get(HttpClient);
-// // vi.spyOn(httpClient, 'post').mockReturnValue(
-// // Observable.of(mockExecutionResult)
-// // );
-
-// // expect(component.isWorkflowRunning).toBeFalsy();
-// // expect(component.isWorkflowPaused).toBeFalsy();
-
-// // executeWorkFlowService.getExecuteStartedStream().subscribe(
-// // () => {
-// // fixture.detectChanges();
-// // expect(component.isWorkflowRunning).toBeTruthy();
-// // expect(component.isWorkflowPaused).toBeFalsy();
-// // }
-// // );
-
-// // executeWorkFlowService.getExecuteEndedStream().subscribe(
-// // () => {
-// // fixture.detectChanges();
-// // expect(component.isWorkflowRunning).toBeFalsy();
-// // expect(component.isWorkflowPaused).toBeFalsy();
-// // }
-// // );
-
-// // m.hot('-e-').do(() => component.onButtonClick()).subscribe();
-
-// // }));
-
-// // it('should call pauseWorkflow function when isWorkflowPaused is
false', () => {
-// // const pauseWorkflowSpy = vi.spyOn(executeWorkFlowService,
'pauseWorkflow');
-// // component.isWorkflowRunning = true;
-// // component.isWorkflowPaused = false;
-
-// // (executeWorkFlowService as any).workflowExecutionID =
'MOCK_EXECUTION_ID';
-
-// // component.onButtonClick();
-// // expect(pauseWorkflowSpy).toHaveBeenCalled();
-// // });
-
-// // it('should call resumeWorkflow function when isWorkflowPaused is
true', () => {
-// // const resumeWorkflowSpy = vi.spyOn(executeWorkFlowService,
'resumeWorkflow');
-// // component.isWorkflowRunning = true;
-// // component.isWorkflowPaused = true;
-
-// // (executeWorkFlowService as any).workflowExecutionID =
'MOCK_EXECUTION_ID';
-
-// // component.onButtonClick();
-// // expect(resumeWorkflowSpy).toHaveBeenCalled();
-// // });
-
-// // it('should not call resumeWorkflow or pauseWorkflow if the workflow is
not currently running', () => {
-
-// // const httpClient: HttpClient = TestBed.get(HttpClient);
-// // vi.spyOn(httpClient, 'post').mockReturnValue(
-// // Observable.of(mockExecutionResult)
-// // );
-
-// // const pauseWorkflowSpy = vi.spyOn(executeWorkFlowService,
'pauseWorkflow');
-// // const resumeWorkflowSpy = vi.spyOn(executeWorkFlowService,
'resumeWorkflow');
-
-// // component.onButtonClick();
-// // expect(pauseWorkflowSpy).toHaveBeenCalledTimes(0);
-// // expect(resumeWorkflowSpy).toHaveBeenCalledTimes(0);
-// // });
-
-// // it('should not call downloadExecutionResult if there is no valid
execution result currently', () => {
-// // const httpClient: HttpClient = TestBed.get(HttpClient);
-// // vi.spyOn(httpClient, 'post').mockReturnValue(
-// // Observable.of(mockExecutionResult)
-// // );
-
-// // const downloadExecutionSpy = vi.spyOn(executeWorkFlowService,
'downloadWorkflowExecutionResult');
-
-// // component.onClickDownloadExecutionResult('txt');
-// // expect(downloadExecutionSpy).toHaveBeenCalledTimes(0);
-// // });
-
-// // it('it should update isWorkflowPaused variable to true when 0 is
returned from getExecutionPauseResumeStream', marbles((m) => {
-// // const endMarbleString = '-e-|';
-// // const endMarblevalues = {
-// // e: 0
-// // };
-
-// // vi.spyOn(executeWorkFlowService,
'getExecutionPauseResumeStream').mockReturnValue(
-// // m.hot(endMarbleString, endMarblevalues)
-// // );
-
-// // const mockComponent = new NavigationComponent(executeWorkFlowService,
-// // workflowActionService, workflowStatusService, undoRedoService,
validationWorkflowService);
-
-// // executeWorkFlowService.getExecutionPauseResumeStream()
-// // .subscribe({
-// // complete: () => {
-// // expect(mockComponent.isWorkflowPaused).toBeTruthy();
-// // }
-// // });
-// // }));
-
-// // it('it should update isWorkflowPaused variable to false when 1 is
returned from getExecutionPauseResumeStream', marbles((m) => {
-// // const endMarbleString = '-e-|';
-// // const endMarblevalues = {
-// // e: 1
-// // };
-
-// // vi.spyOn(executeWorkFlowService,
'getExecutionPauseResumeStream').mockReturnValue(
-// // m.hot(endMarbleString, endMarblevalues)
-// // );
-
-// // const mockComponent = new NavigationComponent(executeWorkFlowService,
-// // workflowActionService, workflowStatusService, undoRedoService,
validationWorkflowService);
-
-// // executeWorkFlowService.getExecutionPauseResumeStream()
-// // .subscribe({
-// // complete: () => {
-// // expect(mockComponent.isWorkflowPaused).toBeFalsy();
-// // }
-// // });
-// // }));
-
-// it('should change zoom to be smaller when user click on the zoom out
buttons', marbles((m) => {
-// // expect initially the zoom ratio is 1;
-// const originalZoomRatio = 1;
-
-// m.hot('-e-').do(() => component.onClickZoomOut()).subscribe();
-//
workflowActionService.getJointGraphWrapper().getWorkflowEditorZoomStream().subscribe(
-// newRatio => {
-// fixture.detectChanges();
-// expect(newRatio).toBeLessThan(originalZoomRatio);
-// expect(newRatio).toEqual(originalZoomRatio -
JointGraphWrapper.ZOOM_CLICK_DIFF);
-// }
-// );
-
-// }));
-
-// it('should change zoom to be bigger when user click on the zoom in
buttons', marbles((m) => {
-
-// // expect initially the zoom ratio is 1;
-// const originalZoomRatio = 1;
-
-// m.hot('-e-').do(() => component.onClickZoomIn()).subscribe();
-//
workflowActionService.getJointGraphWrapper().getWorkflowEditorZoomStream().subscribe(
-// newRatio => {
-// fixture.detectChanges();
-// expect(newRatio).toBeGreaterThan(originalZoomRatio);
-// expect(newRatio).toEqual(originalZoomRatio +
JointGraphWrapper.ZOOM_CLICK_DIFF);
-// }
-// );
-// }));
-
-// it('should execute the zoom in function when the user click on the Zoom
In button', marbles((m) => {
-// m.hot('-e-').do(event => component.onClickZoomIn()).subscribe();
-// const zoomEndStream =
workflowActionService.getJointGraphWrapper().getWorkflowEditorZoomStream().map(value
=> 'e');
-// const expectedStream = '-e-';
-// m.expect(zoomEndStream).toBeObservable(expectedStream);
-// }));
-
-// it('should execute the zoom out function when the user click on the Zoom
Out button', marbles((m) => {
-// m.hot('-e-').do(event => component.onClickZoomOut()).subscribe();
-// const zoomEndStream =
workflowActionService.getJointGraphWrapper().getWorkflowEditorZoomStream().map(value
=> 'e');
-// const expectedStream = '-e-';
-// m.expect(zoomEndStream).toBeObservable(expectedStream);
-// }));
-
-// it('should not increase zoom ratio when the user click on the zoom in
button if zoom ratio already reaches maximum', marbles((m) => {
-//
workflowActionService.getJointGraphWrapper().setZoomProperty(JointGraphWrapper.ZOOM_MAXIMUM);
-// m.hot('-e-').do(() => component.onClickZoomIn()).subscribe();
-// const zoomEndStream =
workflowActionService.getJointGraphWrapper().getWorkflowEditorZoomStream().map(value
=> 'e');
-// const expectedStream = '---';
-// m.expect(zoomEndStream).toBeObservable(expectedStream);
-// }));
-
-// it('should not decrease zoom ratio when the user click on the zoom out
button if zoom ratio already reaches minimum', marbles((m) => {
-//
workflowActionService.getJointGraphWrapper().setZoomProperty(JointGraphWrapper.ZOOM_MINIMUM);
-// m.hot('-e-').do(() => component.onClickZoomOut()).subscribe();
-// const zoomEndStream =
workflowActionService.getJointGraphWrapper().getWorkflowEditorZoomStream().map(value
=> 'e');
-// const expectedStream = '---';
-// m.expect(zoomEndStream).toBeObservable(expectedStream);
-// }));
-
-// it('should execute restore default when the user click on restore
button', marbles((m) => {
-// m.hot('-e-').do(event =>
component.onClickRestoreZoomOffsetDefaullt()).subscribe();
-// const restoreEndStream =
workflowActionService.getJointGraphWrapper().getRestorePaperOffsetStream().map(value
=> 'e');
-// const expectStream = '-e-';
-// m.expect(restoreEndStream).toBeObservable(expectStream);
-// }));
-
-// it('should delete all operators on the graph when user clicks on the
delete all button', marbles((m) => {
-// m.hot('-e-').do(() => {
-// workflowActionService.addOperator(mockScanPredicate, mockPoint);
-// component.onClickDeleteAllOperators();
-// }).subscribe();
-//
expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(0);
-// }));
-
-// // // TODO: this test case related to websocket is not stable, find out
why and fix it
-// // xdescribe('when executionStatus is enabled', () => {
-// // beforeAll(() => {
-// // environment.executionStatusEnabled = true;
-// // });
-
-// // afterAll(() => {
-// // environment.executionStatusEnabled = false;
-// // });
-
-// // it('should send workflowId to websocket when run button is clicked',
() => {
-// // const checkWorkflowSpy = vi.spyOn(workflowStatusService,
'checkStatus').and.stub();
-// // component.onButtonClick();
-// // expect(checkWorkflowSpy).toHaveBeenCalled();
-// // });
-// // });
-
-// });
+import { DatePipe, Location } from "@angular/common";
+import { NO_ERRORS_SCHEMA } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { RouterTestingModule } from "@angular/router/testing";
+import { NzModalService, NzModalModule, NzModalRef } from
"ng-zorro-antd/modal";
+import { BehaviorSubject, of, throwError } from "rxjs";
+
+import { MenuComponent } from "./menu.component";
+import { OperatorMetadataService } from
"../../service/operator-metadata/operator-metadata.service";
+import { StubOperatorMetadataService } from
"../../service/operator-metadata/stub-operator-metadata.service";
+import { ComputingUnitStatusService } from
"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+import { UserService } from "../../../common/service/user/user.service";
+import { StubUserService } from
"../../../common/service/user/stub-user.service";
+import { commonTestProviders } from "../../../common/testing/test-utils";
+import { ExecuteWorkflowService } from
"../../service/execute-workflow/execute-workflow.service";
+import { WorkflowActionService } from
"../../service/workflow-graph/model/workflow-action.service";
+import { ValidationWorkflowService, ValidationOutput } from
"../../service/validation/validation-workflow.service";
+import { PanelService } from "../../service/panel/panel.service";
+import { WorkflowVersionService } from
"../../../dashboard/service/user/workflow-version/workflow-version.service";
+import { WorkflowPersistService } from
"../../../common/service/workflow-persist/workflow-persist.service";
+import { NotificationService } from
"../../../common/service/notification/notification.service";
+import { ExecutionState } from "../../types/execute-workflow.interface";
+import { ComputingUnitState } from
"../../../common/type/computing-unit-connection.interface";
+import { mockPoint, mockScanPredicate } from
"../../service/workflow-graph/model/mock-workflow-data";
+import { saveAs } from "file-saver";
+
+vi.mock("file-saver", () => ({ saveAs: vi.fn() }));
+
+describe("MenuComponent", () => {
+ let component: MenuComponent;
+ let fixture: ComponentFixture<MenuComponent>;
+ let workflowActionService: WorkflowActionService;
+ let executeWorkflowService: ExecuteWorkflowService;
+ let validationWorkflowService: ValidationWorkflowService;
+ let panelService: PanelService;
+ let workflowVersionService: WorkflowVersionService;
+ let workflowPersistService: WorkflowPersistService;
+ let modalService: NzModalService;
+ let notificationService: NotificationService;
+ let location: Location;
+ let validationStream$: BehaviorSubject<ValidationOutput>;
+
+ beforeEach(async () => {
+ TestBed.overrideComponent(MenuComponent, {
+ set: { template: "" },
+ });
+
+ await TestBed.configureTestingModule({
+ imports: [MenuComponent, HttpClientTestingModule,
RouterTestingModule.withRoutes([]), NzModalModule],
+ providers: [
+ DatePipe,
+ { provide: OperatorMetadataService, useClass:
StubOperatorMetadataService },
+ {
+ provide: ComputingUnitStatusService,
+ useValue: {
+ getSelectedComputingUnit: () => of(null),
+ getStatus: () => of(ComputingUnitState.NoComputingUnit),
+ },
+ },
+ { provide: UserService, useClass: StubUserService },
+ ...commonTestProviders,
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ }).compileComponents();
+
+ workflowActionService = TestBed.inject(WorkflowActionService);
+ executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+ validationWorkflowService = TestBed.inject(ValidationWorkflowService);
+ panelService = TestBed.inject(PanelService);
+ workflowVersionService = TestBed.inject(WorkflowVersionService);
+ workflowPersistService = TestBed.inject(WorkflowPersistService);
+ modalService = TestBed.inject(NzModalService);
+ notificationService = TestBed.inject(NotificationService);
+ location = TestBed.inject(Location);
+
+ validationStream$ = new BehaviorSubject<ValidationOutput>({ errors: {},
workflowEmpty: false });
+ vi.spyOn(validationWorkflowService,
"getWorkflowValidationErrorStream").mockReturnValue(
+ validationStream$.asObservable()
+ );
+
+ fixture = TestBed.createComponent(MenuComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ vi.mocked(saveAs).mockClear();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe("getRunButtonBehavior", () => {
+ it("returns 'Invalid Workflow' when the workflow is invalid", () => {
+ component.isWorkflowValid = false;
+ component.isWorkflowEmpty = false;
+
+ const behavior = component.getRunButtonBehavior();
+
+ expect(behavior.text).toBe("Invalid Workflow");
+ expect(behavior.icon).toBe("warning");
+ expect(behavior.disable).toBe(true);
+ });
+
+ it("returns 'Empty Workflow' when the workflow has no operators", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = true;
+
+ const behavior = component.getRunButtonBehavior();
+
+ expect(behavior.text).toBe("Empty Workflow");
+ expect(behavior.icon).toBe("info-circle");
+ expect(behavior.disable).toBe(true);
+ });
+
+ it("returns 'Connect' when no computing unit is attached", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.NoComputingUnit;
+
+ const behavior = component.getRunButtonBehavior();
+
+ expect(behavior.text).toBe("Connect");
+ expect(behavior.icon).toBe("plus-circle");
+ expect(behavior.disable).toBe(false);
+ });
+
+ it("returns 'Run' when connected and execution is uninitialized", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.Running;
+ Object.defineProperty(component.workflowWebsocketService, "isConnected",
{ get: () => true, configurable: true });
+ component.executionState = ExecutionState.Uninitialized;
+
+ const behavior = component.getRunButtonBehavior();
+
+ expect(behavior.text).toBe("Run");
+ expect(behavior.icon).toBe("play-circle");
+ expect(behavior.disable).toBe(false);
+ });
+
+ it("returns 'Pause' while a workflow is running", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.Running;
+ Object.defineProperty(component.workflowWebsocketService, "isConnected",
{ get: () => true, configurable: true });
+ component.executionState = ExecutionState.Running;
+
+ const pauseSpy = vi.spyOn(executeWorkflowService,
"pauseWorkflow").mockImplementation(() => {});
+ const behavior = component.getRunButtonBehavior();
+ behavior.onClick();
+
+ expect(behavior.text).toBe("Pause");
+ expect(behavior.disable).toBe(false);
+ expect(pauseSpy).toHaveBeenCalled();
+ });
+
+ it("returns 'Resume' when execution is paused", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.Running;
+ Object.defineProperty(component.workflowWebsocketService, "isConnected",
{ get: () => true, configurable: true });
+ component.executionState = ExecutionState.Paused;
+
+ const resumeSpy = vi.spyOn(executeWorkflowService,
"resumeWorkflow").mockImplementation(() => {});
+ const behavior = component.getRunButtonBehavior();
+ behavior.onClick();
+
+ expect(behavior.text).toBe("Resume");
+ expect(resumeSpy).toHaveBeenCalled();
+ });
+
+ it("returns 'Connecting' when a unit exists but the websocket is not
connected", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.Running;
+ Object.defineProperty(component.workflowWebsocketService, "isConnected",
{
+ get: () => false,
+ configurable: true,
+ });
+
+ const behavior = component.getRunButtonBehavior();
+
+ expect(behavior.text).toBe("Connecting");
+ expect(behavior.disable).toBe(true);
+ });
+ });
+
+ it("applyRunButtonBehavior copies the behavior onto the bound fields", () =>
{
+ const handler = () => {};
+ component.applyRunButtonBehavior({
+ text: "Custom",
+ icon: "custom-icon",
+ disable: true,
+ onClick: handler,
+ });
+
+ expect(component.runButtonText).toBe("Custom");
+ expect(component.runIcon).toBe("custom-icon");
+ expect(component.runDisable).toBe(true);
+ expect(component.onClickRunHandler).toBe(handler);
+ });
+
+ it("re-applies run button behavior when the validation stream reports an
empty workflow", () => {
+ validationStream$.next({ errors: {}, workflowEmpty: true });
+
+ expect(component.isWorkflowEmpty).toBe(true);
+ expect(component.runButtonText).toBe("Empty Workflow");
+ expect(component.runDisable).toBe(true);
+ });
+
+ describe("hasOperators", () => {
+ it("returns false on an empty graph", () => {
+ expect(component.hasOperators()).toBe(false);
+ });
+
+ it("returns true once an operator is added", () => {
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+ expect(component.hasOperators()).toBe(true);
+ });
+ });
+
+ it("onClickAddCommentBox delegates to the workflow action service", () => {
+ const addCommentBoxSpy = vi.spyOn(workflowActionService, "addCommentBox");
+
+ component.onClickAddCommentBox();
+
+ expect(addCommentBoxSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("onClickDeleteAllOperators removes every operator from the graph", () => {
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+
expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(1);
+
+ component.onClickDeleteAllOperators();
+
+
expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(0);
+ });
+
+ it("onClickAutoLayout is a no-op when there are no operators", () => {
+ const autoLayoutSpy = vi.spyOn(workflowActionService,
"autoLayoutWorkflow");
+
+ component.onClickAutoLayout();
+
+ expect(autoLayoutSpy).not.toHaveBeenCalled();
+ });
+
+ it("onClickAutoLayout invokes auto layout when operators are present", () =>
{
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+ const autoLayoutSpy = vi.spyOn(workflowActionService,
"autoLayoutWorkflow").mockImplementation(() => {});
+
+ component.onClickAutoLayout();
+
+ expect(autoLayoutSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("handleKill delegates to executeWorkflowService.killWorkflow", () => {
+ const killSpy = vi.spyOn(executeWorkflowService,
"killWorkflow").mockImplementation(() => {});
+
+ component.handleKill();
+
+ expect(killSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("handleCheckpoint delegates to
executeWorkflowService.takeGlobalCheckpoint", () => {
+ const checkpointSpy = vi.spyOn(executeWorkflowService,
"takeGlobalCheckpoint").mockImplementation(() => {});
+
+ component.handleCheckpoint();
+
+ expect(checkpointSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("onClickClosePanels and onClickResetPanels delegate to PanelService", ()
=> {
+ const closeSpy = vi.spyOn(panelService,
"closePanels").mockImplementation(() => {});
+ const resetSpy = vi.spyOn(panelService,
"resetPanels").mockImplementation(() => {});
+
+ component.onClickClosePanels();
+ component.onClickResetPanels();
+
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ expect(resetSpy).toHaveBeenCalledTimes(1);
+ });
+
+ describe("runWorkflow", () => {
+ beforeEach(() => {
+ component.computingUnitSelectionComponent = {
+ newComputingUnitName: "",
+ showAddComputeUnitModalVisible: vi.fn(),
+ } as any;
+ });
+
+ it("does nothing when the workflow is invalid", () => {
+ component.isWorkflowValid = false;
+ component.isWorkflowEmpty = false;
+ const executeSpy = vi.spyOn(executeWorkflowService,
"executeWorkflowWithEmailNotification");
+
+ component.runWorkflow();
+
+ expect(executeSpy).not.toHaveBeenCalled();
+
expect(component.computingUnitSelectionComponent.showAddComputeUnitModalVisible).not.toHaveBeenCalled();
+ });
+
+ it("does nothing when the workflow is empty", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = true;
+ const executeSpy = vi.spyOn(executeWorkflowService,
"executeWorkflowWithEmailNotification");
+
+ component.runWorkflow();
+
+ expect(executeSpy).not.toHaveBeenCalled();
+ });
+
+ it("opens the add-computing-unit modal when no unit is connected", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.NoComputingUnit;
+ component.currentWorkflowName = "wf";
+ const executeSpy = vi.spyOn(executeWorkflowService,
"executeWorkflowWithEmailNotification");
+
+ component.runWorkflow();
+
+
expect(component.computingUnitSelectionComponent.newComputingUnitName).toBe("wf's
Computing Unit");
+
expect(component.computingUnitSelectionComponent.showAddComputeUnitModalVisible).toHaveBeenCalledTimes(1);
+ expect(executeSpy).not.toHaveBeenCalled();
+ });
+
+ it("submits the execution when connected", () => {
+ component.isWorkflowValid = true;
+ component.isWorkflowEmpty = false;
+ component.computingUnitStatus = ComputingUnitState.Running;
+ component.currentExecutionName = "exec-1";
+ const executeSpy = vi
+ .spyOn(executeWorkflowService, "executeWorkflowWithEmailNotification")
+ .mockImplementation(() => {});
+
+ component.runWorkflow();
+
+ expect(executeSpy).toHaveBeenCalledWith("exec-1", expect.any(Boolean));
+ });
+ });
+
+ it("onWorkflowNameChange forwards the new name to the workflow action
service", () => {
+ const setNameSpy = vi.spyOn(workflowActionService, "setWorkflowName");
+ component.currentWorkflowName = "renamed";
+
+ component.onWorkflowNameChange();
+
+ expect(setNameSpy).toHaveBeenCalledWith("renamed");
+ });
+
+ describe("onClickExportWorkflow (save)", () => {
+ it("serializes the workflow content as JSON and downloads it under the
workflow name", () => {
+ const fakeContent = { operators: [{ operatorID: "op1" }], links: [],
commentBoxes: [], settings: {} } as any;
+ vi.spyOn(workflowActionService,
"getWorkflowContent").mockReturnValue(fakeContent);
+ component.currentWorkflowName = "my-workflow";
+
+ component.onClickExportWorkflow();
+
+ expect(saveAs).toHaveBeenCalledTimes(1);
+ const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0] as [Blob,
string];
+ expect(fileNameArg).toBe("my-workflow.json");
+ expect(blobArg).toBeInstanceOf(Blob);
+ expect(blobArg.type).toBe("text/plain;charset=utf-8");
+ });
+ });
+
+ describe("version history", () => {
+ it("onClickGetAllVersions delegates to
workflowVersionService.displayWorkflowVersions", () => {
+ const displaySpy = vi.spyOn(workflowVersionService,
"displayWorkflowVersions").mockImplementation(() => {});
+
+ component.onClickGetAllVersions();
+
+ expect(displaySpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("closeParticularVersionDisplay delegates to workflowVersionService", ()
=> {
+ const closeSpy = vi.spyOn(workflowVersionService,
"closeParticularVersionDisplay").mockImplementation(() => {});
+
+ component.closeParticularVersionDisplay();
+
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("revertToVersion reverts and then persists the workflow", () => {
+ const revertSpy = vi.spyOn(workflowVersionService,
"revertToVersion").mockImplementation(() => {});
+ const persistSpy = vi
+ .spyOn(workflowPersistService, "persistWorkflow")
+ .mockReturnValue(of(workflowActionService.getWorkflow()));
+
+ component.revertToVersion();
+
+ expect(revertSpy).toHaveBeenCalledTimes(1);
+ expect(persistSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("cloneVersion notifies success and closes the version panel when
cloning succeeds", () => {
+ vi.spyOn(workflowVersionService,
"cloneWorkflowVersion").mockReturnValue(of(42));
+ const successSpy = vi.spyOn(notificationService,
"success").mockImplementation(() => {});
+ const closeSpy = vi.spyOn(workflowVersionService,
"closeParticularVersionDisplay").mockImplementation(() => {});
+
+ component.cloneVersion();
+
+ expect(successSpy).toHaveBeenCalledTimes(1);
+ expect(successSpy.mock.calls[0][0]).toContain("42");
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("cloneVersion shows an error notification and does not close the panel
when cloning fails", () => {
+ vi.spyOn(workflowVersionService,
"cloneWorkflowVersion").mockReturnValue(throwError(() => new Error("boom")));
+ const errorSpy = vi.spyOn(notificationService,
"error").mockImplementation(() => {});
+ const successSpy = vi.spyOn(notificationService,
"success").mockImplementation(() => {});
+ const closeSpy = vi.spyOn(workflowVersionService,
"closeParticularVersionDisplay").mockImplementation(() => {});
+
+ component.cloneVersion();
+
+ expect(errorSpy).toHaveBeenCalledTimes(1);
+ expect(successSpy).not.toHaveBeenCalled();
+ expect(closeSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("onClickOpenShareAccess (share)", () => {
+ it("looks up workflow owners and opens the share-access modal", async ()
=> {
+ vi.spyOn(workflowPersistService,
"retrieveOwners").mockReturnValue(of(["[email protected]"]));
+ const fakeModalRef = { afterClose: of(undefined) } as unknown as
NzModalRef;
+ const createSpy = vi.spyOn(modalService,
"create").mockReturnValue(fakeModalRef);
+ component.workflowId = 7;
+ component.writeAccess = true;
+
+ await component.onClickOpenShareAccess();
+
+ expect(createSpy).toHaveBeenCalledTimes(1);
+ const config = createSpy.mock.calls[0][0] as any;
+ expect(config.nzTitle).toBe("Share this workflow with others");
+ expect(config.nzData).toEqual(
+ expect.objectContaining({
+ writeAccess: true,
+ type: "workflow",
+ id: 7,
+ allOwners: ["[email protected]"],
+ inWorkspace: true,
+ })
+ );
+ });
+ });
+
+ it("onClickCreateNewWorkflow resets the graph and navigates back to root",
() => {
+ const resetSpy = vi.spyOn(workflowActionService,
"resetAsNewWorkflow").mockImplementation(() => {});
+ const goSpy = vi.spyOn(location, "go").mockImplementation(() => {});
+
+ component.onClickCreateNewWorkflow();
+
+ expect(resetSpy).toHaveBeenCalledTimes(1);
+ expect(goSpy).toHaveBeenCalledWith("/");
+ });
+
+ it("onClickRestoreZoomOffsetDefault delegates to the joint graph wrapper",
() => {
+ const restoreSpy = vi
+ .spyOn(workflowActionService.getJointGraphWrapper(),
"restoreDefaultZoomAndOffset")
+ .mockImplementation(() => {});
+
+ component.onClickRestoreZoomOffsetDefault();
+
+ expect(restoreSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("onClickEditDescription opens the markdown description modal seeded with
the current description", () => {
+ vi.spyOn(workflowActionService, "getWorkflow").mockReturnValue({
+ content: { operators: [], links: [], commentBoxes: [], settings: {} } as
any,
+ name: "wf",
+ description: "hello world",
+ wid: 1,
+ creationTime: undefined,
+ lastModifiedTime: undefined,
+ readonly: false,
+ isPublished: 0,
+ });
+ const fakeModalRef = {
+ afterClose: of(undefined),
+ getContentComponent: () => ({ descriptionChange: of() }),
+ close: vi.fn(),
+ } as unknown as NzModalRef;
+ const createSpy = vi.spyOn(modalService,
"create").mockReturnValue(fakeModalRef);
+
+ component.onClickEditDescription();
+
+ expect(createSpy).toHaveBeenCalledTimes(1);
+ const config = createSpy.mock.calls[0][0] as any;
+ expect(config.nzTitle).toBe("Edit Workflow Description");
+ expect(config.nzData).toEqual({ description: "hello world" });
+ });
+
+ it("onClickExportExecutionResult opens the result-exportation modal with the
current workflow name", () => {
+ const fakeModalRef = { afterClose: of(undefined) } as unknown as
NzModalRef;
+ const createSpy = vi.spyOn(modalService,
"create").mockReturnValue(fakeModalRef);
+ component.currentWorkflowName = "report-wf";
+
+ component.onClickExportExecutionResult();
+
+ expect(createSpy).toHaveBeenCalledTimes(1);
+ const config = createSpy.mock.calls[0][0] as any;
+ expect(config.nzTitle).toBe("Export All Operators Result");
+ expect(config.nzData).toEqual(expect.objectContaining({ workflowName:
"report-wf", sourceTriggered: "menu" }));
+ });
+});
diff --git a/frontend/src/tsconfig.spec.json b/frontend/src/tsconfig.spec.json
index eacd678923..5a73e241a3 100644
--- a/frontend/src/tsconfig.spec.json
+++ b/frontend/src/tsconfig.spec.json
@@ -15,7 +15,6 @@
// need real test cases written before they can be re-enabled.
"**/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts",
"**/app/common/service/user/config/user-config.service.spec.ts",
- "**/app/workspace/component/menu/menu.component.spec.ts",
"**/app/workspace/component/workspace.component.spec.ts",
// jointjs paper geometry: every test in this suite asserts on