aglinxinyuan commented on code in PR #6370:
URL: https://github.com/apache/texera/pull/6370#discussion_r3567373423


##########
frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts:
##########
@@ -32,8 +32,11 @@ import {
   mockPoint,
   mockResultPredicate,
   mockScanPredicate,
+  mockScanSentimentLink,
   mockSentimentPredicate,
 } from "../workflow-graph/model/mock-workflow-data";
+import { NotificationService } from 
"../../../common/service/notification/notification.service";
+import { ExecuteWorkflowService } from 
"../execute-workflow/execute-workflow.service";

Review Comment:
   Stubbed both: `ExecuteWorkflowService` → `{ executeWorkflow: vi.fn() }` and 
`NotificationService` → vi.fn toast methods, provided via `useValue` — so the 
spec no longer boots the real execution/notification stacks (5232a09). Existing 
spies work unchanged against the stubs.



##########
frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts:
##########
@@ -184,4 +191,257 @@ describe("OperatorMenuService", () => {
       expect(service.isToViewResult).toBe(false);
     });
   });
+
+  describe("operator action callbacks", () => {
+    beforeEach(() => {
+      workflowActionService.addOperator(mockScanPredicate, mockPoint);
+      
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+    });
+
+    it("disables then re-enables the highlighted operators based on 
isDisableOperator", () => {
+      const texeraGraph = workflowActionService.getTexeraGraph();
+      // fresh non-disabled operator → the button is in "disable" mode.
+      expect(service.isDisableOperator).toBe(true);
+
+      service.disableHighlightedOperators();
+      
expect(texeraGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(true);
+      // recompute flips the toggle so the next click re-enables.
+      expect(service.isDisableOperator).toBe(false);
+
+      service.disableHighlightedOperators();
+      
expect(texeraGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(false);
+    });
+
+    it("sets then unsets view-result on the highlighted non-sink operators", 
() => {
+      const texeraGraph = workflowActionService.getTexeraGraph();
+      expect(service.isToViewResult).toBe(true);
+
+      service.viewResultHighlightedOperators();
+      
expect(texeraGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(true);
+      expect(service.isToViewResult).toBe(false);
+
+      service.viewResultHighlightedOperators();
+      
expect(texeraGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(false);
+    });
+
+    it("marks then unmarks the highlighted non-sink operators for reuse", () 
=> {
+      const texeraGraph = workflowActionService.getTexeraGraph();
+      expect(service.isMarkForReuse).toBe(true);
+
+      service.reuseResultHighlightedOperator();
+      
expect(texeraGraph.isMarkedForReuseResult(mockScanPredicate.operatorID)).toBe(true);
+      expect(service.isMarkForReuse).toBe(false);
+
+      service.reuseResultHighlightedOperator();
+      
expect(texeraGraph.isMarkedForReuseResult(mockScanPredicate.operatorID)).toBe(false);
+    });
+  });
+
+  describe("executeUpToOperator", () => {
+    it("errors and does not execute when zero operators are highlighted", () 
=> {
+      const notificationService = TestBed.inject(NotificationService);
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      const errorSpy = vi.spyOn(notificationService, 
"error").mockImplementation(() => {});
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflow").mockImplementation(() => undefined);
+
+      service.executeUpToOperator();
+
+      expect(errorSpy).toHaveBeenCalledWith("Can only execute to exactly one 
target operator.");
+      expect(executeSpy).not.toHaveBeenCalled();
+    });
+
+    it("errors and does not execute when more than one operator is 
highlighted", () => {
+      const notificationService = TestBed.inject(NotificationService);
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      const errorSpy = vi.spyOn(notificationService, 
"error").mockImplementation(() => {});
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflow").mockImplementation(() => undefined);
+      workflowActionService.addOperatorsAndLinks(
+        [
+          { op: mockScanPredicate, pos: mockPoint },
+          { op: mockSentimentPredicate, pos: mockPoint },
+        ],
+        []
+      );
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      
wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs());
+      wrapper.setMultiSelectMode(true);
+      wrapper.highlightOperators(mockScanPredicate.operatorID, 
mockSentimentPredicate.operatorID);
+
+      service.executeUpToOperator();
+
+      expect(errorSpy).toHaveBeenCalledWith("Can only execute to exactly one 
target operator.");
+      expect(executeSpy).not.toHaveBeenCalled();
+    });
+
+    it("executes the workflow up to the single highlighted operator", () => {
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflow").mockImplementation(() => undefined);
+      workflowActionService.addOperator(mockScanPredicate, mockPoint);
+      
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+
+      service.executeUpToOperator();
+
+      expect(executeSpy).toHaveBeenCalledWith("", 
mockScanPredicate.operatorID);
+    });
+  });
+
+  describe("saveHighlightedElements", () => {
+    let originalClipboard: PropertyDescriptor | undefined;
+    let writeText: ReturnType<typeof vi.fn>;
+
+    beforeEach(() => {
+      originalClipboard = Object.getOwnPropertyDescriptor(navigator, 
"clipboard");
+      writeText = vi.fn().mockResolvedValue(undefined);
+      Object.defineProperty(navigator, "clipboard", { value: { writeText }, 
configurable: true });
+    });
+
+    afterEach(() => {
+      if (originalClipboard) {
+        Object.defineProperty(navigator, "clipboard", originalClipboard);
+      } else {
+        delete (navigator as any).clipboard;
+      }
+    });
+
+    it("serializes the highlighted operators, links, and comment boxes to the 
clipboard", () => {
+      // give the comment box a distinct ID: operators and comment boxes share 
the joint-graph
+      // cell namespace, and mockCommentBox reuses operator ID "1".
+      const commentBox = { ...mockCommentBox, commentBoxID: "comment-box-a" };
+      workflowActionService.addOperatorsAndLinks(
+        [
+          { op: mockScanPredicate, pos: mockPoint },
+          { op: mockSentimentPredicate, pos: mockPoint },
+        ],
+        [mockScanSentimentLink]
+      );
+      workflowActionService.addCommentBox(commentBox);
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      // start from a clean highlight state, then multi-select every element 
we want copied.
+      
wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs());
+      wrapper.setMultiSelectMode(true);
+      wrapper.highlightOperators(mockScanPredicate.operatorID, 
mockSentimentPredicate.operatorID);
+      wrapper.highlightLinks(mockScanSentimentLink.linkID);
+      wrapper.highlightCommentBoxes(commentBox.commentBoxID);
+
+      service.saveHighlightedElements();
+
+      expect(writeText).toHaveBeenCalledTimes(1);
+      const serialized = JSON.parse(writeText.mock.calls[0][0]);
+      expect(serialized.operators.map((op: any) => 
op.operatorID).sort()).toEqual(["1", "2"]);
+      expect(Object.keys(serialized.operatorPositions).sort()).toEqual(["1", 
"2"]);

Review Comment:
   Now asserts against `[mockScanPredicate.operatorID, 
mockSentimentPredicate.operatorID].sort()` instead of the \"1\"/\"2\" literals, 
so the test tracks the fixtures (5232a09).



##########
frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts:
##########
@@ -184,4 +191,257 @@ describe("OperatorMenuService", () => {
       expect(service.isToViewResult).toBe(false);
     });
   });
+
+  describe("operator action callbacks", () => {
+    beforeEach(() => {
+      workflowActionService.addOperator(mockScanPredicate, mockPoint);
+      
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+    });
+
+    it("disables then re-enables the highlighted operators based on 
isDisableOperator", () => {
+      const texeraGraph = workflowActionService.getTexeraGraph();
+      // fresh non-disabled operator → the button is in "disable" mode.
+      expect(service.isDisableOperator).toBe(true);
+
+      service.disableHighlightedOperators();
+      
expect(texeraGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(true);
+      // recompute flips the toggle so the next click re-enables.
+      expect(service.isDisableOperator).toBe(false);
+
+      service.disableHighlightedOperators();
+      
expect(texeraGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(false);
+    });
+
+    it("sets then unsets view-result on the highlighted non-sink operators", 
() => {
+      const texeraGraph = workflowActionService.getTexeraGraph();
+      expect(service.isToViewResult).toBe(true);
+
+      service.viewResultHighlightedOperators();
+      
expect(texeraGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(true);
+      expect(service.isToViewResult).toBe(false);
+
+      service.viewResultHighlightedOperators();
+      
expect(texeraGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(false);
+    });
+
+    it("marks then unmarks the highlighted non-sink operators for reuse", () 
=> {
+      const texeraGraph = workflowActionService.getTexeraGraph();
+      expect(service.isMarkForReuse).toBe(true);
+
+      service.reuseResultHighlightedOperator();
+      
expect(texeraGraph.isMarkedForReuseResult(mockScanPredicate.operatorID)).toBe(true);
+      expect(service.isMarkForReuse).toBe(false);
+
+      service.reuseResultHighlightedOperator();
+      
expect(texeraGraph.isMarkedForReuseResult(mockScanPredicate.operatorID)).toBe(false);
+    });
+  });
+
+  describe("executeUpToOperator", () => {
+    it("errors and does not execute when zero operators are highlighted", () 
=> {
+      const notificationService = TestBed.inject(NotificationService);
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      const errorSpy = vi.spyOn(notificationService, 
"error").mockImplementation(() => {});
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflow").mockImplementation(() => undefined);
+
+      service.executeUpToOperator();
+
+      expect(errorSpy).toHaveBeenCalledWith("Can only execute to exactly one 
target operator.");
+      expect(executeSpy).not.toHaveBeenCalled();
+    });
+
+    it("errors and does not execute when more than one operator is 
highlighted", () => {
+      const notificationService = TestBed.inject(NotificationService);
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      const errorSpy = vi.spyOn(notificationService, 
"error").mockImplementation(() => {});
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflow").mockImplementation(() => undefined);
+      workflowActionService.addOperatorsAndLinks(
+        [
+          { op: mockScanPredicate, pos: mockPoint },
+          { op: mockSentimentPredicate, pos: mockPoint },
+        ],
+        []
+      );
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      
wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs());
+      wrapper.setMultiSelectMode(true);
+      wrapper.highlightOperators(mockScanPredicate.operatorID, 
mockSentimentPredicate.operatorID);
+
+      service.executeUpToOperator();
+
+      expect(errorSpy).toHaveBeenCalledWith("Can only execute to exactly one 
target operator.");
+      expect(executeSpy).not.toHaveBeenCalled();
+    });
+
+    it("executes the workflow up to the single highlighted operator", () => {
+      const executeWorkflowService = TestBed.inject(ExecuteWorkflowService);
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflow").mockImplementation(() => undefined);
+      workflowActionService.addOperator(mockScanPredicate, mockPoint);
+      
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+
+      service.executeUpToOperator();
+
+      expect(executeSpy).toHaveBeenCalledWith("", 
mockScanPredicate.operatorID);
+    });
+  });
+
+  describe("saveHighlightedElements", () => {
+    let originalClipboard: PropertyDescriptor | undefined;
+    let writeText: ReturnType<typeof vi.fn>;
+
+    beforeEach(() => {
+      originalClipboard = Object.getOwnPropertyDescriptor(navigator, 
"clipboard");
+      writeText = vi.fn().mockResolvedValue(undefined);
+      Object.defineProperty(navigator, "clipboard", { value: { writeText }, 
configurable: true });
+    });
+
+    afterEach(() => {
+      if (originalClipboard) {
+        Object.defineProperty(navigator, "clipboard", originalClipboard);
+      } else {
+        delete (navigator as any).clipboard;
+      }
+    });
+
+    it("serializes the highlighted operators, links, and comment boxes to the 
clipboard", () => {
+      // give the comment box a distinct ID: operators and comment boxes share 
the joint-graph
+      // cell namespace, and mockCommentBox reuses operator ID "1".
+      const commentBox = { ...mockCommentBox, commentBoxID: "comment-box-a" };
+      workflowActionService.addOperatorsAndLinks(
+        [
+          { op: mockScanPredicate, pos: mockPoint },
+          { op: mockSentimentPredicate, pos: mockPoint },
+        ],
+        [mockScanSentimentLink]
+      );
+      workflowActionService.addCommentBox(commentBox);
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      // start from a clean highlight state, then multi-select every element 
we want copied.
+      
wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs());
+      wrapper.setMultiSelectMode(true);
+      wrapper.highlightOperators(mockScanPredicate.operatorID, 
mockSentimentPredicate.operatorID);
+      wrapper.highlightLinks(mockScanSentimentLink.linkID);
+      wrapper.highlightCommentBoxes(commentBox.commentBoxID);
+
+      service.saveHighlightedElements();
+
+      expect(writeText).toHaveBeenCalledTimes(1);
+      const serialized = JSON.parse(writeText.mock.calls[0][0]);
+      expect(serialized.operators.map((op: any) => 
op.operatorID).sort()).toEqual(["1", "2"]);
+      expect(Object.keys(serialized.operatorPositions).sort()).toEqual(["1", 
"2"]);
+      // the serialized position mirrors what the graph reports for that 
operator.
+      
expect(serialized.operatorPositions[mockScanPredicate.operatorID]).toEqual(
+        wrapper.getElementPosition(mockScanPredicate.operatorID)
+      );
+      expect(serialized.links.map((link: any) => 
link.linkID)).toEqual([mockScanSentimentLink.linkID]);
+      expect(serialized.commentBoxes.map((box: any) => 
box.commentBoxID)).toEqual([commentBox.commentBoxID]);
+    });
+
+    it("notifies the user when writing to the clipboard is rejected", async () 
=> {
+      writeText.mockRejectedValue(new Error("no permission"));
+      const notificationService = TestBed.inject(NotificationService);
+      const errorSpy = vi.spyOn(notificationService, 
"error").mockImplementation(() => {});
+      workflowActionService.addOperator(mockScanPredicate, mockPoint);
+      
workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);
+
+      service.saveHighlightedElements();
+      await flushAsync();
+
+      expect(errorSpy).toHaveBeenCalledWith("Copy failed. You don't have the 
permission to write to the clipboard.");
+    });
+  });
+
+  describe("performPasteOperation", () => {
+    let originalClipboard: PropertyDescriptor | undefined;
+    let readText: ReturnType<typeof vi.fn>;
+
+    beforeEach(() => {
+      originalClipboard = Object.getOwnPropertyDescriptor(navigator, 
"clipboard");
+      readText = vi.fn();
+      Object.defineProperty(navigator, "clipboard", { value: { readText }, 
configurable: true });
+    });
+
+    afterEach(() => {
+      if (originalClipboard) {
+        Object.defineProperty(navigator, "clipboard", originalClipboard);
+      } else {
+        delete (navigator as any).clipboard;
+      }
+    });
+
+    it("pastes operators, links, and comment boxes with fresh IDs and 
non-overlapping positions", async () => {
+      // both operators share the same clipboard position, so pasting the 
second one must be
+      // shifted by the getNonOverlappingPosition loop to avoid landing on the 
first.
+      const clipboard = {
+        operators: [mockScanPredicate, mockSentimentPredicate],
+        operatorPositions: {
+          [mockScanPredicate.operatorID]: { x: 100, y: 100 },
+          [mockSentimentPredicate.operatorID]: { x: 100, y: 100 },
+        },
+        links: [mockScanSentimentLink],
+        commentBoxes: [mockCommentBox],
+      };
+      readText.mockResolvedValue(JSON.stringify(clipboard));
+      const texeraGraph = workflowActionService.getTexeraGraph();
+
+      service.performPasteOperation();
+      await flushAsync();
+
+      const pastedOperators = texeraGraph.getAllOperators();
+      expect(pastedOperators.length).toBe(2);
+      // fresh IDs are assigned; none of the originals are reused.
+      expect(pastedOperators.map(op => 
op.operatorID)).not.toContain(mockScanPredicate.operatorID);
+      expect(pastedOperators.map(op => 
op.operatorID)).not.toContain(mockSentimentPredicate.operatorID);
+      expect(texeraGraph.getAllLinks().length).toBe(1);
+      expect(texeraGraph.getAllCommentBoxes().length).toBe(1);
+      // the pasted comment box is a distinct copy, not the original.
+      
expect(texeraGraph.getAllCommentBoxes()[0].commentBoxID).not.toBe(mockCommentBox.commentBoxID);

Review Comment:
   Added the position assertion: both clipboard entries share one position, and 
the test now reads the two pasted elements' positions via 
`getJointGraphWrapper().getElementPosition(...)` and asserts they differ — 
verifying getNonOverlappingPosition actually shifted the second paste (5232a09).



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