for Monaco editor
+ const editorElement = document.createElement("div");
+ editorElement.id = "editor-container";
+ editorElement.style.width = "800px";
+ editorElement.style.height = "600px";
+ document.body.appendChild(editorElement); // Attach to document body
+
+ // Initialize the Monaco editor with the created element
+ component.monacoEditor = monaco.editor.create(editorElement, {
+ value: "function hello() {\n\tconsole.log(\"Hello, world!\");\n}",
+ language: "javascript",
+ });
+
+ // Trigger change detection to ensure view updates
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ // Clean up streams to prevent memory leaks
+ statusUpdateStream.complete();
+ });
+
+ it("should create the component", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should setup monaco breakpoint methods when state is Running", fakeAsync(() => {
+ const setupSpy = spyOn(component, "setupMonacoBreakpointMethods").and.callThrough();
+ const rerenderSpy = spyOn(component, "rerenderExistingBreakpoints").and.callThrough();
+
+ // Emit a Running state event
+ statusUpdateStream.next({
+ [operatorId]: { operatorState: OperatorState.Running, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0 },
+ });
+
+ tick();
+ fixture.detectChanges(); // Trigger change detection
+
+ expect(setupSpy).toHaveBeenCalled();
+ expect(rerenderSpy).toHaveBeenCalled();
+
+ // Emit the same state again (should not trigger setup again)
+ statusUpdateStream.next({
+ [operatorId]: { operatorState: OperatorState.Running, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0 },
+ });
+
+ tick();
+ fixture.detectChanges(); // Trigger change detection
+
+ expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call
+ expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call
+
+ // Emit the paused state (should not trigger setup)
+ statusUpdateStream.next({
+ [operatorId]: { operatorState: OperatorState.Paused, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0 },
+ });
+
+ tick();
+ fixture.detectChanges(); // Trigger change detection
+
+ expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call
+ expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call
+
+ // Emit the running state once more (should not trigger setup)
+ statusUpdateStream.next({
+ [operatorId]: { operatorState: OperatorState.Paused, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0 },
+ });
+
+ tick();
+ fixture.detectChanges(); // Trigger change detection
+
+ expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call
+ expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call
+ }));
+
+ it("should remove monaco breakpoint methods when state changes to Uninitialized", () => {
+ const removeSpy = spyOn(component, "removeMonacoBreakpointMethods").and.callThrough();
+
+ // Emit an Uninitialized state event
+ statusUpdateStream.next({
+ [operatorId]: {
+ operatorState: OperatorState.Uninitialized,
+ aggregatedOutputRowCount: 0,
+ aggregatedInputRowCount: 0,
+ },
+ });
+
+ fixture.detectChanges(); // Trigger change detection
+
+ expect(removeSpy).toHaveBeenCalled();
+
+ // Emit the same state again (should not trigger removal again)
+ statusUpdateStream.next({
+ [operatorId]: {
+ operatorState: OperatorState.Uninitialized,
+ aggregatedOutputRowCount: 0,
+ aggregatedInputRowCount: 0,
+ },
+ });
+
+ expect(removeSpy).toHaveBeenCalledTimes(1); // No additional call
+ });
+
+ it("should call doModifyBreakpoint on left click", () => {
+ // Simulate a left click on line 1
+ component["onMouseLeftClick"](1);
+
+ // Verify that the mock service was called with the correct arguments
+ expect(mockUdfDebugService.doModifyBreakpoint).toHaveBeenCalledWith(operatorId, 1);
+ });
+
+ it("should set breakpoint condition input on right click", () => {
+ // Mock a valid decoration map
+ component.monacoBreakpoint = {
+ lineNumberAndDecorationIdMap: new Map([
+ [1, "breakpoint1"],
+ [2, "breakpoint2"],
+ ]),
+ } as any;
+
+ // Simulate a right click on line 1, it should switch to 1
+ component["onMouseRightClick"](1);
+ expect(component.breakpointConditionLine).toBe(1);
+
+ // Simulate a right click on line 3, which does not have a breakpoint. no changes should occur
+ component["onMouseRightClick"](3);
+ expect(component.breakpointConditionLine).toBe(1);
+
+ // Simulate a right click on line 2, it should switch to 2
+ component["onMouseRightClick"](2);
+ expect(component.breakpointConditionLine).toBe(2);
+
+ // Simulate a right click on line 1, it should switch to 1
+ component["onMouseRightClick"](1);
+ expect(component.breakpointConditionLine).toBe(1);
+ });
+
+ it("should reset the breakpoint condition input when closed", () => {
+ // Set a condition line and close it
+ component.breakpointConditionLine = 1;
+ component.closeBreakpointConditionInput();
+
+ expect(component.breakpointConditionLine).toBeUndefined();
+ });
+});
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts
index 79d45363d56..ebe5212f969 100644
--- a/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts
+++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts
@@ -37,25 +37,25 @@ export class CodeDebuggerComponent implements AfterViewInit, SafeStyle {
public monacoBreakpoint: MonacoBreakpoint | undefined = undefined;
public breakpointConditionLine: number | undefined = undefined;
- constructor(private udfDebugService: UdfDebugService,
- private workflowStatusService: WorkflowStatusService) {
- }
+ constructor(
+ private udfDebugService: UdfDebugService,
+ private workflowStatusService: WorkflowStatusService
+ ) {}
ngAfterViewInit() {
this.registerStatusChangeHandler();
this.registerBreakpointRenderingHandler();
}
- private setupMonacoBreakpointMethods(editor: MonacoEditor) {
- console.log("setup");
-
+ setupMonacoBreakpointMethods(editor: MonacoEditor) {
// mimic the enum in monaco-breakpoints
enum BreakpointEnum {
Exist,
}
-
+
this.monacoBreakpoint = new MonacoBreakpoint({
- editor, hoverMessage: {
+ editor,
+ hoverMessage: {
added: {
value: "Click to remove the breakpoint.",
},
@@ -70,7 +70,7 @@ export class CodeDebuggerComponent implements AfterViewInit, SafeStyle {
// 3) conditional breakpoints. (conditional breakpoints are also exist breakpoints)
this.monacoBreakpoint["createBreakpointDecoration"] = (
range: Range,
- breakpointEnum: BreakpointEnum,
+ breakpointEnum: BreakpointEnum
): { range: Range; options: ModelDecorationOptions } => {
const condition = this.udfDebugService.getCondition(this.currentOperatorId, range.startLineNumber);
@@ -106,7 +106,7 @@ export class CodeDebuggerComponent implements AfterViewInit, SafeStyle {
});
}
- private removeMonacoBreakpointMethods() {
+ removeMonacoBreakpointMethods() {
if (!isDefined(this.monacoBreakpoint)) {
return;
}
@@ -194,7 +194,7 @@ export class CodeDebuggerComponent implements AfterViewInit, SafeStyle {
this.monacoBreakpoint;
}
- private rerenderExistingBreakpoints() {
+ rerenderExistingBreakpoints() {
this.udfDebugService.getDebugState(this.currentOperatorId).forEach(({ breakpointId }, lineNumStr) => {
if (!isDefined(breakpointId)) {
return;
@@ -204,22 +204,27 @@ export class CodeDebuggerComponent implements AfterViewInit, SafeStyle {
}
private registerStatusChangeHandler() {
- this.workflowStatusService.getStatusUpdateStream()
+ this.workflowStatusService
+ .getStatusUpdateStream()
.pipe(
- map(event => event[this.currentOperatorId]?.operatorState === OperatorState.Running || event[this.currentOperatorId]?.operatorState === OperatorState.Paused),
+ map(
+ event =>
+ event[this.currentOperatorId]?.operatorState === OperatorState.Running ||
+ event[this.currentOperatorId]?.operatorState === OperatorState.Paused
+ ),
distinctUntilChanged(),
- untilDestroyed(this),
- ).subscribe((enable) => {
-
- // Only enable the breakpoint methods if the operator is running or paused
- if (enable) {
- this.setupMonacoBreakpointMethods(this.monacoEditor);
- this.rerenderExistingBreakpoints();
- } else {
-
- // for other states, remove the breakpoint methods
- this.removeMonacoBreakpointMethods();
- }
- });
+ untilDestroyed(this)
+ )
+ .subscribe(enable => {
+ console.log("enable", enable);
+ // Only enable the breakpoint methods if the operator is running or paused
+ if (enable) {
+ this.setupMonacoBreakpointMethods(this.monacoEditor);
+ this.rerenderExistingBreakpoints();
+ } else {
+ // for other states, remove the breakpoint methods
+ this.removeMonacoBreakpointMethods();
+ }
+ });
}
}
diff --git a/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts b/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts
index b50bd378739..18adf76aa6f 100644
--- a/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts
+++ b/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts
@@ -150,7 +150,7 @@ export class UdfDebugService {
const debugMessageStream = this.workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").pipe(
filter(evt => evt.operatorId === operatorId && evt.messages.length > 0),
switchMap(evt => evt.messages),
- filter(msg => msg.source === "(Pdb)" && msg.msgType.name === "DEBUGGER"),
+ filter(msg => msg.source === "(Pdb)" && msg.msgType.name === "DEBUGGER")
);
// Handle stepping message.
@@ -159,7 +159,7 @@ export class UdfDebugService {
debugMessageStream
.pipe(
filter(msg => msg.title.startsWith(">")),
- map(msg => this.extractInfo(msg.title)),
+ map(msg => this.extractInfo(msg.title))
)
.subscribe(({ lineNum }) => {
if (!isDefined(lineNum)) return;
@@ -172,7 +172,7 @@ export class UdfDebugService {
debugMessageStream
.pipe(
filter(msg => msg.title.startsWith("Breakpoint")),
- map(msg => this.extractInfo(msg.title)),
+ map(msg => this.extractInfo(msg.title))
)
.subscribe(({ breakpointId, lineNum }) => {
if (isDefined(breakpointId) && isDefined(lineNum)) {
@@ -190,7 +190,7 @@ export class UdfDebugService {
debugMessageStream
.pipe(
filter(msg => msg.title.startsWith("Deleted")),
- map(msg => this.extractInfo(msg.title)),
+ map(msg => this.extractInfo(msg.title))
)
.subscribe(({ lineNum }) => {
if (!isDefined(lineNum)) {
From e06688335c3039c4fe7618f8a949fe7ad1fa9f0d Mon Sep 17 00:00:00 2001
From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com>
Date: Thu, 24 Oct 2024 22:45:26 -0700
Subject: [PATCH 32/33] add test
---
...eakpoint-condition-input.component.spec.ts | 105 ++++++++++++++++--
.../code-debugger.component.ts | 2 +-
2 files changed, 97 insertions(+), 10 deletions(-)
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts
index 7bc8a446b36..4eef287f0db 100644
--- a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts
+++ b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts
@@ -1,25 +1,112 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-
import { BreakpointConditionInputComponent } from "./breakpoint-condition-input.component";
import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service";
-import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { ElementRef, SimpleChanges } from "@angular/core";
+import * as monaco from "monaco-editor";
describe("BreakpointConditionInputComponent", () => {
let component: BreakpointConditionInputComponent;
let fixture: ComponentFixture
;
+ let mockUdfDebugService: jasmine.SpyObj;
+
+ let editorElement: HTMLElement;
+
+ beforeEach(async () => {
+ // Create a mock UdfDebugService
+ mockUdfDebugService = jasmine.createSpyObj("UdfDebugService", ["getCondition", "doUpdateBreakpointCondition"]);
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [UdfDebugService],
+ await TestBed.configureTestingModule({
declarations: [BreakpointConditionInputComponent],
- imports: [HttpClientTestingModule],
- });
+ providers: [{ provide: UdfDebugService, useValue: mockUdfDebugService }],
+ }).compileComponents();
+
fixture = TestBed.createComponent(BreakpointConditionInputComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
+
+ // Create and attach a to host the Monaco editor
+ editorElement = document.createElement("div");
+ editorElement.style.width = "800px";
+ editorElement.style.height = "600px";
+ document.body.appendChild(editorElement); // Attach to the DOM
+
+ // Initialize the Monaco editor
+ component.monacoEditor = monaco.editor.create(editorElement, {
+ value: "function hello() {\n\tconsole.log(\"Hello, world!\");\n}",
+ language: "javascript",
+ });
+
+ // Mock textarea element
+ const textareaElement = document.createElement("textarea");
+ component.conditionTextarea = new ElementRef(textareaElement);
+
+ // Set required inputs
+ component.operatorId = "test-operator";
+ component.lineNum = 1;
+
+ fixture.detectChanges(); // Trigger Angular's change detection
+ });
+
+ afterEach(() => {
+ // Clean up the editor and DOM element after each test
+ component.monacoEditor.dispose();
+ editorElement.remove();
});
- it("should create", () => {
+ it("should create the component", () => {
expect(component).toBeTruthy();
});
+
+ it("should update the condition when lineNum changes", () => {
+ mockUdfDebugService.getCondition.and.returnValue("existing condition");
+
+ const changes: SimpleChanges = {
+ lineNum: {
+ currentValue: 2,
+ previousValue: 1,
+ firstChange: false,
+ isFirstChange: () => false,
+ },
+ };
+
+ component.ngOnChanges(changes);
+
+ expect(component.condition).toBe("existing condition");
+ });
+
+ it("should focus the textarea when visible", () => {
+ const focusSpy = spyOn(component.conditionTextarea.nativeElement, "focus");
+
+ component.ngAfterViewChecked(); // Call lifecycle hook
+
+ expect(focusSpy).toHaveBeenCalled();
+ });
+
+ it("should handle Enter key event and save the condition", () => {
+ const emitSpy = spyOn(component.closeEmitter, "emit");
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
+
+ component.condition = " new condition ";
+ component.handleEvent(event);
+
+ expect(mockUdfDebugService.doUpdateBreakpointCondition).toHaveBeenCalledWith("test-operator", 1, "new condition");
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it("should not handle Enter key event if shift key is pressed", () => {
+ const emitSpy = spyOn(component.closeEmitter, "emit");
+ const event = new KeyboardEvent("keydown", { key: "Enter", shiftKey: true });
+
+ component.handleEvent(event);
+
+ expect(mockUdfDebugService.doUpdateBreakpointCondition).not.toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ });
+
+ it("should emit close event on focusout", () => {
+ const emitSpy = spyOn(component.closeEmitter, "emit");
+
+ component.handleEvent(); // Simulate focusout
+
+ expect(emitSpy).toHaveBeenCalled();
+ });
});
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts
index ebe5212f969..0cc4c14ed86 100644
--- a/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts
+++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts
@@ -52,7 +52,7 @@ export class CodeDebuggerComponent implements AfterViewInit, SafeStyle {
enum BreakpointEnum {
Exist,
}
-
+
this.monacoBreakpoint = new MonacoBreakpoint({
editor,
hoverMessage: {
From 91013c29c1c413006a0c1a19615e61b7a3e29c5a Mon Sep 17 00:00:00 2001
From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com>
Date: Thu, 24 Oct 2024 23:39:04 -0700
Subject: [PATCH 33/33] fix
---
.../breakpoint-condition-input.component.html | 6 +++--
...eakpoint-condition-input.component.spec.ts | 16 ++---------
.../breakpoint-condition-input.component.ts | 27 ++++++++++---------
3 files changed, 20 insertions(+), 29 deletions(-)
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.html b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.html
index 842465d84cb..5874e42bb92 100644
--- a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.html
+++ b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.html
@@ -1,11 +1,13 @@
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts
index 4eef287f0db..d7e2f76e621 100644
--- a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts
+++ b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts
@@ -1,14 +1,13 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { BreakpointConditionInputComponent } from "./breakpoint-condition-input.component";
import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service";
-import { ElementRef, SimpleChanges } from "@angular/core";
+import { SimpleChanges } from "@angular/core";
import * as monaco from "monaco-editor";
describe("BreakpointConditionInputComponent", () => {
let component: BreakpointConditionInputComponent;
let fixture: ComponentFixture;
let mockUdfDebugService: jasmine.SpyObj;
-
let editorElement: HTMLElement;
beforeEach(async () => {
@@ -35,10 +34,6 @@ describe("BreakpointConditionInputComponent", () => {
language: "javascript",
});
- // Mock textarea element
- const textareaElement = document.createElement("textarea");
- component.conditionTextarea = new ElementRef(textareaElement);
-
// Set required inputs
component.operatorId = "test-operator";
component.lineNum = 1;
@@ -50,6 +45,7 @@ describe("BreakpointConditionInputComponent", () => {
// Clean up the editor and DOM element after each test
component.monacoEditor.dispose();
editorElement.remove();
+ component.closeEmitter.emit();
});
it("should create the component", () => {
@@ -73,14 +69,6 @@ describe("BreakpointConditionInputComponent", () => {
expect(component.condition).toBe("existing condition");
});
- it("should focus the textarea when visible", () => {
- const focusSpy = spyOn(component.conditionTextarea.nativeElement, "focus");
-
- component.ngAfterViewChecked(); // Call lifecycle hook
-
- expect(focusSpy).toHaveBeenCalled();
- });
-
it("should handle Enter key event and save the condition", () => {
const emitSpy = spyOn(component.closeEmitter, "emit");
const event = new KeyboardEvent("keydown", { key: "Enter" });
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.ts
index 2e3042f0288..54d0c754f40 100644
--- a/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.ts
+++ b/core/gui/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.ts
@@ -22,32 +22,33 @@ import { MonacoEditor } from "monaco-breakpoints/dist/types";
templateUrl: "./breakpoint-condition-input.component.html",
styleUrls: ["./breakpoint-condition-input.component.scss"],
})
-export class BreakpointConditionInputComponent implements AfterViewChecked, OnChanges {
+export class BreakpointConditionInputComponent implements OnChanges {
constructor(private udfDebugService: UdfDebugService) {}
@Input() operatorId = "";
@Input() lineNum?: number;
@Input() monacoEditor!: MonacoEditor;
@Output() closeEmitter = new EventEmitter();
- @ViewChild("conditionTextarea") conditionTextarea!: ElementRef;
-
public condition = "";
-
+ public topPosition: string = "0px";
+ public leftPosition: string = "0px";
ngOnChanges(changes: SimpleChanges): void {
if (!isDefined(changes["lineNum"]?.currentValue)) {
return;
}
// when the line number changes, update the condition
this.condition = this.udfDebugService.getCondition(this.operatorId, this.lineNum!) ?? "";
- }
- ngAfterViewChecked(): void {
- if (!this.isVisible) {
- return;
- }
-
- // focus the textarea when it is visible
- this.conditionTextarea?.nativeElement.focus();
+ // update position
+ const layoutInfo = this.monacoEditor.getLayoutInfo();
+ const editorRect = this.monacoEditor.getDomNode()?.getBoundingClientRect();
+ const topValue =
+ (editorRect?.top || 0) +
+ this.monacoEditor.getBottomForLineNumber(this.lineNum!) -
+ this.monacoEditor.getScrollTop();
+ const leftValue = (editorRect?.left || 0) + (layoutInfo?.glyphMarginLeft || 0) - 160;
+ this.topPosition = `${topValue}px`;
+ this.leftPosition = `${leftValue}px`;
}
public left(): number {
@@ -58,7 +59,7 @@ export class BreakpointConditionInputComponent implements AfterViewChecked, OnCh
// Calculate the left position of the input popup based on the editor layout
const { glyphMarginLeft } = this.monacoEditor.getLayoutInfo()!;
const { left } = this.monacoEditor.getDomNode()!.getBoundingClientRect();
- return left + glyphMarginLeft - this.monacoEditor.getScrollLeft();
+ return left + glyphMarginLeft - this.monacoEditor.getScrollLeft() - 160;
}
public top(): number {