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 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]);
+  });
+});

Reply via email to