This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-6772-b405528cc4000e69ec1473b08f288b3a1c46ae88
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 355f121f28bc794d729317c34cb51402e72f38c0
Author: Xinyuan Lin <[email protected]>
AuthorDate: Wed Jul 22 20:12:00 2026 -0700

    test(frontend): add unit tests for SharedModelChangeHandler (#6772)
    
    ### What changes were proposed in this PR?
    
    Adds a new spec for `SharedModelChangeHandler` (previously only ~74%
    covered transitively, no dedicated spec). The 29 tests drive its yjs
    deep-observers through a constructed `WorkflowGraph` and assert that the
    operator/link/comment-box add-delete, element-position, and deep
    operator/comment-box property-change subjects fire with the right
    payloads, plus `newYDocLoadedSubject` re-registration. An `afterEach`
    tears down the shared model.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6768.
    
    ### How was this PR tested?
    
    `ng test --include='**/shared-model-change-handler.spec.ts'` -> 29/29
    passing. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
    
    ---------
    
    Signed-off-by: Xinyuan Lin <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../model/shared-model-change-handler.spec.ts      | 545 +++++++++++++++++++++
 1 file changed, 545 insertions(+)

diff --git 
a/frontend/src/app/workspace/service/workflow-graph/model/shared-model-change-handler.spec.ts
 
b/frontend/src/app/workspace/service/workflow-graph/model/shared-model-change-handler.spec.ts
new file mode 100644
index 0000000000..af7ad659d2
--- /dev/null
+++ 
b/frontend/src/app/workspace/service/workflow-graph/model/shared-model-change-handler.spec.ts
@@ -0,0 +1,545 @@
+/**
+ * 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 { TestBed } from "@angular/core/testing";
+import * as joint from "jointjs";
+import { OperatorMetadataService } from 
"../../operator-metadata/operator-metadata.service";
+import { StubOperatorMetadataService } from 
"../../operator-metadata/stub-operator-metadata.service";
+import { JointUIService } from "../../joint-ui/joint-ui.service";
+import { UndoRedoService } from "../../undo-redo/undo-redo.service";
+import { WorkflowUtilService } from "../util/workflow-util.service";
+import { WorkflowActionService } from "./workflow-action.service";
+import { WorkflowGraph } from "./workflow-graph";
+import { JointGraphWrapper } from "./joint-graph-wrapper";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
+import { GuiConfigService } from 
"../../../../common/service/gui-config.service";
+import {
+  mockCommentBox,
+  mockPoint,
+  mockResultPredicate,
+  mockScanPredicate,
+  mockScanResultLink,
+  mockSentimentPredicate,
+} from "./mock-workflow-data";
+import { Comment, OperatorLink, OperatorPredicate, Point } from 
"../../../types/workflow-common.interface";
+import { createYTypeFromObject } from 
"../../../types/shared-editing.interface";
+
+/**
+ * SharedModelChangeHandler is constructed internally by 
WorkflowActionService. It wires yjs deep-observers on the
+ * TexeraGraph's shared model (operatorIDMap, operatorLinkMap, 
elementPositionMap, commentBoxMap) and triggers the
+ * TexeraGraph's change subjects while mirroring the change onto the JointJS 
graph.
+ *
+ * These tests build the real service (so the handler is wired to real 
collaborators: WorkflowGraph, joint.dia.Graph,
+ * JointGraphWrapper, JointUIService) and drive the shared model through the 
graph's mutation API, asserting the
+ * corresponding subjects fire with the right payloads and that JointJS is 
kept in sync.
+ */
+describe("SharedModelChangeHandler", () => {
+  let service: WorkflowActionService;
+  let texeraGraph: WorkflowGraph;
+  let jointGraph: joint.dia.Graph;
+  let jointGraphWrapper: JointGraphWrapper;
+
+  beforeEach(() => {
+    // silence the informational console.log calls emitted by the 
graph/handler on some mutations
+    vi.spyOn(console, "log").mockImplementation(() => {});
+
+    TestBed.configureTestingModule({
+      providers: [
+        WorkflowActionService,
+        WorkflowUtilService,
+        JointUIService,
+        UndoRedoService,
+        {
+          provide: OperatorMetadataService,
+          useClass: StubOperatorMetadataService,
+        },
+        ...commonTestProviders,
+      ],
+    });
+    service = TestBed.inject(WorkflowActionService);
+    texeraGraph = (service as any).texeraGraph;
+    jointGraph = service.getJointGraph();
+    jointGraphWrapper = service.getJointGraphWrapper();
+  });
+
+  afterEach(() => {
+    // Tear down the underlying shared model (awareness / websocket provider) 
to avoid leaks across tests.
+    // If this throws, the test should fail so we don't silently leak 
shared-editing resources.
+    texeraGraph.destroyYModel();
+    vi.restoreAllMocks();
+  });
+
+  /**
+   * The handler's operator-add observer reads the operator's position out of 
elementPositionMap, so a real add must
+   * write both maps inside a single transaction (this mirrors 
WorkflowActionService.addOperator).
+   */
+  function addOperatorWithPosition(operator: OperatorPredicate, point: Point = 
mockPoint): void {
+    texeraGraph.bundleActions(() => {
+      texeraGraph.addOperator(operator);
+      texeraGraph.sharedModel.elementPositionMap.set(operator.operatorID, 
point);
+    });
+  }
+
+  describe("operator add and delete", () => {
+    it("should emit operatorAddSubject and add a joint element when an 
operator is added", () => {
+      let added: OperatorPredicate | undefined;
+      const sub = texeraGraph.getOperatorAddStream().subscribe(op => (added = 
op));
+
+      addOperatorWithPosition(mockScanPredicate);
+
+      expect(added).toBeDefined();
+      expect(added!.operatorID).toEqual(mockScanPredicate.operatorID);
+      expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeTruthy();
+      // a single local add highlights the new operator without multi-select
+      
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);
+      sub.unsubscribe();
+    });
+
+    it("should throw when adding an operator that has no entry in the position 
map", () => {
+      // adding to operatorIDMap alone (no elementPositionMap entry) trips the 
handler's position guard
+      expect(() => texeraGraph.addOperator(mockScanPredicate)).toThrowError(
+        new RegExp("does not exist in position map")
+      );
+    });
+
+    it("should highlight all newly added operators in multi-select mode when 
several are added together", () => {
+      const added: string[] = [];
+      const sub = texeraGraph.getOperatorAddStream().subscribe(op => 
added.push(op.operatorID));
+
+      texeraGraph.bundleActions(() => {
+        texeraGraph.addOperator(mockScanPredicate);
+        
texeraGraph.sharedModel.elementPositionMap.set(mockScanPredicate.operatorID, 
mockPoint);
+        texeraGraph.addOperator(mockResultPredicate);
+        
texeraGraph.sharedModel.elementPositionMap.set(mockResultPredicate.operatorID, 
mockPoint);
+      });
+
+      expect(added).toEqual([mockScanPredicate.operatorID, 
mockResultPredicate.operatorID]);
+      expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeTruthy();
+      expect(jointGraph.getCell(mockResultPredicate.operatorID)).toBeTruthy();
+      const highlighted = jointGraphWrapper.getCurrentHighlightedOperatorIDs();
+      expect(highlighted.length).toEqual(2);
+      expect(highlighted).toContain(mockScanPredicate.operatorID);
+      expect(highlighted).toContain(mockResultPredicate.operatorID);
+      sub.unsubscribe();
+    });
+
+    it("should emit operatorDeleteSubject and remove the joint element 
(unhighlighting first) when an operator is deleted", () => {
+      addOperatorWithPosition(mockScanPredicate);
+      // the add above left the operator highlighted, so deletion must 
exercise the unhighlight branch
+      
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);
+
+      let deleted: { deletedOperatorID: string } | undefined;
+      const sub = texeraGraph.getOperatorDeleteStream().subscribe(v => 
(deleted = v));
+
+      texeraGraph.deleteOperator(mockScanPredicate.operatorID);
+
+      expect(deleted).toEqual({ deletedOperatorID: 
mockScanPredicate.operatorID });
+      expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeFalsy();
+      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]);
+      sub.unsubscribe();
+    });
+  });
+
+  describe("link add and delete", () => {
+    beforeEach(() => {
+      addOperatorWithPosition(mockScanPredicate);
+      addOperatorWithPosition(mockResultPredicate);
+    });
+
+    it("should emit linkAddSubject and add a joint link cell when a valid link 
is added", () => {
+      let addedLink: OperatorLink | undefined;
+      const sub = texeraGraph.getLinkAddStream().subscribe(l => (addedLink = 
l));
+
+      texeraGraph.addLink(mockScanResultLink);
+
+      expect(addedLink).toEqual(mockScanResultLink);
+      expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeTruthy();
+      sub.unsubscribe();
+    });
+
+    it("should emit linkDeleteSubject and remove the joint link cell when a 
link is deleted", () => {
+      texeraGraph.addLink(mockScanResultLink);
+      expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeTruthy();
+
+      let deletedLink: { deletedLink: OperatorLink } | undefined;
+      const sub = texeraGraph.getLinkDeleteStream().subscribe(v => 
(deletedLink = v));
+
+      texeraGraph.deleteLinkWithID(mockScanResultLink.linkID);
+
+      expect(deletedLink).toEqual({ deletedLink: mockScanResultLink });
+      expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeFalsy();
+      sub.unsubscribe();
+    });
+
+    it("should repair the shared model (and not emit linkAddSubject) when an 
invalid link is added directly", () => {
+      let addFired = false;
+      const sub = texeraGraph.getLinkAddStream().subscribe(() => (addFired = 
true));
+
+      // a link whose source operator does not exist fails 
validateAndRepairNewLink and is deleted from the model
+      const invalidLink: OperatorLink = {
+        linkID: "invalid-link",
+        source: { operatorID: "nonexist", portID: "output-0" },
+        target: { operatorID: mockResultPredicate.operatorID, portID: 
"input-0" },
+      };
+      texeraGraph.sharedModel.operatorLinkMap.set(invalidLink.linkID, 
invalidLink);
+
+      expect(addFired).toBe(false);
+      expect(texeraGraph.hasLinkWithID(invalidLink.linkID)).toBe(false);
+      expect(jointGraph.getCell(invalidLink.linkID)).toBeFalsy();
+      sub.unsubscribe();
+    });
+  });
+
+  describe("element position change", () => {
+    it("should reposition the joint element when the element position map is 
updated", () => {
+      addOperatorWithPosition(mockScanPredicate, { x: 10, y: 20 });
+
+      
texeraGraph.sharedModel.elementPositionMap.set(mockScanPredicate.operatorID, { 
x: 99, y: 88 });
+
+      const element = jointGraph.getCell(mockScanPredicate.operatorID) as 
joint.dia.Element;
+      expect(element.position()).toEqual({ x: 99, y: 88 });
+    });
+  });
+
+  describe("comment box add and delete", () => {
+    it("should emit commentBoxAddSubject and add a joint comment element when 
a comment box is added", () => {
+      let addedCommentBox: any;
+      const sub = texeraGraph.getCommentBoxAddStream().subscribe(cb => 
(addedCommentBox = cb));
+
+      texeraGraph.addCommentBox(mockCommentBox);
+
+      expect(addedCommentBox).toBeDefined();
+      
expect(addedCommentBox.commentBoxID).toEqual(mockCommentBox.commentBoxID);
+      expect(jointGraph.getCell(mockCommentBox.commentBoxID)).toBeTruthy();
+      sub.unsubscribe();
+    });
+
+    it("should remove the joint comment element when a comment box is 
deleted", () => {
+      texeraGraph.addCommentBox(mockCommentBox);
+      expect(jointGraph.getCell(mockCommentBox.commentBoxID)).toBeTruthy();
+
+      texeraGraph.deleteCommentBox(mockCommentBox.commentBoxID);
+
+      expect(jointGraph.getCell(mockCommentBox.commentBoxID)).toBeFalsy();
+    });
+  });
+
+  describe("deep operator changes", () => {
+    beforeEach(() => {
+      addOperatorWithPosition(mockScanPredicate);
+    });
+
+    it("should emit viewResultOperatorChangedSubject when the view-result flag 
is toggled on and off", () => {
+      let change: { newViewResultOps: readonly string[]; newUnviewResultOps: 
readonly string[] } | undefined;
+      const sub = 
texeraGraph.getViewResultOperatorsChangedStream().subscribe(v => (change = v));
+
+      texeraGraph.setViewOperatorResult(mockScanPredicate.operatorID);
+      expect(change).toEqual({ newViewResultOps: 
[mockScanPredicate.operatorID], newUnviewResultOps: [] });
+
+      texeraGraph.unsetViewOperatorResult(mockScanPredicate.operatorID);
+      expect(change).toEqual({ newViewResultOps: [], newUnviewResultOps: 
[mockScanPredicate.operatorID] });
+      sub.unsubscribe();
+    });
+
+    it("should emit reuseOperatorChangedSubject when the reuse-cache flag is 
toggled on and off", () => {
+      let change: { newReuseCacheOps: readonly string[]; newUnreuseCacheOps: 
readonly string[] } | undefined;
+      const sub = 
texeraGraph.getReuseCacheOperatorsChangedStream().subscribe(v => (change = v));
+
+      texeraGraph.markReuseResult(mockScanPredicate.operatorID);
+      expect(change).toEqual({ newReuseCacheOps: 
[mockScanPredicate.operatorID], newUnreuseCacheOps: [] });
+
+      texeraGraph.removeMarkReuseResult(mockScanPredicate.operatorID);
+      expect(change).toEqual({ newReuseCacheOps: [], newUnreuseCacheOps: 
[mockScanPredicate.operatorID] });
+      sub.unsubscribe();
+    });
+
+    it("should emit disabledOperatorChangedSubject when the operator is 
disabled and enabled", () => {
+      let change: { newDisabled: readonly string[]; newEnabled: readonly 
string[] } | undefined;
+      const sub = texeraGraph.getDisabledOperatorsChangedStream().subscribe(v 
=> (change = v));
+
+      texeraGraph.disableOperator(mockScanPredicate.operatorID);
+      expect(change).toEqual({ newDisabled: [mockScanPredicate.operatorID], 
newEnabled: [] });
+
+      texeraGraph.enableOperator(mockScanPredicate.operatorID);
+      expect(change).toEqual({ newDisabled: [], newEnabled: 
[mockScanPredicate.operatorID] });
+      sub.unsubscribe();
+    });
+
+    it("should emit operatorPropertyChangeSubject when the operator properties 
change", () => {
+      let change: { operator: OperatorPredicate } | undefined;
+      const sub = texeraGraph.getOperatorPropertyChangeStream().subscribe(v => 
(change = v));
+
+      texeraGraph.setOperatorProperty(mockScanPredicate.operatorID, { 
tableName: "some-table" });
+
+      expect(change).toBeDefined();
+      
expect(change!.operator.operatorID).toEqual(mockScanPredicate.operatorID);
+      expect(change!.operator.operatorProperties).toEqual({ tableName: 
"some-table" });
+      sub.unsubscribe();
+    });
+
+    it("should update local awareness when the currently-edited operator's 
property changes locally", () => {
+      // making the operator the locally-edited one exercises the 
awareness-update branch in onOperatorPropertyChanged
+      texeraGraph.sharedModel.awareness.setLocalState({ currentlyEditing: 
mockScanPredicate.operatorID } as any);
+      const awarenessSpy = vi.spyOn(texeraGraph, "updateSharedModelAwareness");
+
+      let changed = false;
+      const sub = texeraGraph.getOperatorPropertyChangeStream().subscribe(() 
=> (changed = true));
+
+      texeraGraph.setOperatorProperty(mockScanPredicate.operatorID, { 
tableName: "edited" });
+
+      expect(changed).toBe(true);
+      expect(awarenessSpy).toHaveBeenCalledWith("changed", 
mockScanPredicate.operatorID);
+      expect(awarenessSpy).toHaveBeenCalledWith("changed", undefined);
+      sub.unsubscribe();
+    });
+
+    it("should emit operatorPropertyChangeSubject when the whole 
operatorProperties type is replaced", () => {
+      // A wholesale replacement of the operatorProperties key (e.g. from a 
remote update / undo) is handled by the
+      // path-length-1 branch, distinct from the in-place nested update that 
setOperatorProperty performs.
+      let change: { operator: OperatorPredicate } | undefined;
+      const sub = texeraGraph.getOperatorPropertyChangeStream().subscribe(v => 
(change = v));
+
+      texeraGraph
+        .getSharedOperatorType(mockScanPredicate.operatorID)
+        .set("operatorProperties", createYTypeFromObject({ replaced: "yes" }) 
as any);
+
+      expect(change).toBeDefined();
+      
expect(change!.operator.operatorID).toEqual(mockScanPredicate.operatorID);
+      expect(change!.operator.operatorProperties).toEqual({ replaced: "yes" });
+      sub.unsubscribe();
+    });
+
+    it("should throw on an unrecognized nested operator change", () => {
+      // Mutating a nested field the handler does not know how to sync 
(operatorVersion text) hits the defensive guard.
+      expect(() =>
+        
(texeraGraph.getSharedOperatorType(mockScanPredicate.operatorID).get("operatorVersion")
 as any).insert(0, "v2-")
+      ).toThrowError(new RegExp("undefined operation on shared type"));
+    });
+
+    it("should emit operatorDisplayNameChangedSubject when the custom display 
name text changes", () => {
+      // rebuild the operator with a customDisplayName so the shared model 
holds a Y.Text we can mutate
+      texeraGraph.deleteOperator(mockScanPredicate.operatorID);
+      const namedOperator: OperatorPredicate = { ...mockScanPredicate, 
customDisplayName: "Original" };
+      addOperatorWithPosition(namedOperator);
+
+      let change: { operatorID: string; newDisplayName: string } | undefined;
+      const sub = 
texeraGraph.getOperatorDisplayNameChangedStream().subscribe(v => (change = v));
+
+      
(texeraGraph.getSharedOperatorType(namedOperator.operatorID).get("customDisplayName")
 as any).insert(0, "New-");
+
+      expect(change).toBeDefined();
+      expect(change!.operatorID).toEqual(namedOperator.operatorID);
+      expect(change!.newDisplayName).toEqual("New-Original");
+      sub.unsubscribe();
+    });
+  });
+
+  describe("port events", () => {
+    it("should emit portAddedOrDeletedSubject and add a joint port when an 
output port is added", () => {
+      addOperatorWithPosition(mockScanPredicate); // has one existing output 
port
+
+      let event: { newOperator: OperatorPredicate } | undefined;
+      const sub = texeraGraph.getPortAddedOrDeletedStream().subscribe(v => 
(event = v));
+
+      texeraGraph.addPort(mockScanPredicate.operatorID, { portID: "output-1" 
}, false);
+
+      expect(event).toBeDefined();
+      
expect(event!.newOperator.operatorID).toEqual(mockScanPredicate.operatorID);
+      expect(event!.newOperator.outputPorts.map(p => 
p.portID)).toEqual(["output-0", "output-1"]);
+      const element = jointGraph.getCell(mockScanPredicate.operatorID) as 
joint.dia.Element;
+      expect(element.getPorts().some(p => p.id === "output-1")).toBe(true);
+      sub.unsubscribe();
+    });
+
+    it("should emit portAddedOrDeletedSubject and add a joint port when an 
input port is added", () => {
+      addOperatorWithPosition(mockSentimentPredicate); // has one existing 
input port
+
+      let event: { newOperator: OperatorPredicate } | undefined;
+      const sub = texeraGraph.getPortAddedOrDeletedStream().subscribe(v => 
(event = v));
+
+      texeraGraph.addPort(mockSentimentPredicate.operatorID, { portID: 
"input-1" }, true);
+
+      expect(event).toBeDefined();
+      expect(event!.newOperator.inputPorts.map(p => 
p.portID)).toEqual(["input-0", "input-1"]);
+      const element = jointGraph.getCell(mockSentimentPredicate.operatorID) as 
joint.dia.Element;
+      expect(element.getPorts().some(p => p.id === "input-1")).toBe(true);
+      sub.unsubscribe();
+    });
+
+    it("should emit portAddedOrDeletedSubject and remove a joint port when 
input and output ports are removed", () => {
+      addOperatorWithPosition(mockSentimentPredicate); // input-0, output-0
+      texeraGraph.addPort(mockSentimentPredicate.operatorID, { portID: 
"input-1" }, true);
+
+      const events: OperatorPredicate[] = [];
+      const sub = texeraGraph.getPortAddedOrDeletedStream().subscribe(v => 
events.push(v.newOperator));
+
+      // removing when >1 port yields a retain+delete delta; removing the last 
port yields a delete-at-0 delta
+      texeraGraph.removePort(mockSentimentPredicate.operatorID, true); // 
remove input-1 (delta[1].delete)
+      texeraGraph.removePort(mockSentimentPredicate.operatorID, false); // 
remove output-0 (delta[0].delete)
+
+      expect(events.length).toEqual(2);
+      const element = jointGraph.getCell(mockSentimentPredicate.operatorID) as 
joint.dia.Element;
+      expect(element.getPorts().some(p => p.id === "input-1")).toBe(false);
+      expect(element.getPorts().some(p => p.id === "output-0")).toBe(false);
+      sub.unsubscribe();
+    });
+
+    it("should emit portDisplayNameChangedSubject when a port's display name 
text changes", () => {
+      const operatorWithNamedPort: OperatorPredicate = {
+        ...mockSentimentPredicate,
+        inputPorts: [{ portID: "input-0", displayName: "orig" }],
+      };
+      addOperatorWithPosition(operatorWithNamedPort);
+
+      let change: { operatorID: string; portID: string; newDisplayName: string 
} | undefined;
+      const sub = texeraGraph.getPortDisplayNameChangedSubject().subscribe(v 
=> (change = v));
+
+      const portType = texeraGraph.getSharedPortDescriptionType({
+        operatorID: operatorWithNamedPort.operatorID,
+        portID: "input-0",
+      });
+      (portType!.get("displayName") as any).insert(0, "new-");
+
+      expect(change).toBeDefined();
+      expect(change!.operatorID).toEqual(operatorWithNamedPort.operatorID);
+      expect(change!.portID).toEqual("input-0");
+      expect(change!.newDisplayName).toEqual("new-orig");
+      sub.unsubscribe();
+    });
+
+    it("should emit portPropertyChangedSubject when a port's 
partition/dependency properties change", () => {
+      addOperatorWithPosition(mockSentimentPredicate);
+
+      let change: any;
+      const sub = texeraGraph.getPortPropertyChangedStream().subscribe(v => 
(change = v));
+
+      texeraGraph.setPortProperty(
+        { operatorID: mockSentimentPredicate.operatorID, portID: "input-0" },
+        { partitionInfo: { type: "hash", hashAttributeNames: ["col"] }, 
dependencies: [{ id: 0, internal: false }] }
+      );
+
+      expect(change).toBeDefined();
+      expect(change.operatorPortID).toEqual({ operatorID: 
mockSentimentPredicate.operatorID, portID: "input-0" });
+      expect(change.newProperty.partitionInfo).toEqual({ type: "hash", 
hashAttributeNames: ["col"] });
+      expect(change.newProperty.dependencies).toEqual([{ id: 0, internal: 
false }]);
+      sub.unsubscribe();
+    });
+  });
+
+  describe("deep comment box changes", () => {
+    beforeEach(() => {
+      texeraGraph.addCommentBox(mockCommentBox);
+    });
+
+    it("should emit commentBoxAddCommentSubject when a comment is added", () 
=> {
+      let event: { addedComment: Comment; commentBox: any } | undefined;
+      const sub = texeraGraph.getCommentBoxAddCommentStream().subscribe(v => 
(event = v));
+
+      const comment: Comment = { content: "hello", creationTime: "t1", 
creatorName: "alice", creatorID: 1 };
+      texeraGraph.addCommentToCommentBox(comment, mockCommentBox.commentBoxID);
+
+      expect(event).toBeDefined();
+      expect(event!.addedComment.content).toEqual("hello");
+      
expect(event!.commentBox.commentBoxID).toEqual(mockCommentBox.commentBoxID);
+      sub.unsubscribe();
+    });
+
+    it("should emit commentBoxDeleteCommentSubject when a comment is deleted", 
() => {
+      const comment: Comment = { content: "hello", creationTime: "t1", 
creatorName: "alice", creatorID: 1 };
+      texeraGraph.addCommentToCommentBox(comment, mockCommentBox.commentBoxID);
+
+      let event: { commentBox: any } | undefined;
+      const sub = texeraGraph.getCommentBoxDeleteCommentStream().subscribe(v 
=> (event = v));
+
+      texeraGraph.deleteCommentFromCommentBox(1, "t1", 
mockCommentBox.commentBoxID);
+
+      expect(event).toBeDefined();
+      
expect(event!.commentBox.commentBoxID).toEqual(mockCommentBox.commentBoxID);
+      sub.unsubscribe();
+    });
+
+    it("should emit commentBoxEditCommentSubject when a comment is edited in 
place", () => {
+      const comment: Comment = { content: "hello", creationTime: "t1", 
creatorName: "alice", creatorID: 1 };
+      texeraGraph.addCommentToCommentBox(comment, mockCommentBox.commentBoxID);
+
+      let event: { commentBox: any } | undefined;
+      const sub = texeraGraph.getCommentBoxEditCommentStream().subscribe(v => 
(event = v));
+
+      texeraGraph.editCommentInCommentBox(1, "t1", 
mockCommentBox.commentBoxID, "edited");
+
+      expect(event).toBeDefined();
+      
expect(event!.commentBox.commentBoxID).toEqual(mockCommentBox.commentBoxID);
+      sub.unsubscribe();
+    });
+  });
+
+  describe("new Y doc reload", () => {
+    it("should re-register the observers on the freshly loaded shared model", 
() => {
+      // Load a new model; the handler subscribes to newYDocLoadedSubject and 
must re-attach every observer.
+      texeraGraph.loadNewYModel();
+
+      let added: OperatorPredicate | undefined;
+      const sub = texeraGraph.getOperatorAddStream().subscribe(op => (added = 
op));
+
+      addOperatorWithPosition(mockScanPredicate);
+
+      expect(added).toBeDefined();
+      expect(added!.operatorID).toEqual(mockScanPredicate.operatorID);
+      expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeTruthy();
+      sub.unsubscribe();
+    });
+  });
+
+  describe("async rendering path", () => {
+    beforeEach(() => {
+      (TestBed.inject(GuiConfigService) as any).setConfig({ 
asyncRenderingEnabled: true });
+    });
+
+    it("should batch-add operators onto the joint graph when async rendering 
is enabled", () => {
+      const added: string[] = [];
+      const sub = texeraGraph.getOperatorAddStream().subscribe(op => 
added.push(op.operatorID));
+
+      texeraGraph.bundleActions(() => {
+        texeraGraph.addOperator(mockScanPredicate);
+        
texeraGraph.sharedModel.elementPositionMap.set(mockScanPredicate.operatorID, 
mockPoint);
+        texeraGraph.addOperator(mockResultPredicate);
+        
texeraGraph.sharedModel.elementPositionMap.set(mockResultPredicate.operatorID, 
mockPoint);
+      });
+
+      expect(added).toEqual([mockScanPredicate.operatorID, 
mockResultPredicate.operatorID]);
+      expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeTruthy();
+      expect(jointGraph.getCell(mockResultPredicate.operatorID)).toBeTruthy();
+      sub.unsubscribe();
+    });
+
+    it("should batch-add links onto the joint graph when async rendering is 
enabled", () => {
+      addOperatorWithPosition(mockScanPredicate);
+      addOperatorWithPosition(mockResultPredicate);
+
+      let addedLink: OperatorLink | undefined;
+      const sub = texeraGraph.getLinkAddStream().subscribe(l => (addedLink = 
l));
+
+      texeraGraph.addLink(mockScanResultLink);
+
+      expect(addedLink).toEqual(mockScanResultLink);
+      expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeTruthy();
+      sub.unsubscribe();
+    });
+  });
+});

Reply via email to