import React from 'react'; import { act, create, type ReactTestRenderer } from 'react-test-renderer'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { SavedQuery, TabData } from '../types'; import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; import QueryEditor, { resolveQueryEditorNavigationTarget } from './QueryEditor'; const storeState = vi.hoisted(() => ({ connections: [ { id: 'conn-1', name: 'local', config: { type: 'mysql', host: '127.0.0.1', port: 3306, user: 'root', password: '', database: 'main', }, }, ], addSqlLog: vi.fn(), addTab: vi.fn(), setActiveContext: vi.fn(), updateQueryTabDraft: vi.fn(), savedQueries: [] as SavedQuery[], saveQuery: vi.fn(), theme: 'light', appearance: { uiVersion: 'legacy' as 'legacy' | 'v2' }, sqlFormatOptions: { keywordCase: 'upper' as const }, setSqlFormatOptions: vi.fn(), queryOptions: { maxRows: 5000 }, setQueryOptions: vi.fn(), shortcutOptions: { runQuery: { mac: { enabled: false, combo: '' }, windows: { enabled: false, combo: '' }, }, selectCurrentStatement: { mac: { enabled: false, combo: '' }, windows: { enabled: false, combo: '' }, }, }, activeTabId: 'tab-1', aiPanelVisible: false, setAIPanelVisible: vi.fn(), })); const backendApp = vi.hoisted(() => ({ DBQuery: vi.fn(), DBQueryWithCancel: vi.fn(), DBQueryMulti: vi.fn(), DBGetTables: vi.fn(), DBGetAllColumns: vi.fn(), DBGetDatabases: vi.fn(), DBGetColumns: vi.fn(), DBGetIndexes: vi.fn(), CancelQuery: vi.fn(), GenerateQueryID: vi.fn(), WriteSQLFile: vi.fn(), })); const messageApi = vi.hoisted(() => ({ error: vi.fn(), info: vi.fn(), success: vi.fn(), warning: vi.fn(), })); const dataGridState = vi.hoisted(() => ({ latestProps: null as any, })); const autoFetchState = vi.hoisted(() => ({ visible: false, })); const editorState = vi.hoisted(() => { const state = { value: '', editor: null as any, position: { lineNumber: 1, column: 1 }, selection: null as any, providers: [] as any[], cursorPositionListeners: [] as Array<(event: any) => void>, mouseMoveListeners: [] as Array<(event: any) => void>, mouseDownListeners: [] as Array<(event: any) => void>, mouseLeaveListeners: [] as Array<() => void>, hasTextFocus: true, decorationIds: [] as string[], }; const offsetAt = (position: { lineNumber: number; column: number }) => { const text = state.value; let offset = 0; for (let lineNumber = 1; lineNumber < Math.max(1, position.lineNumber); lineNumber++) { const nextLineBreak = text.indexOf('\n', offset); if (nextLineBreak === -1) { return text.length; } offset = nextLineBreak + 1; } return Math.min(text.length, offset + Math.max(0, position.column - 1)); }; const positionAt = (offset: number) => { const text = state.value.replace(/\r\n/g, '\n'); const safeOffset = Math.max(0, Math.min(text.length, Number(offset) || 0)); const prefix = text.slice(0, safeOffset); const lines = prefix.split('\n'); return { lineNumber: lines.length, column: (lines[lines.length - 1]?.length || 0) + 1 }; }; const valueInRange = (range: any) => { if (!range) return ''; const start = offsetAt({ lineNumber: range.startLineNumber, column: range.startColumn }); const end = offsetAt({ lineNumber: range.endLineNumber, column: range.endColumn }); return state.value.slice(Math.min(start, end), Math.max(start, end)); }; const model = { getValue: () => state.value, setValue: (value: string) => { state.value = value; }, getValueInRange: valueInRange, getLineContent: (lineNumber: number) => state.value.replace(/\r\n/g, '\n').split('\n')[lineNumber - 1] || '', getLineCount: () => state.value.replace(/\r\n/g, '\n').split('\n').length, getLineMaxColumn: (lineNumber: number) => (state.value.replace(/\r\n/g, '\n').split('\n')[lineNumber - 1] || '').length + 1, getWordUntilPosition: () => ({ startColumn: 1, endColumn: 1, word: '' }), getOffsetAt: offsetAt, getPositionAt: positionAt, }; state.editor = { getValue: vi.fn(() => state.value), setValue: vi.fn((value: string) => { state.value = value; }), getModel: vi.fn(() => model), getPosition: vi.fn(() => state.position), setPosition: vi.fn((position: any) => { state.position = position; }), getSelection: vi.fn(() => state.selection), setSelection: vi.fn((selection: any) => { state.selection = selection; }), executeEdits: vi.fn((_source: string, edits: any[]) => { edits.forEach((edit) => { const start = offsetAt({ lineNumber: edit.range.startLineNumber, column: edit.range.startColumn }); const end = offsetAt({ lineNumber: edit.range.endLineNumber, column: edit.range.endColumn }); state.value = state.value.slice(0, start) + edit.text + state.value.slice(end); }); }), addAction: vi.fn(), onDidChangeModelContent: vi.fn(() => ({ dispose: vi.fn() })), onDidChangeCursorPosition: vi.fn((listener: (event: any) => void) => { state.cursorPositionListeners.push(listener); return { dispose: vi.fn() }; }), onMouseMove: vi.fn((listener: (event: any) => void) => { state.mouseMoveListeners.push(listener); return { dispose: vi.fn() }; }), onMouseDown: vi.fn((listener: (event: any) => void) => { state.mouseDownListeners.push(listener); return { dispose: vi.fn() }; }), onMouseLeave: vi.fn((listener: () => void) => { state.mouseLeaveListeners.push(listener); return { dispose: vi.fn() }; }), deltaDecorations: vi.fn((oldDecorations: string[], newDecorations: any[]) => { state.decorationIds = newDecorations.map((_: any, index: number) => `decoration-${index + 1}`); return state.decorationIds; }), updateOptions: vi.fn(), onDidDispose: vi.fn(), hasTextFocus: vi.fn(() => state.hasTextFocus), revealLineInCenterIfOutsideViewport: vi.fn(), revealRangeInCenterIfOutsideViewport: vi.fn(), focus: vi.fn(), trigger: vi.fn(), }; return state; }); vi.mock('../store', () => { const useStore = Object.assign( (selector: (state: typeof storeState) => any) => selector(storeState), { getState: () => storeState }, ); return { useStore }; }); vi.mock('../../wailsjs/go/app/App', () => backendApp); vi.mock('../utils/autoFetchVisibility', () => ({ useAutoFetchVisibility: () => autoFetchState.visible, })); vi.mock('@monaco-editor/react', () => ({ default: ({ defaultValue, onMount }: any) => { React.useEffect(() => { editorState.value = String(defaultValue || ''); onMount?.(editorState.editor, { editor: { setTheme: vi.fn() }, languages: { CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 }, CompletionItemInsertTextRule: { InsertAsSnippet: 1 }, registerCompletionItemProvider: vi.fn((_language: string, provider: any) => { editorState.providers.push(provider); return { dispose: vi.fn() }; }), }, Range: class { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { this.startLineNumber = startLineNumber; this.startColumn = startColumn; this.endLineNumber = endLineNumber; this.endColumn = endColumn; } }, MarkdownString: class { value: string; constructor(value: string) { this.value = value; } }, Position: class { lineNumber: number; column: number; constructor(lineNumber: number, column: number) { this.lineNumber = lineNumber; this.column = column; } }, }); }, []); return