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 fb1a5c4255f03e195e4ff36821d29acb71348071
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Aug 9 02:27:00 2026 -0700

    test(frontend): cover the debugger's breakpoint gutter (#7425)
    
    ### What changes were proposed in this PR?
    
    `setupMonacoBreakpointMethods` was the component's one uncovered block.
    The existing suite stubs it out — with a comment saying so — because the
    minimal editor mock cannot back a real `MonacoBreakpoint`, so neither of
    the two overrides it installs was exercised.
    
    Adds 13 tests over both.
    
    **The glyph override** decides what the gutter shows:
    
    ```
    exists && condition present   ->  monaco-conditional-breakpoint
    exists && no condition        ->  monaco-breakpoint
    hovering only                 ->  monaco-hover-breakpoint
    ```
    
    Covered including the `Boolean(condition?.trim())` guard — a condition
    left as whitespace must render as an ordinary breakpoint rather than
    claiming a condition the debugger will not apply — and the lookup
    happening at `range.startLineNumber`, since reading `endLineNumber`
    would attribute another line's condition to this glyph.
    
    **The mouse-down override** replaces the library's own handler. The
    `dispose()` before re-registering is load-bearing: two live handlers
    would add and immediately remove a breakpoint on a single click. A left
    click toggles; a right click opens the condition input instead of
    toggling, and only for a line that already has a breakpoint; clicks
    below the last line and outside the gutter do nothing.
    
    **Verified by mutation**, all reverted (production diff empty):
    
    | Mutation | Result |
    |---|---|
    | treat a blank condition as a condition | red |
    | read the condition from the range's end line | red |
    | key the condition lookup to a fixed operator | red |
    | swap the conditional and plain glyphs | red |
    | swap the exists and hover arms | red |
    | skip disposing the previous mouse-down handler | red |
    | drop the gutter target-type check | red |
    | drop the after-lines guard | red |
    | invert the left/right button branch | red |
    
    The stand-in editor is a `Proxy` that answers any unstubbed `on*`
    listener with an inert disposable, so the spec does not have to track
    which events `monaco-breakpoints` subscribes to — the first attempt
    failed on `onDidChangeCursorPosition`, and guessing at the rest would
    have been fragile.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7422
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/code-debugger.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  29 passed (29)
    ```
    
    13 new on top of the existing 16. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../code-debugger.component.spec.ts                | 220 +++++++++++++++++++++
 1 file changed, 220 insertions(+)

diff --git 
a/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts
 
b/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts
index cac2331de8..dbbfe40e10 100644
--- 
a/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts
@@ -30,6 +30,7 @@ import { commonTestProviders } from 
"../../../common/testing/test-utils";
 import type { Mocked } from "vitest";
 import type { MonacoBreakpoint } from "monaco-breakpoints";
 import type * as monaco from "monaco-editor";
+import * as monacoNs from "monaco-editor";
 describe("CodeDebuggerComponent", () => {
   let component: CodeDebuggerComponent;
   let fixture: ComponentFixture<CodeDebuggerComponent>;
@@ -392,3 +393,222 @@ describe("CodeDebuggerComponent", () => {
     });
   });
 });
+/**
+ * setupMonacoBreakpointMethods is the one uncovered block in the component: 
the suite above stubs it
+ * out because the minimal editor mock cannot back a real MonacoBreakpoint. It 
decides which gutter
+ * glyph a line gets and what a gutter click does, so it is worth driving 
directly.
+ */
+describe("CodeDebuggerComponent breakpoint gutter", () => {
+  const GUTTER = monacoNs.editor.MouseTargetType.GUTTER_GLYPH_MARGIN;
+  const CONTENT_TEXT = monacoNs.editor.MouseTargetType.CONTENT_TEXT;
+
+  let component: CodeDebuggerComponent;
+  let fixture: ComponentFixture<CodeDebuggerComponent>;
+  let debugService: {
+    getDebugState: ReturnType<typeof vi.fn>;
+    doModifyBreakpoint: ReturnType<typeof vi.fn>;
+    getCondition: ReturnType<typeof vi.fn>;
+  };
+  let editor: any;
+  let mouseDown: ((evt: any) => void) | undefined;
+  let firstMouseDownDisposable: { dispose: ReturnType<typeof vi.fn> } | 
undefined;
+
+  const operatorId = "op-under-debug";
+
+  /**
+   * A stand-in editor exposing only what MonacoBreakpoint and the override 
touch. Every listener
+   * hands back its own spy disposable so the dispose-before-reregister step 
is observable.
+   */
+  function makeEditor() {
+    const disposable = () => ({ dispose: vi.fn() });
+    const model = {
+      deltaDecorations: vi.fn(() => [] as string[]),
+      getLineCount: vi.fn(() => 100),
+      getValue: vi.fn(() => ""),
+      onDidChangeContent: vi.fn(disposable),
+      uri: { toString: () => "inmemory://model/1" },
+    };
+    let downCount = 0;
+    const base: Record<string, unknown> = {
+      onMouseDown: vi.fn((h: (evt: any) => void) => {
+        downCount += 1;
+        const d = disposable();
+        if (downCount === 1) {
+          // The MonacoBreakpoint constructor registers first; the override 
replaces it.
+          firstMouseDownDisposable = d;
+        } else {
+          mouseDown = h;
+        }
+        return d;
+      }),
+      getModel: vi.fn(() => model),
+      deltaDecorations: vi.fn(() => [] as string[]),
+      createDecorationsCollection: vi.fn(() => ({ set: vi.fn(), clear: vi.fn() 
})),
+      dispose: vi.fn(),
+    };
+    // MonacoBreakpoint subscribes to a handful of editor events whose exact 
set is its own business;
+    // anything named on* that is not explicitly stubbed above yields an inert 
disposable.
+    const editor = new Proxy(base, {
+      get(target, prop) {
+        if (prop in target) {
+          return target[prop as string];
+        }
+        if (typeof prop === "string" && prop.startsWith("on")) {
+          return () => disposable();
+        }
+        return undefined;
+      },
+      has() {
+        return true;
+      },
+    });
+    return { model, editor };
+  }
+
+  function gutterClick(lineNumber: number, opts: { leftButton?: boolean; 
isAfterLines?: boolean; type?: number } = {}) {
+    return {
+      target: {
+        type: opts.type ?? GUTTER,
+        detail: { isAfterLines: opts.isAfterLines ?? false },
+        position: { lineNumber },
+      },
+      event: { leftButton: opts.leftButton ?? true },
+    };
+  }
+
+  /** Calls the installed override the way monaco-breakpoints would. 
exists=true means Exist. */
+  function glyphFor(lineNumber: number, exists: boolean): string {
+    const range = { startLineNumber: lineNumber, endLineNumber: lineNumber + 5 
} as any;
+    const decoration = (component.monacoBreakpoint as 
any)["createBreakpointDecoration"](range, exists ? 0 : 1);
+    return decoration.options.glyphMarginClassName;
+  }
+
+  beforeEach(async () => {
+    mouseDown = undefined;
+    firstMouseDownDisposable = undefined;
+    debugService = {
+      getDebugState: vi.fn(() => new Y.Doc().getMap<BreakpointInfo>("debug")),
+      doModifyBreakpoint: vi.fn(),
+      getCondition: vi.fn(() => undefined),
+    };
+
+    TestBed.resetTestingModule();
+    await TestBed.configureTestingModule({
+      imports: [CodeDebuggerComponent],
+      schemas: [CUSTOM_ELEMENTS_SCHEMA],
+      providers: [
+        {
+          provide: WorkflowStatusService,
+          useValue: { getStatusUpdateStream: vi.fn(() => new 
Subject().asObservable()) },
+        },
+        { provide: UdfDebugService, useValue: debugService },
+        ...commonTestProviders,
+      ],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(CodeDebuggerComponent);
+    component = fixture.componentInstance;
+    component.currentOperatorId = operatorId;
+
+    const fake = makeEditor();
+    editor = fake.editor;
+    component.monacoEditor = editor;
+    component.setupMonacoBreakpointMethods(editor);
+  });
+
+  afterEach(() => {
+    // This suite drives a real MonacoBreakpoint, so tear it down the way the 
component does. Left
+    // alive, its editor listeners would outlive the test that registered them.
+    component.removeMonacoBreakpointMethods();
+    component.monacoEditor?.dispose();
+  });
+
+  describe("gutter glyph", () => {
+    it("marks a plain breakpoint", () => {
+      expect(glyphFor(3, true)).toBe("monaco-breakpoint");
+    });
+
+    it("marks a breakpoint that carries a condition", () => {
+      debugService.getCondition.mockReturnValue("count > 1");
+
+      expect(glyphFor(3, true)).toBe("monaco-conditional-breakpoint");
+    });
+
+    it("does not treat a blank condition as a condition", () => {
+      // A condition input left as whitespace must render as an ordinary 
breakpoint, otherwise the
+      // gutter claims a condition the debugger will not apply.
+      debugService.getCondition.mockReturnValue("   ");
+
+      expect(glyphFor(3, true)).toBe("monaco-breakpoint");
+    });
+
+    it("marks a line that is only being hovered", () => {
+      expect(glyphFor(3, false)).toBe("monaco-hover-breakpoint");
+    });
+
+    it("looks the condition up on the line the glyph starts at", () => {
+      // The range spans several lines; reading endLineNumber would attribute 
another line's
+      // condition to this glyph.
+      glyphFor(7, true);
+
+      expect(debugService.getCondition).toHaveBeenCalledWith(operatorId, 7);
+    });
+
+    it("keys the lookup to the operator being debugged", () => {
+      glyphFor(2, true);
+
+      expect(debugService.getCondition).toHaveBeenCalledWith(operatorId, 
expect.anything());
+    });
+  });
+
+  describe("gutter clicks", () => {
+    it("replaces the library's own mouse-down handler rather than adding to 
it", () => {
+      // Two live handlers would both toggle, so a single click would add and 
immediately remove a
+      // breakpoint.
+      expect(firstMouseDownDisposable!.dispose).toHaveBeenCalledTimes(1);
+      expect(editor.onMouseDown).toHaveBeenCalledTimes(2);
+    });
+
+    it("toggles the breakpoint on a left click in the gutter", () => {
+      mouseDown!(gutterClick(12));
+
+      expect(debugService.doModifyBreakpoint).toHaveBeenCalledWith(operatorId, 
12);
+    });
+
+    it("ignores a click below the last line", () => {
+      mouseDown!(gutterClick(12, { isAfterLines: true }));
+
+      expect(debugService.doModifyBreakpoint).not.toHaveBeenCalled();
+    });
+
+    it("ignores a click that is not on the gutter", () => {
+      mouseDown!(gutterClick(12, { type: CONTENT_TEXT }));
+
+      expect(debugService.doModifyBreakpoint).not.toHaveBeenCalled();
+    });
+
+    it("does not toggle on a right click", () => {
+      // The right button opens the condition input instead; toggling as well 
would remove the
+      // breakpoint the user is trying to annotate.
+      mouseDown!(gutterClick(12, { leftButton: false }));
+
+      expect(debugService.doModifyBreakpoint).not.toHaveBeenCalled();
+    });
+
+    it("opens the condition input for a right click on an existing 
breakpoint", () => {
+      (component.monacoBreakpoint as any)["lineNumberAndDecorationIdMap"] = 
new Map([[12, "dec-1"]]);
+
+      mouseDown!(gutterClick(12, { leftButton: false }));
+
+      expect(component.breakpointConditionLine).toBe(12);
+    });
+
+    it("opens nothing for a right click on a line with no breakpoint", () => {
+      (component.monacoBreakpoint as any)["lineNumberAndDecorationIdMap"] = 
new Map();
+
+      mouseDown!(gutterClick(12, { leftButton: false }));
+
+      expect(component.breakpointConditionLine).toBeUndefined();
+    });
+  });
+});

Reply via email to