aglinxinyuan commented on code in PR #7425:
URL: https://github.com/apache/texera/pull/7425#discussion_r3743282692
##########
frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts:
##########
@@ -392,3 +393,215 @@ 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);
+ });
Review Comment:
Good catch — added an `afterEach` that calls
`removeMonacoBreakpointMethods()`, which is exactly the teardown the component
itself does (disposes the mouse-down disposable, then the `MonacoBreakpoint`),
plus the editor dispose the earlier block does. Fixed in a0e60cb.
Worth noting the teardown is safe against the stand-in editor: `dispose()`
nulls `editor` before `removeAllDecorations()`, so the model lookups
short-circuit rather than reaching the fake.
##########
frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts:
##########
@@ -392,3 +393,215 @@ 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);
+ });
+
+ 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).not.toBe(12);
+ });
Review Comment:
Agreed — switched to `toBeUndefined()` in a0e60cb, which is the component's
initial value for `breakpointConditionLine` and so the right assertion for
"nothing opened".
Confirmed it is load-bearing by mutation: making the no-breakpoint branch of
`onMouseRightClick` assign `0` before returning leaves the old `not.toBe(12)`
green, while the new assertion fails with `expected +0 to be undefined`.
Mutation reverted.
--
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]