// Copyright 2026 OpenObserve Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { describe, it, expect, vi } from "vitest"; import { flushPromises, mount } from "@vue/test-utils"; import { createStore } from "vuex"; import CodeQueryEditor from "./CodeQueryEditor.vue"; // Any Monaco service call before editor.all.js finishes initialises StandaloneServices without the contributions' singletons. const load = vi.hoisted(() => { let release!: () => void; let entered!: () => void; const state = { contributionsLoaded: false, early: [] as string[], released: new Promise((resolve) => (release = resolve)), started: new Promise((resolve) => (entered = resolve)), release: () => release(), entered: () => entered(), }; return state; }); const track = (name: string, result?: T) => (..._args: unknown[]) => { if (!load.contributionsLoaded) load.early.push(name); return result; }; const mockModel = { getValue: vi.fn(() => ""), setValue: vi.fn(), getLineCount: vi.fn(() => 1), getLineLength: vi.fn(() => 0), getOffsetAt: vi.fn(() => 0), getPositionAt: vi.fn(() => ({ lineNumber: 1, column: 1 })), getLineContent: vi.fn(() => ""), getValueInRange: vi.fn(() => ""), getWordUntilPosition: vi.fn(() => ({ word: "", startColumn: 1, endColumn: 1 })), getValueLength: vi.fn(() => 0), uri: { toString: () => "inmemory://model" }, }; const mockEditor = { onDidChangeModelContent: vi.fn(), createContextKey: vi.fn(), addCommand: vi.fn(), onKeyDown: vi.fn(), onDidFocusEditorWidget: vi.fn(), onDidBlurEditorWidget: vi.fn(), onDidBlurEditorText: vi.fn(), onDidChangeCursorPosition: vi.fn(), createDecorationsCollection: vi.fn(() => ({ set: vi.fn(), clear: vi.fn() })), dispose: vi.fn(), getValue: vi.fn(() => ""), setValue: vi.fn(), layout: vi.fn(), getModel: vi.fn(() => mockModel), updateOptions: vi.fn(), hasWidgetFocus: vi.fn(() => false), getRawOptions: vi.fn(() => ({ readOnly: false })), deltaDecorations: vi.fn(() => []), getPosition: vi.fn(() => ({ lineNumber: 1, column: 1 })), trigger: vi.fn(), getAction: vi.fn(() => ({ run: vi.fn(() => Promise.resolve()) })), }; vi.mock("monaco-editor/esm/vs/editor/editor.all.js", async () => { load.entered(); await load.released; load.contributionsLoaded = true; return {}; }); vi.mock("monaco-editor/esm/vs/editor/editor.api", () => { const disposable = { dispose: () => {} }; return { default: {}, editor: { create: vi.fn(track("editor.create", mockEditor)), defineTheme: vi.fn(track("editor.defineTheme")), setTheme: vi.fn(track("editor.setTheme")), setModelMarkers: vi.fn(), setModelLanguage: vi.fn(), TrackedRangeStickiness: { NeverGrowsWhenTypingAtEdges: 1 }, addCommand: vi.fn(track("editor.addCommand", disposable)), addKeybindingRule: vi.fn(track("editor.addKeybindingRule", disposable)), addKeybindingRules: vi.fn(track("editor.addKeybindingRules", disposable)), registerCommand: vi.fn(track("editor.registerCommand", disposable)), getEditors: vi.fn(() => []), }, languages: { CompletionItemKind: { Method: 0, Function: 1, Constructor: 2, Field: 3, Variable: 4, Operator: 11, Value: 13, Keyword: 17, Text: 18, Snippet: 27, }, CompletionItemInsertTextRule: { None: 0, KeepWhitespace: 1, InsertAsSnippet: 4 }, register: vi.fn(), setMonarchTokensProvider: vi.fn(), setLanguageConfiguration: vi.fn(), registerCompletionItemProvider: vi.fn(track("languages.registerCompletion", disposable)), registerSignatureHelpProvider: vi.fn(track("languages.registerSignatureHelp", disposable)), registerHoverProvider: vi.fn(track("languages.registerHover", disposable)), }, KeyMod: { CtrlCmd: 2048, WinCtrl: 256 }, KeyCode: { Backspace: 1, Tab: 2, Enter: 13, Escape: 9, Space: 10, PageUp: 11, PageDown: 12, UpArrow: 16, DownArrow: 18, KeyI: 39, KeyN: 44, KeyP: 46, KeyZ: 56, }, Range: class { constructor(...args: number[]) { Object.assign(this, { args }); } }, }; }); vi.mock("monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js", () => ({})); vi.mock("@/composables/useLogs/searchState", () => ({ default: () => ({ searchObj: { data: {}, meta: {} } }), searchState: () => ({ searchObj: { data: {}, meta: {} } }), })); vi.mock("@/composables/useNLQuery", () => ({ useNLQuery: () => ({ detectNaturalLanguage: vi.fn(() => false), generateSQL: vi.fn(() => Promise.resolve(null)), transformToSQL: vi.fn((_nl: string, sql: string) => sql), isGenerating: { value: false }, streamingResponse: { value: "" }, }), })); vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) })); vi.mock("@/composables/useNotifications", () => ({ default: () => ({ showErrorNotification: vi.fn(), showPositiveNotification: vi.fn() }), })); vi.mock("@/utils/zincutils", () => ({ getImageURL: vi.fn((path: string) => `/mocked/${path}`) })); vi.mock("@/utils/query/vrlLanguageDefinition", () => ({ vrlLanguageDefinition: {} })); const attachHost = (id: string) => { const host = document.createElement("div"); host.id = id; document.body.appendChild(host); }; const mountEditor = (editorId: string) => mount(CodeQueryEditor, { props: { editorId, query: "SELECT * FROM logs", language: "sql" }, global: { plugins: [createStore({ state: { theme: "light" } })] }, }); const settle = async () => { for (let i = 0; i < 5; i++) { await flushPromises(); await new Promise((resolve) => setTimeout(resolve, 10)); } }; describe("CodeQueryEditor Monaco loading", () => { it("makes an editor mounted mid-load wait until every Monaco contribution has loaded", async () => { const monacoApi: any = await import("monaco-editor/esm/vs/editor/editor.api"); attachHost("editor-first"); attachHost("editor-second"); const first = mountEditor("editor-first"); await load.started; const second = mountEditor("editor-second"); await settle(); expect(load.early).toEqual([]); expect(monacoApi.editor.create).not.toHaveBeenCalled(); load.release(); await vi.waitFor(() => expect(monacoApi.editor.create).toHaveBeenCalledTimes(2)); expect(load.early).toEqual([]); first.unmount(); second.unmount(); }); });