You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/23 03:12:22 UTC
(texera) branch gh-readonly-queue/main/pr-6772-b405528cc4000e69ec1473b08f288b3a1c46ae88 created (now 355f121f28)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-6772-b405528cc4000e69ec1473b08f288b3a1c46ae88
in repository https://gitbox.apache.org/repos/asf/texera.git
at 355f121f28 test(frontend): add unit tests for SharedModelChangeHandler (#6772)
This branch includes the following new commits:
new 355f121f28 test(frontend): add unit tests for SharedModelChangeHandler (#6772)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(frontend): add unit tests for SharedModelChangeHandler (#6772)
Posted by gi...@apache.org.
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 <xi...@uci.edu>
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 <xi...@uci.edu>
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
---
.../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();
+ });
+ });
+});