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/05/25 22:46:33 UTC
(texera) 01/02: test(frontend): cover joint-ui.service surface and remove dead code (#5196)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 2153c17879ca4504da09d4d7dc9648d17f364aa0
Author: Yicong Huang <17...@users.noreply.github.com>
AuthorDate: Mon May 25 15:35:02 2026 -0700
test(frontend): cover joint-ui.service surface and remove dead code (#5196)
### What changes were proposed in this PR?
`joint-ui.service.spec.ts` had ~60 commented test lines plus an
`it.todo` placeholder; the commented tests called methods that have
since been removed from `JointUIService`, so they're unrevivable. Drop
them and add real tests for the public surface the spec was leaving
unverified — static helpers, the joint-paper-bound
state/color/visibility setters, the port-iteration loops,
`getJointOperatorElement`, and `changeOperatorStatistics`.
Writing those tests made four pieces of stale `joint-ui.service.ts`
itself obvious: a static tooltip-style helper with zero callers (its
paired element was removed years ago), two dead `originalName` chains
inside `changeOperatorStatistics` (computed, never read), and a `if
(!element) return` guard at the very end of `unfoldOperatorDetails`
(no-op — nothing follows it). Removed.
### Any related issues, documentation, discussions?
Closes #5195. Same shape as #5194.
### How was this PR tested?
`yarn ng test --watch=false --include=…` runs 56 tests, all green. `yarn
lint` and `yarn format:ci` both clean.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 4.7
---
.../service/joint-ui/joint-ui.service.spec.ts | 850 +++++++++++++--------
.../workspace/service/joint-ui/joint-ui.service.ts | 64 +-
2 files changed, 535 insertions(+), 379 deletions(-)
diff --git a/frontend/src/app/workspace/service/joint-ui/joint-ui.service.spec.ts b/frontend/src/app/workspace/service/joint-ui/joint-ui.service.spec.ts
index ccefd937de..87b5acd924 100644
--- a/frontend/src/app/workspace/service/joint-ui/joint-ui.service.spec.ts
+++ b/frontend/src/app/workspace/service/joint-ui/joint-ui.service.spec.ts
@@ -17,327 +17,26 @@
* under the License.
*/
-// import { mockResultPredicate, mockPoint } from './../workflow-graph/model/mock-workflow-data';
-// import { TestBed, inject } from '@angular/core/testing';
-// import * as joint from 'jointjs';
-
-// import { JointUIService, deleteButtonPath, sourceOperatorHandle, targetOperatorHandle } from './joint-ui.service';
-// import { OperatorMetadataService } from '../operator-metadata/operator-metadata.service';
-// import { StubOperatorMetadataService } from '../operator-metadata/stub-operator-metadata.service';
-// import { mockScanPredicate, mockSentimentPredicate } from '../workflow-graph/model/mock-workflow-data';
-// import { mockScanStatistic1, mockScanStatistic2 } from '../workflow-status/mock-workflow-status';
-
-// describe('JointUIService', () => {
-// let service: JointUIService;
-
-// beforeEach(() => {
-// TestBed.configureTestingModule({
-// providers: [
-// JointUIService,
-// { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },
-// ],
-// });
-// service = TestBed.get(JointUIService);
-// });
-
-// it('should be created', inject([JointUIService], (injectedService: JointUIService) => {
-// expect(injectedService).toBeTruthy();
-// }));
-
-// /**
-// * Check if the getJointOperatorElement() can successfully creates a JointJS Element
-// */
-// it('should create an JointJS Element successfully when the function is called', () => {
-// const result = service.getJointOperatorElement(
-// mockScanPredicate, mockPoint);
-// expect(result).toBeTruthy();
-// });
-
-// /**
-// * Check if the error in getJointOperatorElement() is correctly thrown
-// */
-// it('should throw an error with an non existing operator', () => {
-// expect(() => {
-// service.getJointOperatorElement(
-// {
-// operatorID: 'nonexistOperator',
-// operatorType: 'nonexistOperatorType',
-// operatorProperties: {},
-// inputPorts: [],
-// outputPorts: [],
-// showAdvanced: true
-// },
-// mockPoint
-// );
-// }).toThrowError(new RegExp(`doesn't exist`));
-// });
-
-// /**
-// * Check if the getJointTooltipElement() can successfully creates a JointJS Element
-// */
-// it('should create an JointJS Element successfully when the function is called', () => {
-// const result = service.getJointOperatorStatusTooltipElement(
-// mockScanPredicate, mockPoint);
-// expect(result).toBeTruthy();
-// });
-
-// /**
-// * Check if the error in getJointTooltipElement() is correctly thrown
-// */
-// it('should throw an error with an non existing operator', () => {
-// expect(() => {
-// service.getJointOperatorStatusTooltipElement(
-// {
-// operatorID: 'nonexistOperator',
-// operatorType: 'nonexistOperatorType',
-// operatorProperties: {},
-// inputPorts: [],
-// outputPorts: [],
-// showAdvanced: true
-// },
-// mockPoint
-// );
-// }).toThrowError(new RegExp(`doesn't exist`));
-// });
-
-// /**
-// * Check if showTooltip/hideTooltip works properly
-// */
-// it('should reveal/hide tooltip and its content when showToolTip/hideTooltip is called', () => {
-// // creating a JointJS graph with one operator and its tooltip
-// const jointGraph = new joint.dia.Graph();
-// const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph};
-// const paper = new joint.dia.Paper(jointPaperOptions);
-
-// jointGraph.addCell([
-// service.getJointOperatorElement(
-// mockScanPredicate,
-// mockPoint
-// ),
-// service.getJointOperatorStatusTooltipElement(
-// mockScanPredicate,
-// mockPoint
-// )
-// ]);
-// // tooltip should not be shown when operator is just created
-// // disply attr should be none
-// const tooltipId = JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID);
-// const graph_tooltip1 = jointGraph.getCell(tooltipId);
-// expect(graph_tooltip1.attr('polygon')['display']).toEqual('none');
-// expect(graph_tooltip1.attr('#operatorCount')['display']).toEqual('none');
-// expect(graph_tooltip1.attr('#operatorSpeed')['display']).toEqual('none');
-// // showTooltip removes display == none attr to show tooltip
-// service.showOperatorStatusToolTip(paper, tooltipId);
-// expect(graph_tooltip1.attr('polygon')['display']).toBeUndefined();
-// expect(graph_tooltip1.attr('#operatorCount')['display']).toBeUndefined();
-// expect(graph_tooltip1.attr('#operatorSpeed')['display']).toBeUndefined();
-// // hideTooltip adds display == none attr to hide tooltip
-// service.hideOperatorStatusToolTip(paper, tooltipId);
-// expect(graph_tooltip1.attr('polygon')['display']).toEqual('none');
-// expect(graph_tooltip1.attr('#operatorCount')['display']).toEqual('none');
-// expect(graph_tooltip1.attr('#operatorSpeed')['display']).toEqual('none');
-// });
-
-// /**
-// * check if tooltip content can be updated properly
-// */
-// it('should update the content in the tooltip when changeOperatorTooltipInfo is called', () => {
-// // creating a JointJS graph with one operator and its tooltip
-// const jointGraph = new joint.dia.Graph();
-// const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph};
-// const paper = new joint.dia.Paper(jointPaperOptions);
-
-// jointGraph.addCell([
-// service.getJointOperatorElement(
-// mockScanPredicate,
-// mockPoint
-// ),
-// service.getJointOperatorStatusTooltipElement(
-// mockScanPredicate,
-// mockPoint
-// )
-// ]);
-// const tooltipId = JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID);
-// const graph_tooltip = jointGraph.getCell(tooltipId);
-// // tooltip should not contain any content when created
-// expect(graph_tooltip.attr('#operatorCount')['text']).toBeUndefined();
-// expect(graph_tooltip.attr('#operatorSpeed')['text']).toBeUndefined();
-// // updating it with mock statistics
-// service.changeOperatorStatusTooltipInfo(paper, tooltipId, mockScanStatistic1);
-// expect(graph_tooltip.attr('#operatorCount')['text']).toEqual('Output:' + mockScanStatistic1.outputCount + ' tuples');
-// expect(graph_tooltip.attr('#operatorSpeed')['text']).toEqual('Speed:' + mockScanStatistic1.speed + ' tuples/s');
-// // updating it with another mock statistics
-// service.changeOperatorStatusTooltipInfo(paper, tooltipId, mockScanStatistic2);
-// expect(graph_tooltip.attr('#operatorCount')['text']).toEqual('Output:' + mockScanStatistic2.outputCount + ' tuples');
-// expect(graph_tooltip.attr('#operatorSpeed')['text']).toEqual('Speed:' + mockScanStatistic2.speed + ' tuples/s');
-// });
-
-// it('should change the operator state name and color when changeOperatorStates is called', () => {
-// // creating a JointJS graph with one operator and its tooltip
-// const jointGraph = new joint.dia.Graph();
-// const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph};
-// const paper = new joint.dia.Paper(jointPaperOptions);
-
-// jointGraph.addCell(
-// service.getJointOperatorElement(
-// mockScanPredicate,
-// mockPoint
-// ));
-
-// // operator state name and color should be changed correctly
-// const graph_operator = jointGraph.getCell(mockScanPredicate.operatorID);
-// expect(graph_operator.attr('#operatorStates')['text']).toEqual('Ready');
-// expect(graph_operator.attr('#operatorStates')['fill']).toEqual('green');
-// service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Initializing);
-// expect(graph_operator.attr('#operatorStates')['text']).toEqual('Initializing');
-// expect(graph_operator.attr('#operatorStates')['fill']).toEqual('orange');
-// service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Running);
-// expect(graph_operator.attr('#operatorStates')['text']).toEqual('Running');
-// expect(graph_operator.attr('#operatorStates')['fill']).toEqual('orange');
-// service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Pausing);
-// expect(graph_operator.attr('#operatorStates')['text']).toEqual('Pausing');
-// expect(graph_operator.attr('#operatorStates')['fill']).toEqual('red');
-// service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Paused);
-// expect(graph_operator.attr('#operatorStates')['text']).toEqual('Paused');
-// expect(graph_operator.attr('#operatorStates')['fill']).toEqual('red');
-// service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Completed);
-// expect(graph_operator.attr('#operatorStates')['text']).toEqual('Completed');
-// expect(graph_operator.attr('#operatorStates')['fill']).toEqual('green');
-// });
-
-// /**
-// * Check if the number of inPorts and outPorts created by getJointOperatorElement()
-// * matches the port number specified by the operator metadata
-// */
-// it('should create correct number of inPorts and outPorts based on operator metadata', () => {
-// const element1 = service.getJointOperatorElement(mockScanPredicate, mockPoint);
-// const element2 = service.getJointOperatorElement(mockSentimentPredicate, mockPoint);
-// const element3 = service.getJointOperatorElement(mockResultPredicate, mockPoint);
-
-// const inPortCount1 = element1.getPorts().filter(port => port.group === 'in').length;
-// const outPortCount1 = element1.getPorts().filter(port => port.group === 'out').length;
-// const inPortCount2 = element2.getPorts().filter(port => port.group === 'in').length;
-// const outPortCount2 = element2.getPorts().filter(port => port.group === 'out').length;
-// const inPortCount3 = element3.getPorts().filter(port => port.group === 'in').length;
-// const outPortCount3 = element3.getPorts().filter(port => port.group === 'out').length;
-
-// expect(inPortCount1).toEqual(0);
-// expect(outPortCount1).toEqual(1);
-// expect(inPortCount2).toEqual(1);
-// expect(outPortCount2).toEqual(1);
-// expect(inPortCount3).toEqual(1);
-// expect(outPortCount3).toEqual(0);
-
-// });
-
-// /**
-// * Check if the custom attributes / svgs are correctly used by the JointJS graph
-// */
-// it('should apply the custom SVG styling to the JointJS element', () => {
-
-// const graph = new joint.dia.Graph();
-// // operator and its tooltip element should be added together
-// graph.addCell([
-// service.getJointOperatorElement(
-// mockScanPredicate,
-// mockPoint
-// ),
-// service.getJointOperatorStatusTooltipElement(
-// mockScanPredicate,
-// mockPoint
-// )
-// ]);
-// graph.addCell([
-// service.getJointOperatorElement(
-// mockResultPredicate,
-// { x: 500, y: 100 }
-// ),
-// service.getJointOperatorStatusTooltipElement(
-// mockResultPredicate,
-// { x: 500, y: 100 }
-// )
-// ]);
-
-// const link = JointUIService.getJointLinkCell({
-// linkID: 'link-1',
-// source: { operatorID: 'operator1', portID: 'out0' },
-// target: { operatorID: 'operator2', portID: 'in0' },
-// });
-
-// graph.addCell(link);
-
-// const graph_operator1 = graph.getCell(mockScanPredicate.operatorID);
-// const graph_operator2 = graph.getCell(mockResultPredicate.operatorID);
-// const graph_link = graph.getLinks()[0];
-// const graph_tooltip1 = graph.getCell(JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID));
-
-// // testing getCustomTooltipStyleAttrs()
-// // style: {'pointer-events': 'none'} makes tooltip unselectable thus not draggable
-// expect(graph_tooltip1.attr('polygon')).toEqual({
-// fill: '#FFFFFF', 'follow-scale': true, stroke: 'purple', 'stroke-width': '2',
-// rx: '5px', ry: '5px', refPoints: '0,30 150,30 150,120 85,120 75,150 65,120 0,120',
-// display: 'none', style: {'pointer-events': 'none'}
-// });
-// expect(graph_tooltip1.attr('#operatorCount')).toEqual({
-// fill: '#595959', 'font-size': '12px', ref: 'polygon',
-// 'y-alignment': 'middle',
-// 'x-alignment': 'left',
-// 'ref-x': .05, 'ref-y': .2,
-// display: 'none', style: {'pointer-events': 'none'}
-// });
-// expect(graph_tooltip1.attr('#operatorSpeed')).toEqual({
-// fill: '#595959',
-// ref: 'polygon',
-// 'x-alignment': 'left',
-// 'font-size': '12px',
-// 'ref-x': .05, 'ref-y': .5,
-// display: 'none', style: {'pointer-events': 'none'}
-// });
-
-// // testing getCustomOperatorStyleAttrs()
-// expect(graph_operator1.attr('#operatorStates')).toEqual({
-// text: 'Ready' , fill: 'green', 'font-size': '14px', 'visible' : false,
-// 'ref-x': 0.5, 'ref-y': -10, ref: 'rect', 'y-alignment': 'middle', 'x-alignment': 'middle'
-// });
-// expect(graph_operator1.attr('rect')).toEqual(
-// { fill: '#FFFFFF', 'follow-scale': true, stroke: 'red', 'stroke-width': '2',
-// rx: '5px', ry: '5px' }
-// );
-// expect(graph_operator2.attr('rect')).toEqual(
-// { fill: '#FFFFFF', 'follow-scale': true, stroke: 'red', 'stroke-width': '2',
-// rx: '5px', ry: '5px' }
-// );
-// expect(graph_operator1.attr('.delete-button')).toEqual(
-// {
-// x: 60, y: -20, cursor: 'pointer',
-// fill: '#D8656A', event: 'element:delete'
-// }
-// );
-// expect(graph_operator2.attr('.delete-button')).toEqual(
-// {
-// x: 60, y: -20, cursor: 'pointer',
-// fill: '#D8656A', event: 'element:delete'
-// }
-// );
-
-// // testing getDefaultLinkElement()
-// expect(graph_link.attr('.marker-source/d')).toEqual(sourceOperatorHandle);
-// expect(graph_link.attr('.marker-target/d')).toEqual(targetOperatorHandle);
-// expect(graph_link.attr('.tool-remove path/d')).toEqual(deleteButtonPath);
-// });
-// });
-
import { of } from "rxjs";
import * as joint from "jointjs";
-import { JointUIService, operatorNameClass } from "./joint-ui.service";
-import { OperatorPredicate } from "../../types/workflow-common.interface";
+import { JointUIService, operatorNameClass, operatorStateClass, operatorPortMetricsClass } from "./joint-ui.service";
+import { CommentBox, OperatorPredicate } from "../../types/workflow-common.interface";
+import { OperatorState } from "../../types/execute-workflow.interface";
+import { Coeditor } from "../../../common/type/user";
+
+// Minimal mock of OperatorMetadataService — the constructor subscribes to
+// getOperatorMetadata() but the schemas list isn't needed for the methods
+// covered here. Tests that exercise `getJointOperatorElement` build their
+// own metadata stub with real schemas inline.
+const emptyMetadataStub = {
+ getOperatorMetadata: () =>
+ of({
+ operators: [],
+ groups: [],
+ }),
+};
describe("JointUIService", () => {
- // Pre-existing spec body is commented out. Placeholder keeps Vitest's
- // discovery happy; rewriting the real tests against the new test
- // runner is tracked in #4861.
- it.todo("add unit tests for JointUIService");
-
describe("truncateOperatorDisplayName", () => {
// Deterministic measurer: 10px per character. With the 200-px budget,
// 20 chars fit exactly; longer strings get truncated to a prefix plus "…".
@@ -511,4 +210,521 @@ describe("JointUIService", () => {
}
});
});
+
+ // ---------------------------------------------------------------------------
+ // Static helpers — pure functions, easiest to test directly.
+ // ---------------------------------------------------------------------------
+
+ describe("getOperatorFillColor (static)", () => {
+ it("returns the disabled fill for an isDisabled=true operator", () => {
+ expect(JointUIService.getOperatorFillColor({ isDisabled: true } as OperatorPredicate)).toBe("#E0E0E0");
+ });
+ it("returns the default white fill for an enabled operator", () => {
+ expect(JointUIService.getOperatorFillColor({} as OperatorPredicate)).toBe("#FFFFFF");
+ expect(JointUIService.getOperatorFillColor({ isDisabled: false } as OperatorPredicate)).toBe("#FFFFFF");
+ });
+ });
+
+ describe("getOperatorCacheDisplayText (static)", () => {
+ it("returns empty string when cacheStatus is undefined", () => {
+ expect(JointUIService.getOperatorCacheDisplayText({ markedForReuse: true } as OperatorPredicate)).toBe("");
+ });
+ it("returns empty string when the operator is not marked for reuse", () => {
+ expect(
+ JointUIService.getOperatorCacheDisplayText({ markedForReuse: false } as OperatorPredicate, "cache valid")
+ ).toBe("");
+ });
+ it("returns the cache status text when both are set", () => {
+ expect(
+ JointUIService.getOperatorCacheDisplayText({ markedForReuse: true } as OperatorPredicate, "cache valid")
+ ).toBe("cache valid");
+ });
+ });
+
+ describe("getOperatorCacheIcon (static)", () => {
+ it("returns empty when the operator is not marked for reuse", () => {
+ expect(JointUIService.getOperatorCacheIcon({ markedForReuse: false } as OperatorPredicate, "cache valid")).toBe(
+ ""
+ );
+ });
+ it("returns the valid-cache icon when cacheStatus is 'cache valid'", () => {
+ expect(JointUIService.getOperatorCacheIcon({ markedForReuse: true } as OperatorPredicate, "cache valid")).toBe(
+ "assets/svg/operator-reuse-cache-valid.svg"
+ );
+ });
+ it("returns the invalid-cache icon for any other status (including undefined)", () => {
+ expect(JointUIService.getOperatorCacheIcon({ markedForReuse: true } as OperatorPredicate)).toBe(
+ "assets/svg/operator-reuse-cache-invalid.svg"
+ );
+ expect(JointUIService.getOperatorCacheIcon({ markedForReuse: true } as OperatorPredicate, "cache invalid")).toBe(
+ "assets/svg/operator-reuse-cache-invalid.svg"
+ );
+ });
+ });
+
+ describe("getOperatorViewResultIcon (static)", () => {
+ it("returns the view-result asset when viewResult=true", () => {
+ expect(JointUIService.getOperatorViewResultIcon({ viewResult: true } as OperatorPredicate)).toBe(
+ "assets/svg/operator-view-result.svg"
+ );
+ });
+ it("returns empty otherwise", () => {
+ expect(JointUIService.getOperatorViewResultIcon({} as OperatorPredicate)).toBe("");
+ expect(JointUIService.getOperatorViewResultIcon({ viewResult: false } as OperatorPredicate)).toBe("");
+ });
+ });
+
+ describe("getJointLinkCell (static)", () => {
+ it("builds a joint link cell carrying source/target/id from the OperatorLink", () => {
+ const link = JointUIService.getJointLinkCell({
+ linkID: "link-1",
+ source: { operatorID: "op-A", portID: "out-0" },
+ target: { operatorID: "op-B", portID: "in-0" },
+ });
+ expect(link.id).toBe("link-1");
+ expect(link.get("source")).toEqual({ id: "op-A", port: "out-0" });
+ expect(link.get("target")).toEqual({ id: "op-B", port: "in-0" });
+ // z=0 keeps links rendered under operator elements (z=1 in
+ // getJointOperatorElement).
+ expect(link.get("z")).toBe(0);
+ });
+ });
+
+ describe("getJointUserPointerName (static)", () => {
+ it("prefixes the coeditor clientId with 'pointer_'", () => {
+ expect(JointUIService.getJointUserPointerName({ clientId: "abc123" } as Coeditor)).toBe("pointer_abc123");
+ });
+ });
+
+ describe("getJointUserPointerCell (static)", () => {
+ it("builds a circle cell whose id matches getJointUserPointerName", () => {
+ const coeditor = { clientId: "42", name: "Ada", color: "#ff0000" } as Coeditor;
+ const cell = JointUIService.getJointUserPointerCell(coeditor, { x: 10, y: 20 }, "#abcdef");
+ expect(cell.id).toBe(JointUIService.getJointUserPointerName(coeditor));
+ // attr('body/fill') reflects the explicit color argument.
+ expect(cell.attr("body/fill")).toBe("#abcdef");
+ expect(cell.attr("body/stroke")).toBe("#abcdef");
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // Instance methods that operate on a joint Paper. Each test stubs the paper's
+ // getModelById to return a model with an `attr` spy; assertions look at what
+ // the SUT wrote on that model.
+ // ---------------------------------------------------------------------------
+
+ function makePaperWithModel() {
+ const attrSpy = vi.fn();
+ const portPropSpy = vi.fn();
+ const getPortsSpy = vi.fn(() => [] as { id?: string; group?: string }[]);
+ const model = { attr: attrSpy, getPorts: getPortsSpy, portProp: portPropSpy };
+ const paper = { getModelById: vi.fn(() => model) } as unknown as joint.dia.Paper;
+ return { paper, attrSpy, portPropSpy, getPortsSpy, model };
+ }
+
+ describe("changeOperatorColor", () => {
+ it("paints the body stroke neutral for a valid operator", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorColor(paper, "op-1", true);
+ expect(attrSpy).toHaveBeenCalledWith("rect.body/stroke", "#CFCFCF");
+ });
+ it("paints the body stroke red for an invalid operator", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorColor(paper, "op-1", false);
+ expect(attrSpy).toHaveBeenCalledWith("rect.body/stroke", "red");
+ });
+ });
+
+ describe("changeOperatorState", () => {
+ // For each state, the SUT writes a fill color to .${operatorStateClass};
+ // we only assert on the color since the rest of the attr payload (port
+ // labels, worker count) is exercised through the existing port mocks.
+ const cases: Array<[OperatorState, string]> = [
+ [OperatorState.Ready, "#a6bd37"],
+ [OperatorState.Completed, "green"],
+ [OperatorState.Paused, "magenta"],
+ [OperatorState.Pausing, "magenta"],
+ [OperatorState.Running, "orange"],
+ [OperatorState.Uninitialized, "gray"],
+ ];
+ cases.forEach(([state, color]) => {
+ it(`writes fill='${color}' for state=${state}`, () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorState(paper, "op-1", state);
+ // The attr payload is an object keyed by selectors; pluck the state class entry.
+ const [payload] = attrSpy.mock.calls[0];
+ expect(payload[`.${operatorStateClass}`]).toEqual({ text: state.toString(), fill: color });
+ expect(payload["rect.body"]).toEqual({ stroke: color });
+ });
+ });
+ });
+
+ describe("foldOperatorDetails / unfoldOperatorDetails", () => {
+ it("hides operator state + metric texts and the action buttons when folded", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.foldOperatorDetails(paper, "op-1");
+ const [payload] = attrSpy.mock.calls[0];
+ expect(payload[`.${operatorStateClass}`].visibility).toBe("hidden");
+ expect(payload[`.${operatorPortMetricsClass}`].visibility).toBe("hidden");
+ expect(payload[".delete-button"].visibility).toBe("hidden");
+ expect(payload[".chat-button"].visibility).toBe("hidden");
+ });
+ it("reveals the same surface when unfolded", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.unfoldOperatorDetails(paper, "op-1");
+ const [payload] = attrSpy.mock.calls[0];
+ expect(payload[`.${operatorStateClass}`].visibility).toBe("visible");
+ expect(payload[`.${operatorPortMetricsClass}`].visibility).toBe("visible");
+ expect(payload[".delete-button"].visibility).toBe("visible");
+ expect(payload[".chat-button"].visibility).toBe("visible");
+ });
+ });
+
+ describe("showAgentActionLabel / hideAgentActionLabel", () => {
+ it("writes the action label with the agent name prefix when the model exists", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.showAgentActionLabel(paper, "op-1", "modified", "Aria");
+ const [payload] = attrSpy.mock.calls[0];
+ const entry = Object.values(payload)[0] as { text: string; visibility: string };
+ expect(entry.text).toBe("Aria: modified");
+ expect(entry.visibility).toBe("visible");
+ });
+ it("uses the default 'Agent' name when none is supplied", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.showAgentActionLabel(paper, "op-1", "viewed");
+ const [payload] = attrSpy.mock.calls[0];
+ const entry = Object.values(payload)[0] as { text: string };
+ expect(entry.text).toBe("Agent: viewed");
+ });
+ it("no-ops when the model is missing", () => {
+ const paper = { getModelById: vi.fn(() => null) } as unknown as joint.dia.Paper;
+ const service = new JointUIService(emptyMetadataStub as never);
+ expect(() => service.showAgentActionLabel(paper, "missing-op", "added")).not.toThrow();
+ });
+ it("clears the label text and hides it on hideAgentActionLabel", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.hideAgentActionLabel(paper, "op-1");
+ const [payload] = attrSpy.mock.calls[0];
+ const entry = Object.values(payload)[0] as { text: string; visibility: string };
+ expect(entry.text).toBe("");
+ expect(entry.visibility).toBe("hidden");
+ });
+ it("hideAgentActionLabel is a no-op when the model is missing", () => {
+ const paper = { getModelById: vi.fn(() => null) } as unknown as joint.dia.Paper;
+ const service = new JointUIService(emptyMetadataStub as never);
+ expect(() => service.hideAgentActionLabel(paper, "missing-op")).not.toThrow();
+ });
+ });
+
+ describe("getCommentElement", () => {
+ it("builds a comment element with the supplied commentBoxID and position", () => {
+ const service = new JointUIService(emptyMetadataStub as never);
+ const cell = service.getCommentElement({
+ commentBoxID: "cb-1",
+ commentBoxPosition: { x: 42, y: 99 },
+ comments: [],
+ } as unknown as CommentBox);
+ expect(cell.id).toBe("cb-1");
+ });
+ it("falls back to (0,0) when commentBoxPosition is missing", () => {
+ const service = new JointUIService(emptyMetadataStub as never);
+ // Should not throw — the implementation guards both the basic
+ // shape position and the joint element construction.
+ const cell = service.getCommentElement({
+ commentBoxID: "cb-no-pos",
+ comments: [],
+ } as unknown as CommentBox);
+ expect(cell.id).toBe("cb-no-pos");
+ });
+ });
+
+ describe("getJointOperatorElement", () => {
+ function buildMetadataWithSchemas(schemas: object[]) {
+ return {
+ getOperatorMetadata: () =>
+ of({
+ operators: schemas,
+ groups: [],
+ }),
+ };
+ }
+
+ const minimalSchema = (operatorType: string, friendlyName = "Friendly") => ({
+ operatorType,
+ operatorVersion: "v1",
+ jsonSchema: {},
+ additionalMetadata: { userFriendlyName: friendlyName },
+ });
+
+ it("throws when the operator type isn't in the loaded schema list", () => {
+ const service = new JointUIService(emptyMetadataStub as never);
+ const operator = {
+ operatorID: "op-x",
+ operatorType: "DefinitelyNotARealType",
+ operatorProperties: {},
+ inputPorts: [],
+ outputPorts: [],
+ showAdvanced: false,
+ } as unknown as OperatorPredicate;
+ expect(() => service.getJointOperatorElement(operator, { x: 0, y: 0 })).toThrow(
+ /operator type DefinitelyNotARealType doesn't exist/
+ );
+ });
+
+ it("returns an element carrying the predicate's operatorID and z-index 1", () => {
+ const schema = minimalSchema("TestOp", "Test Operator");
+ const service = new JointUIService(buildMetadataWithSchemas([schema]) as never);
+ const predicate = {
+ operatorID: "my-op",
+ operatorType: "TestOp",
+ operatorVersion: "v1",
+ operatorProperties: {},
+ inputPorts: [{ portID: "in-0" }],
+ outputPorts: [{ portID: "out-0" }],
+ showAdvanced: false,
+ isDisabled: false,
+ } as OperatorPredicate;
+ const element = service.getJointOperatorElement(predicate, { x: 100, y: 50 });
+ expect(element.id).toBe("my-op");
+ expect(element.get("z")).toBe(1);
+ // Both ports flow through addPort.
+ const ports = element.getPorts();
+ expect(ports.map(p => p.id).sort()).toEqual(["in-0", "out-0"]);
+ });
+
+ it("emits add/remove port buttons in the markup when dynamicInputPorts and dynamicOutputPorts are true", () => {
+ const schema = minimalSchema("DynamicOp");
+ const service = new JointUIService(buildMetadataWithSchemas([schema]) as never);
+ const predicate = {
+ operatorID: "dyn",
+ operatorType: "DynamicOp",
+ operatorVersion: "v1",
+ operatorProperties: {},
+ inputPorts: [],
+ outputPorts: [],
+ showAdvanced: false,
+ isDisabled: false,
+ dynamicInputPorts: true,
+ dynamicOutputPorts: true,
+ } as unknown as OperatorPredicate;
+ const element = service.getJointOperatorElement(predicate, { x: 0, y: 0 });
+ // The markup is stamped onto the element's attributes; both port-button
+ // classes only appear when their dynamic-ports flag is true.
+ const markup = (element as unknown as { attributes: { markup: string } }).attributes.markup;
+ expect(markup).toContain("add-input-port-button");
+ expect(markup).toContain("add-output-port-button");
+ expect(markup).toContain("remove-input-port-button");
+ expect(markup).toContain("remove-output-port-button");
+ });
+
+ it("renders customDisplayName in the operator name attr when supplied", () => {
+ const schema = minimalSchema("Named", "Friendly");
+ const service = new JointUIService(buildMetadataWithSchemas([schema]) as never);
+ const predicate = {
+ operatorID: "named",
+ operatorType: "Named",
+ operatorVersion: "v1",
+ operatorProperties: {},
+ inputPorts: [],
+ outputPorts: [],
+ showAdvanced: false,
+ isDisabled: false,
+ customDisplayName: "Custom-Display",
+ } as unknown as OperatorPredicate;
+ const element = service.getJointOperatorElement(predicate, { x: 0, y: 0 });
+ // The display name lands in `.texera-operator-name/text` via
+ // getCustomOperatorStyleAttrs. truncateOperatorDisplayName is the no-op
+ // identity for short names so the value comes through verbatim.
+ expect(element.attr(`.${operatorNameClass}/text`)).toBe("Custom-Display");
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // Port-iteration paths — the original tests stubbed getPorts() to return [],
+ // which skipped the forEach branches inside changeOperatorState and the
+ // entire body of changeOperatorStatistics. The tests below cover those.
+ // ---------------------------------------------------------------------------
+
+ describe("changeOperatorState — port label re-coloring", () => {
+ it("re-paints every input and output port label to the state's fill color", () => {
+ const attrSpy = vi.fn();
+ const portPropSpy = vi.fn();
+ // changeOperatorState iterates getPorts() splitting by group; both
+ // groups must be present so the in/out filters each yield matches.
+ const getPortsSpy = vi.fn(() => [
+ { id: "in-0", group: "in" },
+ { id: "out-0", group: "out" },
+ ]);
+ const model = { attr: attrSpy, getPorts: getPortsSpy, portProp: portPropSpy };
+ const paper = { getModelById: vi.fn(() => model) } as unknown as joint.dia.Paper;
+ const service = new JointUIService(emptyMetadataStub as never);
+
+ service.changeOperatorState(paper, "op-1", OperatorState.Running);
+
+ // Running → orange. Both ports get the same color through portProp.
+ expect(portPropSpy).toHaveBeenCalledWith("in-0", "attrs/.port-label/fill", "orange");
+ expect(portPropSpy).toHaveBeenCalledWith("out-0", "attrs/.port-label/fill", "orange");
+ });
+ });
+
+ describe("changeOperatorDisableStatus", () => {
+ it("paints the body fill with the disabled grey color for a disabled operator", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorDisableStatus(paper, { operatorID: "op-1", isDisabled: true } as OperatorPredicate);
+ expect(attrSpy).toHaveBeenCalledWith("rect.body/fill", "#E0E0E0");
+ });
+ it("paints the body fill with the default white color for an enabled operator", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorDisableStatus(paper, { operatorID: "op-1" } as OperatorPredicate);
+ expect(attrSpy).toHaveBeenCalledWith("rect.body/fill", "#FFFFFF");
+ });
+ });
+
+ describe("changeOperatorViewResultStatus", () => {
+ it("writes the view-result asset path when viewResult is true", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorViewResultStatus(paper, {
+ operatorID: "op-1",
+ viewResult: true,
+ } as unknown as OperatorPredicate);
+ expect(attrSpy).toHaveBeenCalledTimes(1);
+ const [selector, value] = attrSpy.mock.calls[0];
+ expect(String(selector)).toContain("view-result");
+ expect(value).toBe("assets/svg/operator-view-result.svg");
+ });
+ it("writes the empty asset path when viewResult is missing/false", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorViewResultStatus(paper, { operatorID: "op-1" } as OperatorPredicate);
+ expect(attrSpy.mock.calls[0][1]).toBe("");
+ });
+ });
+
+ describe("changeOperatorReuseCacheStatus", () => {
+ it("writes both the reuse-cache icon and the view-result icon when the cache is valid", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorReuseCacheStatus(
+ paper,
+ { operatorID: "op-1", markedForReuse: true } as unknown as OperatorPredicate,
+ "cache valid"
+ );
+ // Two attr() calls — one for the cache icon, one for the view-result
+ // icon — both targeted at the operator's image attrs.
+ expect(attrSpy).toHaveBeenCalledTimes(2);
+ const allValues = attrSpy.mock.calls.map(c => c[1]).join(" | ");
+ expect(allValues).toContain("assets/svg/operator-reuse-cache-valid.svg");
+ });
+ it("writes the empty path for both icons when the operator isn't marked for reuse", () => {
+ const { paper, attrSpy } = makePaperWithModel();
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorReuseCacheStatus(paper, {
+ operatorID: "op-1",
+ markedForReuse: false,
+ } as unknown as OperatorPredicate);
+ expect(attrSpy).toHaveBeenCalledTimes(2);
+ expect(attrSpy.mock.calls.every(c => c[1] === "")).toBe(true);
+ });
+ });
+
+ describe("changeOperatorStatistics", () => {
+ function makeStatsPaper(getPortsImpl: () => Array<{ id?: string; group?: string; attrs?: unknown }>) {
+ const attrSpy = vi.fn();
+ const portPropSpy = vi.fn();
+ const getPortsSpy = vi.fn(getPortsImpl);
+ const model = { attr: attrSpy, getPorts: getPortsSpy, portProp: portPropSpy };
+ const paper = { getModelById: vi.fn(() => model) } as unknown as joint.dia.Paper;
+ return { paper, attrSpy, portPropSpy };
+ }
+
+ it("falls back to the Uninitialized state when statistics is undefined", () => {
+ const { paper, attrSpy } = makeStatsPaper(() => []);
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorStatistics(paper, "op-1", undefined, false, false);
+ // changeOperatorState writes the state-class fill payload.
+ const [payload] = attrSpy.mock.calls[0];
+ expect((payload as Record<string, { text: string }>)[`.${operatorStateClass}`].text).toBe(
+ OperatorState.Uninitialized.toString()
+ );
+ });
+
+ it("writes per-port counts derived from inputPortMetrics and outputPortMetrics", () => {
+ const { paper, portPropSpy } = makeStatsPaper(() => [
+ // Port IDs use the "<group>-<index>" convention; the SUT splits on "-"
+ // and uses the suffix to look up the metrics map.
+ { id: "in-0", group: "in", attrs: { ".port-label": { text: "data: 0" } } },
+ { id: "out-1", group: "out", attrs: { ".port-label": { text: "result: 0" } } },
+ ]);
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorStatistics(
+ paper,
+ "op-1",
+ {
+ operatorState: OperatorState.Running,
+ aggregatedInputRowCount: 0,
+ aggregatedOutputRowCount: 0,
+ inputPortMetrics: { "0": 42 },
+ outputPortMetrics: { "1": 7 },
+ numWorkers: 3,
+ },
+ false,
+ false
+ );
+ expect(portPropSpy).toHaveBeenCalledWith("in-0", "attrs/.port-label/text", (42).toLocaleString());
+ expect(portPropSpy).toHaveBeenCalledWith("out-1", "attrs/.port-label/text", (7).toLocaleString());
+ });
+
+ it("writes the worker count label when statistics include numWorkers", () => {
+ const { paper, attrSpy } = makeStatsPaper(() => []);
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorStatistics(
+ paper,
+ "op-1",
+ {
+ operatorState: OperatorState.Ready,
+ aggregatedInputRowCount: 0,
+ aggregatedOutputRowCount: 0,
+ inputPortMetrics: {},
+ outputPortMetrics: {},
+ numWorkers: 8,
+ },
+ false,
+ false
+ );
+ // attr() is called once with the workers selector and the formatted string.
+ const valuesWritten = attrSpy.mock.calls.map(c => c[1]);
+ expect(valuesWritten).toContain("#workers: 8");
+ });
+
+ it("defaults the worker count to 1 when numWorkers is unspecified", () => {
+ const { paper, attrSpy } = makeStatsPaper(() => []);
+ const service = new JointUIService(emptyMetadataStub as never);
+ service.changeOperatorStatistics(
+ paper,
+ "op-1",
+ {
+ operatorState: OperatorState.Ready,
+ aggregatedInputRowCount: 0,
+ aggregatedOutputRowCount: 0,
+ inputPortMetrics: {},
+ outputPortMetrics: {},
+ },
+ false,
+ false
+ );
+ const valuesWritten = attrSpy.mock.calls.map(c => c[1]);
+ expect(valuesWritten).toContain("#workers: 1");
+ });
+ });
});
diff --git a/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts b/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts
index d5d8f78c58..d069a270eb 100644
--- a/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts
+++ b/frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts
@@ -396,18 +396,8 @@ export class JointUIService {
if (portId != null) {
const parts = portId.split("-");
const numericSuffix = parts.length > 1 ? parts[1] : portId;
-
const count: number = inputMetrics[numericSuffix] ?? 0;
- const rawAttrs = (portDef.attrs as any) || {};
- const oldText: string = (rawAttrs[".port-label"] && rawAttrs[".port-label"].text) || "";
- let originalName = oldText.includes(":") ? oldText.split(":", 1)[0].trim() : oldText;
-
- if (!originalName) {
- originalName = portId;
- }
-
- const labelText = count.toLocaleString();
- element.portProp(portId, "attrs/.port-label/text", labelText);
+ element.portProp(portId, "attrs/.port-label/text", count.toLocaleString());
}
});
@@ -416,19 +406,8 @@ export class JointUIService {
if (portId != null) {
const parts = portId.split("-");
const numericSuffix = parts.length > 1 ? parts[1] : portId;
-
const count: number = outputMetrics[numericSuffix] ?? 0;
- const rawAttrs = (portDef.attrs as any) || {};
- const oldText: string = (rawAttrs[".port-label"] && rawAttrs[".port-label"].text) || "";
- let originalName = oldText.includes(":") ? oldText.split(":", 1)[0].trim() : oldText;
-
- if (!originalName) {
- originalName = portId;
- }
-
- const labelText = count.toLocaleString();
-
- element.portProp(portId, "attrs/.port-label/text", labelText);
+ element.portProp(portId, "attrs/.port-label/text", count.toLocaleString());
}
});
this.changeOperatorState(jointPaper, operatorID, statistics.operatorState);
@@ -457,11 +436,6 @@ export class JointUIService {
".remove-input-port-button": { visibility: "visible" },
".remove-output-port-button": { visibility: "visible" },
});
-
- const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model;
- if (!element) {
- return;
- }
}
public changeOperatorState(jointPaper: joint.dia.Paper, operatorID: string, operatorState: OperatorState): void {
@@ -696,40 +670,6 @@ export class JointUIService {
};
}
- /**
- * This function create a custom svg style for the operator
- * @returns the custom attributes of the tooltip.
- */
- public static getCustomOperatorStatusTooltipStyleAttrs(): joint.shapes.devs.ModelSelectors {
- return {
- "element-node": {
- style: { "pointer-events": "none" },
- },
- polygon: {
- fill: "#FFFFFF",
- "follow-scale": true,
- stroke: "purple",
- "stroke-width": "2",
- rx: "5px",
- ry: "5px",
- refPoints: "0,30 150,30 150,120 85,120 75,150 65,120 0,120",
- display: "none",
- style: { "pointer-events": "none" },
- },
- "#operatorCount": {
- fill: "#595959",
- "font-size": "12px",
- ref: "polygon",
- "y-alignment": "middle",
- "x-alignment": "left",
- "ref-x": 0.05,
- "ref-y": 0.2,
- display: "none",
- style: { "pointer-events": "none" },
- },
- };
- }
-
/**
* This function creates a custom svg style for the operator.
* This function also makes the delete button defined above to emit the delete event that will