diff --git a/packages/suite-base/src/panels/UserScriptEditor/Editor.test.tsx b/packages/suite-base/src/panels/UserScriptEditor/Editor.test.tsx new file mode 100644 index 0000000000..449f6f9ee6 --- /dev/null +++ b/packages/suite-base/src/panels/UserScriptEditor/Editor.test.tsx @@ -0,0 +1,305 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { act, render, waitFor } from "@testing-library/react"; +import * as monacoApi from "monaco-editor/esm/vs/editor/editor.api"; + +import { BasicBuilder } from "@lichtblick/test-builders"; + +import "@testing-library/jest-dom"; + +import Editor from "./Editor"; +import { Script } from "./script"; + +let mockOpenHandler: + | (( + input: { resource: { path: string }; options?: { selection?: unknown } }, + editor: unknown, + ) => Promise) + | undefined = undefined; + +jest.mock("monaco-editor", () => ({ + typescript: { + typescriptDefaults: { + addExtraLib: jest.fn(() => ({ dispose: jest.fn() })), + setEagerModelSync: jest.fn(), + setDiagnosticsOptions: jest.fn(), + setCompilerOptions: jest.fn(), + getCompilerOptions: jest.fn(() => ({})), + }, + javascriptDefaults: { + setEagerModelSync: jest.fn(), + }, + }, + KeyMod: { CtrlCmd: 1 }, + KeyCode: { KeyS: 55 }, +})); + +type MockModel = { + uri: { path: string; toString: () => string }; + value: string; + options: Record; + getValue: jest.Mock; + setValue: jest.Mock; + updateOptions: jest.Mock]>; + getFullModelRange: jest.Mock, []>; +}; +jest.mock("monaco-editor/esm/vs/editor/editor.api", () => { + const models = new Map(); + + const createModel = ( + value: string, + _language: string, + uri: { path: string; toString: () => string }, + ) => { + const model: MockModel = { + uri, + value, + options: {}, + getValue: jest.fn(() => model.value), + setValue: jest.fn((next) => { + model.value = next; + }), + updateOptions: jest.fn((opts) => { + model.options = { ...model.options, ...opts }; + }), + getFullModelRange: jest.fn(() => ({})), + }; + models.set(uri.path, model); + return model; + }; + + const getModel = (uri: { path: string; toString: () => string }) => models.get(uri.path); + + return { + editor: { + defineTheme: jest.fn(), + createModel: jest.fn( + (value: string, language: string, uri: { path: string; toString: () => string }) => + createModel(value, language, uri), + ), + getModel: jest.fn((uri: { path: string; toString: () => string }) => getModel(uri)), + }, + languages: { + registerDocumentFormattingEditProvider: jest.fn(), + }, + Uri: { + parse: jest.fn((value: string) => ({ + path: new URL(value).pathname, + toString: () => value, + })), + }, + KeyMod: { CtrlCmd: 1 }, + KeyCode: { KeyS: 55 }, + clearModels: () => { + models.clear(); + }, + __getModels: () => models, + }; +}); + +jest.mock("monaco-editor/esm/vs/editor/browser/services/codeEditorService", () => ({ + ICodeEditorService: Symbol("ICodeEditorService"), +})); + +jest.mock("monaco-editor/esm/vs/editor/standalone/browser/standaloneServices", () => ({ + StandaloneServices: { + get: jest.fn(() => ({ + registerCodeEditorOpenHandler: jest.fn((handler) => { + mockOpenHandler = handler; + return { dispose: jest.fn() }; + }), + })), + }, +})); + +let mockOnChange: ((code: string) => void) | undefined; +let mockEditor: ReturnType | undefined; + +const createMockEditor = () => { + const actions = new Map(); + const formatAction = { run: jest.fn(async () => {}) }; + actions.set("editor.action.formatDocument", formatAction); + let currentModel: MockModel | undefined; + + return { + setModel: jest.fn((model: MockModel) => { + currentModel = model; + }), + getModel: jest.fn(() => currentModel), + addAction: jest.fn(({ id, run }: { id: string; run: () => Promise | void }) => { + actions.set(id, { run: jest.fn(run) }); + }), + getAction: jest.fn((id: string) => actions.get(id)), + setSelection: jest.fn(), + revealRangeInCenter: jest.fn(), + setPosition: jest.fn(), + revealPositionInCenter: jest.fn(), + layout: jest.fn(), + }; +}; + +jest.mock("react-monaco-editor", () => { + return function MockMonacoEditor(props: { + editorWillMount?: (monaco: unknown) => unknown; + editorDidMount?: (editor: unknown, monaco: unknown) => void; + onChange?: (code: string) => void; + }) { + const mockMonacoApi = jest.requireMock("monaco-editor/esm/vs/editor/editor.api"); + mockOnChange = props.onChange; + mockEditor = createMockEditor(); + props.editorWillMount?.(mockMonacoApi); + props.editorDidMount?.(mockEditor, mockMonacoApi); + return undefined; + }; +}); + +jest.mock("@mui/material", () => ({ + useTheme: () => ({ palette: { mode: "dark" } }), +})); + +jest.mock("react-resize-detector", () => ({ + useResizeDetector: jest.fn(() => ({ ref: jest.fn() })), +})); + +jest.mock( + "@lichtblick/suite-base/players/UserScriptPlayer/transformerWorker/typescript/projectConfig", + () => ({ + getUserScriptProjectConfig: jest.fn(() => ({ + rosLib: { fileName: "ros-lib.d.ts" }, + declarations: [{ fileName: "types.d.ts", sourceCode: "// declarations" }], + utilityFiles: [{ filePath: "/utility.ts", sourceCode: "export const util = 1;" }], + })), + }), +); + +jest.mock("@lichtblick/suite-base/stories/inScreenshotTests", () => jest.fn(() => false)); + +// Tests + +describe("Editor", () => { + let baseScript: Script; + const buildScript = (overrides: Partial