Xiao-zhen-Liu commented on code in PR #5146:
URL: https://github.com/apache/texera/pull/5146#discussion_r3320350405
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -232,99 +233,107 @@ describe("WorkflowEditorComponent", () => {
fixture.detectChanges();
});
- it("should try to highlight the operator when user mouse clicks on an
operator", () => {
- const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
- // install a spy on the highlight operator function and pass the call
through
- vi.spyOn(jointGraphWrapper, "highlightOperators");
- workflowActionService.addOperator(mockScanPredicate, mockPoint);
-
- // unhighlight the operator in case it's automatically highlighted
- jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID);
-
- // find the joint Cell View object of the operator element
- const jointCellView =
component.paper.findViewByModel(mockScanPredicate.operatorID);
- jointCellView.$el.trigger("mousedown");
-
- fixture.detectChanges();
-
- // assert the function is called once
- // expect(highlightOperatorFunctionSpy.calls.count()).toEqual(1);
- // assert the highlighted operator is correct
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);
- });
-
- it("should highlight the commentBox when user clicks on a commentBox", ()
=> {
- const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
- vi.spyOn(jointGraphWrapper, "highlightCommentBoxes");
- workflowActionService.addCommentBox(mockCommentBox);
- jointGraphWrapper.unhighlightCommentBoxes(mockCommentBox.commentBoxID);
- const jointCellView =
component.paper.findViewByModel(mockCommentBox.commentBoxID);
- jointCellView.$el.trigger("mousedown");
- fixture.detectChanges();
-
expect(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()).toEqual([mockCommentBox.commentBoxID]);
- });
-
- it("should open commentBox as NzModal when user double clicks on a
commentBox", () => {
- const modalRef: NzModalRef = nzModalService.create({
- nzTitle: "CommentBox",
- nzContent: NzModalCommentBoxComponent,
- nzData: { commentBox: createYTypeFromObject(mockCommentBox) },
- nzAutofocus: null,
- nzFooter: [
- {
- label: "OK",
- onClick: () => {
- modalRef.destroy();
- },
- type: "primary",
- },
- ],
- });
- vi.spyOn(nzModalService, "create").mockReturnValue(modalRef);
- const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
- workflowActionService.addCommentBox(mockCommentBox);
- jointGraphWrapper.highlightCommentBoxes(mockCommentBox.commentBoxID);
- const jointCellView =
component.paper.findViewByModel(mockCommentBox.commentBoxID);
- jointCellView.$el.trigger("dblclick");
- expect(nzModalService.create).toHaveBeenCalled();
- fixture.detectChanges();
- modalRef.destroy();
- });
-
- it("should unhighlight all highlighted operators when user mouse clicks on
the blank space", () => {
- const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
-
- // add and highlight two operators
- workflowActionService.addOperatorsAndLinks(
- [
- { op: mockScanPredicate, pos: mockPoint },
- { op: mockResultPredicate, pos: mockPoint },
- ],
- []
- );
- jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID,
mockResultPredicate.operatorID);
-
- // assert that both operators are highlighted
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);
-
- // find a blank area on the JointJS paper
- const blankPoint = { x: mockPoint.x + 100, y: mockPoint.y + 100 };
- expect(component.paper.findViewsFromPoint(blankPoint)).toEqual([]);
-
- // trigger a click on the blank area using JointJS paper's jQuery element
- const point = component.paper.localToClientPoint(blankPoint);
- const event = createJQueryEvent("mousedown", {
- clientX: point.x,
- clientY: point.y,
- });
- component.paper.$el.trigger(event);
-
- fixture.detectChanges();
-
- // assert that all operators are unhighlighted
- expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]);
- });
+ // TODO(#3614): the following four mouse/click-event tests rely on JointJS
Review Comment:
These 6 tests are the browser-only coverage for this file, and the
`test-browser` target (`angular.json:105`) runs *only* this spec. Since they're
commented out *here*, they no longer run under `ng run gui:test-browser` either
— the PR description's claim that they "continue to pass" there isn't accurate;
commenting them out drops them from every target.
Prefer `it.skip(...)`/`xit(...)` so they still appear as explicitly skipped
(discoverable, not silently lost), or better, a runtime guard that skips under
jsdom but executes under the real-Chrome `test-browser` runner — that keeps the
coverage the browser target exists to provide. ~150 lines of commented-out test
code will rot quickly and a follow-up PR to "revive" them rarely happens.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -844,52 +853,56 @@ describe("WorkflowEditorComponent", () => {
// }
// });
- it("should highlight multiple operators when user clicks on them with
shift key pressed", () => {
- const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
-
- workflowActionService.addOperator(mockScanPredicate, mockPoint);
- workflowActionService.addOperator(mockResultPredicate, mockPoint);
- jointGraphWrapper.highlightOperators(mockResultPredicate.operatorID);
-
- // assert that only the last operator is highlighted
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).not.toContain(mockScanPredicate.operatorID);
-
- // find the joint Cell View object of the first operator element
- const jointCellView =
component.paper.findViewByModel(mockScanPredicate.operatorID);
-
- // trigger a shift click on the cell view using its jQuery element
- const event = createJQueryEvent("mousedown", { shiftKey: true });
- jointCellView.$el.trigger(event);
-
- fixture.detectChanges();
-
- // assert that both operators are highlighted
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);
- });
-
- it("should unhighlight the highlighted operator when user clicks on it
with shift key pressed", () => {
- const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
-
- workflowActionService.addOperator(mockScanPredicate, mockPoint);
- jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);
-
- // assert that the operator is highlighted
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);
-
- // find the joint Cell View object of the operator element
- const jointCellView =
component.paper.findViewByModel(mockScanPredicate.operatorID);
-
- // trigger a shift click on the cell view using its jQuery element
- const event = createJQueryEvent("mousedown", { shiftKey: true });
- jointCellView.$el.trigger(event);
-
- fixture.detectChanges();
-
- // assert that the operator is unhighlighted
-
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).not.toContain(mockScanPredicate.operatorID);
- });
+ // TODO(#3614): the next two shift-click multi-select tests also depend on
Review Comment:
Same concern as the earlier commented-out block — these shift-click tests
are also removed from every target, including `test-browser`. `it.skip`/`xit`
or an env-guard would keep them visible and reviveable.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -358,6 +358,28 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
});
}
});
+
+ // When operators are (re)added to the graph — e.g. after navigating back
to
+ // the workflow page, where WorkspaceComponent calls reloadWorkflow and
+ // operators are recreated from the workflow JSON — restore their visual
+ // state from the cached status so completed runs don't appear to reset.
+ this.workflowActionService
Review Comment:
This new subscription and `handleOperatorValidation` below now both read
`workflowStatusService.getCurrentStatus()` and paint the execution-state
border. That's duplicated "read cache → repaint" logic across two handlers, and
the root problem (two streams racing to write `rect.body/stroke`) isn't removed
— it's just made consistent for the valid+cached case. A single helper that
resolves the final border from (validation result, cached state) and is invoked
from one place would be cleaner and order-independent.
Minor: a `// eslint`-style nit aside, this restores port counts/worker count
(which validation doesn't), so the subscription is genuinely needed — it's the
border-stroke overlap with validation that's the smell.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -949,15 +971,31 @@ export class WorkflowEditorComponent implements OnInit,
AfterViewInit, OnDestroy
}
/**
- * if the operator is valid , the border of the box will be default
+ * Applies the validation result to the operator's border.
+ * - Invalid operators are drawn with a red border (validation takes
priority).
+ * - Valid operators with a cached execution status keep their
execution-state
+ * border so e.g. a Completed (green) run isn't repainted gray when the
user
+ * navigates back to the workflow and reloadWorkflow re-adds the operators,
+ * which triggers a validation pass that would otherwise overwrite the
+ * execution-state stroke set by handleOperatorStatisticsUpdate.
+ * - Valid operators with no cached status get the default valid border.
*/
private handleOperatorValidation(): void {
this.validationWorkflowService
.getOperatorValidationStream()
.pipe(untilDestroyed(this))
- .subscribe(value =>
- this.jointUIService.changeOperatorColor(this.paper, value.operatorID,
value.validation.isValid)
- );
+ .subscribe(value => {
+ if (!value.validation.isValid) {
+ this.jointUIService.changeOperatorColor(this.paper,
value.operatorID, false);
+ return;
+ }
+ const statistics =
this.workflowStatusService.getCurrentStatus()[value.operatorID];
+ if (statistics) {
+ this.jointUIService.changeOperatorState(this.paper,
value.operatorID, statistics.operatorState);
Review Comment:
Correctness/fragility: the invalid + cached path is order-dependent. The
operator-add subscription above paints the cached *state* color (e.g. green)
via `changeOperatorStatistics`, and this handler paints red. The red only
"wins" if the validation stream emits *after* the add-stream handler — but they
reach here via different event chains (add-stream → component subscription vs.
operator-add → `dynamicSchemaService` → validation). Nothing guarantees that
ordering. It's rare in practice (an invalid operator rarely has cached stats),
but if validation ever fires first, the green paint would overwrite the red
border. Computing the final stroke in one resolver (validation takes priority,
else cached state, else default) would remove the dependency on emission order.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -962,5 +975,75 @@ describe("WorkflowEditorComponent", () => {
fixture.detectChanges();
expect(redoSpy).toHaveBeenCalledTimes(4);
});
+
+ /**
+ * Regression coverage for the bug where the operator border resets to the
+ * default (gray) when the user navigates away from and back to a workflow
+ * that has already finished executing. The visual state is driven by two
+ * separate streams that both touch rect.body/stroke: the execution status
+ * stream (sets state-derived color) and the validation stream (sets red on
+ * invalid, gray on valid). When operators are re-added by reloadWorkflow,
+ * the validation pass fires after the status repaint and used to overwrite
+ * it. handleOperatorValidation now consults the cached status before
+ * deciding which color to apply.
+ */
+ describe("operator border restoration after navigation", () => {
+ let workflowStatusService: WorkflowStatusService;
+ const cachedCompleted = {
+ [mockScanPredicate.operatorID]: {
+ operatorState: OperatorState.Completed,
+ aggregatedInputRowCount: 0,
+ inputPortMetrics: {},
+ aggregatedOutputRowCount: 0,
+ outputPortMetrics: {},
+ },
+ };
+
+ beforeEach(() => {
+ workflowStatusService = TestBed.inject(WorkflowStatusService);
+ });
+
+ it("repaints execution-state stroke for valid operators with a cached
status", () => {
Review Comment:
This test doesn't isolate the change it's meant to cover.
`changeOperatorState(..., Completed)` is *also* called by the operator-add
subscription via `changeOperatorStatistics`, so this assertion passes even if
`handleOperatorValidation` were reverted to the old behavior. To actually
exercise the validation branch, either stub/disable the add-stream path or
assert the final `rect.body/stroke` attribute on the model (the visible
outcome) rather than that a method was called.
##########
frontend/angular.json:
##########
@@ -92,10 +92,7 @@
"runnerConfig": "vitest.config.ts",
"tsConfig": "src/tsconfig.spec.json",
"include": ["**/*.spec.ts"],
- "setupFiles": ["src/jsdom-svg-polyfill.ts"],
- "exclude": [
-
"**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts"
- ]
+ "setupFiles": ["src/jsdom-svg-polyfill.ts"]
Review Comment:
Decomposability: un-excluding this spec from the default jsdom target is a
CI/test-infra change that is logically independent of the operator-border bug
fix. Bundling them couples a behavior fix to a test-config change, making the
PR harder to review and revert as a unit. Consider splitting the
test-enablement into its own PR.
Also note `test-browser` (line 105 below) includes *only* this spec — see
the comment on the commented-out tests in the spec file for why that matters.
##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -962,5 +975,75 @@ describe("WorkflowEditorComponent", () => {
fixture.detectChanges();
expect(redoSpy).toHaveBeenCalledTimes(4);
});
+
+ /**
+ * Regression coverage for the bug where the operator border resets to the
+ * default (gray) when the user navigates away from and back to a workflow
+ * that has already finished executing. The visual state is driven by two
+ * separate streams that both touch rect.body/stroke: the execution status
+ * stream (sets state-derived color) and the validation stream (sets red on
+ * invalid, gray on valid). When operators are re-added by reloadWorkflow,
+ * the validation pass fires after the status repaint and used to overwrite
+ * it. handleOperatorValidation now consults the cached status before
+ * deciding which color to apply.
+ */
+ describe("operator border restoration after navigation", () => {
+ let workflowStatusService: WorkflowStatusService;
+ const cachedCompleted = {
+ [mockScanPredicate.operatorID]: {
+ operatorState: OperatorState.Completed,
+ aggregatedInputRowCount: 0,
+ inputPortMetrics: {},
+ aggregatedOutputRowCount: 0,
+ outputPortMetrics: {},
+ },
+ };
+
+ beforeEach(() => {
+ workflowStatusService = TestBed.inject(WorkflowStatusService);
+ });
+
+ it("repaints execution-state stroke for valid operators with a cached
status", () => {
+ vi.spyOn(workflowStatusService,
"getCurrentStatus").mockReturnValue(cachedCompleted);
+ vi.spyOn(validationWorkflowService,
"validateOperator").mockReturnValue({ isValid: true });
+ const changeOperatorStateSpy = vi.spyOn(jointUIService,
"changeOperatorState");
+
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+ fixture.detectChanges();
+
+ expect(changeOperatorStateSpy).toHaveBeenCalledWith(
+ component.paper,
+ mockScanPredicate.operatorID,
+ OperatorState.Completed
+ );
+ });
+
+ it("falls back to the default valid stroke when no cached status
exists", () => {
+ vi.spyOn(workflowStatusService,
"getCurrentStatus").mockReturnValue({});
+ vi.spyOn(validationWorkflowService,
"validateOperator").mockReturnValue({ isValid: true });
+ const changeOperatorColorSpy = vi.spyOn(jointUIService,
"changeOperatorColor");
+
+ workflowActionService.addOperator(mockScanPredicate, mockPoint);
+ fixture.detectChanges();
+
+ expect(changeOperatorColorSpy).toHaveBeenCalledWith(component.paper,
mockScanPredicate.operatorID, true);
+ });
+
+ it("paints the invalid stroke (red) for invalid operators regardless of
cached status", () => {
Review Comment:
This asserts `changeOperatorColor(..., false)` was *called*, but not that
red is the *final/winning* paint — and per the note on
`handleOperatorValidation`, that outcome is order-dependent. The test even
acknowledges in its comment that the add-stream still paints green first.
Asserting the resulting `rect.body/stroke` === `red` on the model would catch a
regression where the green statistics paint lands last; the current assertion
would not.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]