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-7481-5914ae077fee211ba89af55ac7fea86bde2e0b72 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 5482f7769bd9706440c9c027a60ea89e82f270af Author: Xinyuan Lin <[email protected]> AuthorDate: Sun Aug 9 17:33:16 2026 -0700 test(frontend): cover the link-breakpoint handlers in the workflow editor (#7481) ### What changes were proposed in this PR? `handleLinkBreakpoint()` and the four handlers it installs have never run in any test. The guard at `workflow-editor.component.ts:202` needs `linkBreakpointEnabled` **and** `getHighlightingEnabled()`, and both default to false under test — `MockGuiConfigService` for the first, `WorkflowActionService`'s private field for the second. `gui.conf` ships `link-breakpoint-enabled = true`, so the block is a shipped feature with no coverage rather than dead code. Adds 5 tests in a new `describe` that sets both flags before the first change-detection cycle (`ngAfterViewInit` reads them once, when it decides whether to install the handlers at all) and then drives the handlers through the paper: | Test | What it pins | |---|---| | tool attached, hidden | a new link gets a breakpoint tool, and it stays out of sight until wanted | | breakpoint click highlights | clicking the button highlights that link | | shift-click unhighlights | a second shift-click removes an already-selected link | | shift reaches multi-select | the modifier is carried into multi-select mode | | show/hide streams | both streams reach the link view, in that order | **Verified by mutation**, all reverted (production diff empty): | Mutation | Result | |---|---| | tool never attached | red | | tool left visible | red | | shift not carried into multi-select | red | | re-highlights instead of unhighlighting | red | | show/hide streams swapped | red | | breakpoint handlers never installed | red | Two of these needed the test strengthened before they died: - **show/hide swapped** initially survived — wiring each handler to the other's stream still calls `showTools` and `hideTools` once each. The test now asserts call order. - **shift not carried into multi-select** is routed through the unhighlight branch on purpose. `WorkflowActionService.highlightLinks` sets multi-select itself, so on the highlight branch the handler's own `setMultiSelectMode` is unobservable; `unhighlightLinks` does not touch it. No production file is touched. ### Any related issues, documentation, discussions? Closes #7478 ### How was this PR tested? ``` npx ng test --watch=false --include="**/workflow-editor.component.spec.ts" ``` ``` Test Files 1 passed (1) Tests 62 passed (62) ``` 5 new on top of the existing 57. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --- .../workflow-editor.component.spec.ts | 129 +++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts index d721f10a8d..d2ecfda618 100644 --- a/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts +++ b/frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts @@ -59,6 +59,8 @@ import { ComputingUnitStatusService } from "../../../common/service/computing-un import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; import { OperatorMenuService } from "../../service/operator-menu/operator-menu.service"; +import { GuiConfigService } from "src/app/common/service/gui-config.service"; +import { MockGuiConfigService } from "src/app/common/service/gui-config.service.mock"; describe("WorkflowEditorComponent", () => { /** @@ -1515,3 +1517,130 @@ describe("WorkflowEditorComponent", () => { }); }); }); +/** + * Link breakpoints are a shipped feature — `gui.conf` sets `link-breakpoint-enabled = true` — but + * `MockGuiConfigService` defaults it to false, so `handleLinkBreakpoint` and the four handlers it + * installs have never run in any test. Turning the flag on before the first change-detection cycle + * (which is when ngAfterViewInit wires them) reaches the whole block. + */ +describe("WorkflowEditorComponent link breakpoints", () => { + let fixture: ComponentFixture<WorkflowEditorComponent>; + let component: WorkflowEditorComponent; + let workflowActionService: WorkflowActionService; + + beforeEach(async () => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + HttpClientTestingModule, + NzModalModule, + NzDropDownModule, + WorkflowEditorComponent, + ContextMenuComponent, + ], + providers: [ + JointUIService, + WorkflowUtilService, + UndoRedoService, + DragDropService, + ValidationWorkflowService, + WorkflowActionService, + NzContextMenuService, + Overlay, + { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, + { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, + WorkflowStatusService, + ExecuteWorkflowService, + ...commonTestProviders, + ], + }).compileComponents(); + + // Both halves of the guard at workflow-editor.component.ts:202 must be satisfied, and both must + // be set before the first detectChanges: ngAfterViewInit reads them once, when it decides + // whether to install the breakpoint handlers at all. + (TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService).setConfig({ + linkBreakpointEnabled: true, + }); + workflowActionService = TestBed.inject(WorkflowActionService); + workflowActionService.setHighlightingEnabled(true); + + fixture = TestBed.createComponent(WorkflowEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + /** Adds scan -> result and returns the link plus its rendered view. */ + function withLink() { + workflowActionService.addOperator(mockScanPredicate, mockPoint); + workflowActionService.addOperator(mockResultPredicate, mockPoint); + workflowActionService.addLink(mockScanResultLink); + const model = component.paper.getModelById(mockScanResultLink.linkID); + return { linkID: mockScanResultLink.linkID, model, view: model.findView(component.paper) as any }; + } + + it("attaches a breakpoint tool to every link, hidden until it is wanted", () => { + // The tool is what the user clicks to set a breakpoint; without it the feature has no entry + // point, and leaving it visible would put a button on every link on the canvas. + const { view } = withLink(); + + expect(view.hasTools()).toBe(true); + // The tool exists but stays out of sight until the cursor hovers the link or a breakpoint is + // set; a visible one would put a button on every link on the canvas. + expect(view._toolsView.tools[0].isVisible()).toBe(false); + }); + + it("highlights the link whose breakpoint button was clicked", () => { + const { linkID, view } = withLink(); + + (component.paper as any).trigger("tool:breakpoint", view, { shiftKey: false }); + + expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs()).toEqual([linkID]); + }); + + it("unhighlights an already-highlighted link on a shift-click", () => { + // Shift is the multi-select modifier, so a second shift-click on the same link is how the user + // removes it from the selection rather than re-adding it. + const { linkID, view } = withLink(); + (component.paper as any).trigger("tool:breakpoint", view, { shiftKey: true }); + expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs()).toEqual([linkID]); + + (component.paper as any).trigger("tool:breakpoint", view, { shiftKey: true }); + + expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs()).toEqual([]); + }); + + it("carries the shift modifier into multi-select mode", () => { + // Routed through the unhighlight branch deliberately. On the highlight branch + // `WorkflowActionService.highlightLinks` sets multi-select itself, so the handler's own + // `setMultiSelectMode` could be deleted and the assertion would still pass; `unhighlightLinks` + // does not touch it, leaving this handler as the only writer. + const { view } = withLink(); + // multiSelect is private and has no getter; read it directly rather than adding an accessor. + const wrapper = workflowActionService.getJointGraphWrapper() as any; + + (component.paper as any).trigger("tool:breakpoint", view, { shiftKey: false }); + expect(wrapper.multiSelect).toBe(false); + (component.paper as any).trigger("tool:breakpoint", view, { shiftKey: true }); + + expect(wrapper.multiSelect).toBe(true); + }); + + it("shows and hides the tool as the breakpoint streams ask", () => { + // These two streams are how a link that already has a breakpoint keeps its marker visible after + // the cursor leaves it. + const { linkID, view } = withLink(); + const wrapper = workflowActionService.getJointGraphWrapper(); + const show = vi.spyOn(view, "showTools"); + const hide = vi.spyOn(view, "hideTools"); + + (wrapper as any).jointLinkBreakpointShowStream.next({ linkID }); + (wrapper as any).jointLinkBreakpointHideStream.next({ linkID }); + + expect(show).toHaveBeenCalledTimes(1); + expect(hide).toHaveBeenCalledTimes(1); + // Order matters, otherwise a handler pair wired to each other's stream passes: both would + // still be called once, just for the opposite reason. + expect(show.mock.invocationCallOrder[0]).toBeLessThan(hide.mock.invocationCallOrder[0]); + }); +});
