Copilot commented on code in PR #4993:
URL: https://github.com/apache/texera/pull/4993#discussion_r3212270759


##########
frontend/src/app/workspace/component/menu/menu.component.spec.ts:
##########
@@ -17,311 +17,503 @@
  * 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 { 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 } 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;
+
+  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();
+
+    fixture = TestBed.createComponent(MenuComponent);
+    component = fixture.componentInstance;
+    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);
+    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", () => {
+    (validationWorkflowService as any).workflowValidationErrorStream.next({
+      errors: {},
+      workflowEmpty: true,
+    });
+

Review Comment:
   This test reaches into ValidationWorkflowService’s private 
`workflowValidationErrorStream` via `as any` to emit values, which makes the 
spec brittle against internal refactors. Prefer stubbing 
`getWorkflowValidationErrorStream()` to return a Subject/BehaviorSubject you 
control in the spec, then emit on that instead.



-- 
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