mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-13 09:59:37 +08:00
✨ feat(query-editor): 增强 SQL 编辑器执行与历史体验
- 支持仅执行选中 SQL、光标所在语句和增量新增语句 - 持久化查询草稿,避免重启后丢失历史 SQL - 在表字段提示中展示注释信息 - 修复清空默认 SQL 后被自动回填的问题 Refs #483
This commit is contained in:
@@ -23,10 +23,11 @@ const storeState = vi.hoisted(() => ({
|
||||
],
|
||||
addSqlLog: vi.fn(),
|
||||
addTab: vi.fn(),
|
||||
updateQueryTabDraft: vi.fn(),
|
||||
savedQueries: [] as SavedQuery[],
|
||||
saveQuery: vi.fn(),
|
||||
theme: 'light',
|
||||
appearance: { uiVersion: 'legacy' as const },
|
||||
appearance: { uiVersion: 'legacy' as 'legacy' | 'v2' },
|
||||
sqlFormatOptions: { keywordCase: 'upper' as const },
|
||||
setSqlFormatOptions: vi.fn(),
|
||||
queryOptions: { maxRows: 5000 },
|
||||
@@ -47,6 +48,7 @@ const storeState = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
const backendApp = vi.hoisted(() => ({
|
||||
DBQuery: vi.fn(),
|
||||
DBQueryWithCancel: vi.fn(),
|
||||
DBQueryMulti: vi.fn(),
|
||||
DBGetTables: vi.fn(),
|
||||
@@ -74,25 +76,82 @@ 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>,
|
||||
hasTextFocus: true,
|
||||
};
|
||||
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(() => ({
|
||||
getValue: () => state.value,
|
||||
setValue: (value: string) => {
|
||||
state.value = value;
|
||||
},
|
||||
getValueInRange: () => '',
|
||||
getLineContent: () => '',
|
||||
getWordUntilPosition: () => ({ startColumn: 1, endColumn: 1 }),
|
||||
})),
|
||||
getSelection: vi.fn(() => null),
|
||||
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() })),
|
||||
hasTextFocus: vi.fn(() => true),
|
||||
onDidChangeCursorPosition: vi.fn((listener: (event: any) => void) => {
|
||||
state.cursorPositionListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
hasTextFocus: vi.fn(() => state.hasTextFocus),
|
||||
revealLineInCenterIfOutsideViewport: vi.fn(),
|
||||
revealRangeInCenterIfOutsideViewport: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
trigger: vi.fn(),
|
||||
};
|
||||
return state;
|
||||
});
|
||||
@@ -119,7 +178,31 @@ vi.mock('@monaco-editor/react', () => ({
|
||||
editor: { setTheme: vi.fn() },
|
||||
languages: {
|
||||
CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 },
|
||||
registerCompletionItemProvider: vi.fn(),
|
||||
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;
|
||||
}
|
||||
},
|
||||
Position: class {
|
||||
lineNumber: number;
|
||||
column: number;
|
||||
constructor(lineNumber: number, column: number) {
|
||||
this.lineNumber = lineNumber;
|
||||
this.column = column;
|
||||
}
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
@@ -145,12 +228,13 @@ vi.mock('@ant-design/icons', () => {
|
||||
CloseOutlined: Icon,
|
||||
StopOutlined: Icon,
|
||||
RobotOutlined: Icon,
|
||||
DatabaseOutlined: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('antd', () => {
|
||||
const Button: any = ({ children, disabled, loading, onClick, ...rest }: any) => (
|
||||
<button type="button" disabled={disabled || loading} onClick={onClick} {...rest}>
|
||||
const Button: any = ({ children, disabled, loading, onClick, onMouseDown, ...rest }: any) => (
|
||||
<button type="button" disabled={disabled || loading} onClick={onClick} onMouseDown={onMouseDown} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
@@ -169,7 +253,15 @@ vi.mock('antd', () => {
|
||||
Dropdown: ({ children }: any) => <>{children}</>,
|
||||
Tooltip: ({ children }: any) => <>{children}</>,
|
||||
Select: () => null,
|
||||
Tabs: ({ items }: any) => <div>{items?.[0]?.children}</div>,
|
||||
Tabs: ({ activeKey, items }: any) => {
|
||||
const activeItem = items?.find((item: any) => item.key === activeKey) || items?.[0];
|
||||
return (
|
||||
<div>
|
||||
<div>{items?.map((item: any) => <span key={item.key}>{item.label}</span>)}</div>
|
||||
<div>{activeItem?.children}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -205,6 +297,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
messageApi.success.mockReset();
|
||||
messageApi.error.mockReset();
|
||||
messageApi.warning.mockReset();
|
||||
backendApp.DBQuery.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.WriteSQLFile.mockResolvedValue({ success: true });
|
||||
backendApp.DBQueryMulti.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.DBGetColumns.mockResolvedValue({ success: true, data: [] });
|
||||
@@ -212,10 +305,18 @@ describe('QueryEditor external SQL save', () => {
|
||||
backendApp.GenerateQueryID.mockResolvedValue('query-1');
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
storeState.appearance.uiVersion = 'legacy';
|
||||
dataGridState.latestProps = null;
|
||||
editorState.value = '';
|
||||
editorState.position = { lineNumber: 1, column: 1 };
|
||||
editorState.selection = null;
|
||||
editorState.providers = [];
|
||||
editorState.cursorPositionListeners = [];
|
||||
editorState.hasTextFocus = true;
|
||||
editorState.editor.getValue.mockClear();
|
||||
editorState.editor.setValue.mockClear();
|
||||
editorState.editor.executeEdits.mockClear();
|
||||
storeState.updateQueryTabDraft.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -223,6 +324,29 @@ describe('QueryEditor external SQL save', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows the default SQL template for a fresh blank query tab', async () => {
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: '' })} />);
|
||||
});
|
||||
|
||||
expect(editorState.value).toBe('SELECT * FROM ');
|
||||
});
|
||||
|
||||
it('keeps the editor empty when a tab draft is externally synced to an empty query', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ query: 'SELECT * FROM ' })} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderer.update(<QueryEditor tab={createTab({ query: '' })} />);
|
||||
});
|
||||
|
||||
expect(editorState.value).toBe('');
|
||||
expect(editorState.editor.setValue).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('writes external SQL file tabs back to disk without creating saved queries', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
const filePath = '/Users/me/Documents/gonavi-queries/report.sql';
|
||||
@@ -538,6 +662,467 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(messageApi.warning).toHaveBeenCalledWith('查询结果保持只读:main.users 未检测到主键或可用唯一索引,无法安全提交修改。');
|
||||
});
|
||||
|
||||
it('runs the SQL statement at the cursor instead of the whole editor when nothing is selected', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['two'], rows: [{ two: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1;\nselect 2 as two;\nselect 3;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 2, column: 8 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.();
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as two'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
|
||||
expect(storeState.addSqlLog).toHaveBeenCalledWith(expect.objectContaining({
|
||||
sql: expect.stringContaining('select 2 as two'),
|
||||
}));
|
||||
});
|
||||
|
||||
it('keeps cursor statement execution available in v2 UI', async () => {
|
||||
storeState.appearance.uiVersion = 'v2';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['two'], rows: [{ two: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1;\nselect 2 as two;\nselect 3;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 2, column: 8 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.();
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as two'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
|
||||
});
|
||||
|
||||
it('uses the last editor cursor position when the run button takes focus', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['two'], rows: [{ two: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\nselect 2 as b;\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.cursorPositionListeners.forEach((listener) => {
|
||||
listener({ position: { lineNumber: 2, column: 'select 2 as b;'.length + 1 } });
|
||||
});
|
||||
editorState.hasTextFocus = false;
|
||||
editorState.position = { lineNumber: 3, column: 'select 3 as c;'.length + 1 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as b'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3 as c');
|
||||
});
|
||||
|
||||
it('prefers the last editor cursor event even if Monaco still reports text focus', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['two'], rows: [{ two: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\nselect 2 as b;\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.cursorPositionListeners.forEach((listener) => {
|
||||
listener({ position: { lineNumber: 2, column: 'select 2 as b;'.length + 1 } });
|
||||
});
|
||||
editorState.hasTextFocus = true;
|
||||
editorState.position = { lineNumber: 3, column: 'select 3 as c;'.length + 1 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as b'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3 as c');
|
||||
});
|
||||
|
||||
it('uses Monaco active selection position when run button focus drifts onto a blank line', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['b'], rows: [{ b: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\nselect 2 as b;\n\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.selection = {
|
||||
startLineNumber: 2,
|
||||
startColumn: 'select 2 as b;'.length + 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 'select 2 as b;'.length + 1,
|
||||
positionLineNumber: 2,
|
||||
positionColumn: 'select 2 as b;'.length + 1,
|
||||
};
|
||||
editorState.position = { lineNumber: 3, column: 1 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as b'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1 as a');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3 as c');
|
||||
expect(messageApi.info).not.toHaveBeenCalledWith('没有可执行的 SQL。');
|
||||
});
|
||||
|
||||
it('keeps cursor statement execution when CRLF line endings put the cursor after a semicolon', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['b'], rows: [{ b: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\r\nselect 2 as b;\r\n\r\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 2, column: 'select 2 as b;'.length + 1 };
|
||||
editorState.selection = {
|
||||
startLineNumber: 2,
|
||||
startColumn: 'select 2 as b;'.length + 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 'select 2 as b;'.length + 1,
|
||||
positionLineNumber: 2,
|
||||
positionColumn: 'select 2 as b;'.length + 1,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as b'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1 as a');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3 as c');
|
||||
expect(messageApi.info).not.toHaveBeenCalledWith('没有可执行的 SQL。');
|
||||
});
|
||||
|
||||
it('does not execute SQL when the cursor is on a blank line', async () => {
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\nselect 2 as b;\n\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 3, column: 1 };
|
||||
editorState.selection = {
|
||||
startLineNumber: 3,
|
||||
startColumn: 1,
|
||||
endLineNumber: 3,
|
||||
endColumn: 1,
|
||||
positionLineNumber: 3,
|
||||
positionColumn: 1,
|
||||
};
|
||||
editorState.cursorPositionListeners.forEach((listener) => {
|
||||
listener({ position: { lineNumber: 3, column: 1 } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
|
||||
expect(messageApi.info).toHaveBeenCalledWith('没有可执行的 SQL。');
|
||||
});
|
||||
|
||||
it('runs only appended SQL and keeps existing results after a full editor execution', async () => {
|
||||
backendApp.DBQueryMulti
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['a'], rows: [{ a: 1 }] }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['b'], rows: [{ b: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 1, column: 'select 1 as a;'.length + 1 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
editorState.value = 'select 1 as a;\nselect 2 as b;';
|
||||
editorState.position = { lineNumber: 2, column: 'select 2 as b;'.length + 1 };
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(2);
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('select 1 as a');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).toContain('select 2 as b');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).not.toContain('select 1 as a');
|
||||
expect(textContent(renderer!.toJSON())).toContain('结果 1 (1)');
|
||||
expect(textContent(renderer!.toJSON())).toContain('结果 2 (1)');
|
||||
});
|
||||
|
||||
it('replaces the current result when rerunning the same cursor SQL', async () => {
|
||||
backendApp.DBQueryMulti
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['a'], rows: [{ a: 1 }] }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['a'], rows: [{ a: 10 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\nselect 2 as b;\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 1, column: 'select 1 as a;'.length + 1 };
|
||||
editorState.selection = {
|
||||
startLineNumber: 1,
|
||||
startColumn: 'select 1 as a;'.length + 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 'select 1 as a;'.length + 1,
|
||||
positionLineNumber: 1,
|
||||
positionColumn: 'select 1 as a;'.length + 1,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const tabLabels = renderer!.root.findAll((node) => textContent(node).includes('结果 '));
|
||||
expect(textContent(renderer!.toJSON())).toContain('结果 1 (1)');
|
||||
expect(textContent(renderer!.toJSON())).not.toContain('结果 2 (1)');
|
||||
expect(tabLabels.length).toBeGreaterThan(0);
|
||||
expect(dataGridState.latestProps?.data).toEqual(expect.arrayContaining([expect.objectContaining({ a: 10 })]));
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(2);
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).toContain('select 1 as a');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).not.toContain('select 2 as b');
|
||||
});
|
||||
|
||||
it('appends a result when running a different cursor SQL after an existing result', async () => {
|
||||
backendApp.DBQueryMulti
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['a'], rows: [{ a: 1 }] }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['b'], rows: [{ b: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1 as a;\nselect 2 as b;\nselect 3 as c;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 1, column: 'select 1 as a;'.length + 1 };
|
||||
editorState.selection = {
|
||||
startLineNumber: 1,
|
||||
startColumn: 'select 1 as a;'.length + 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 'select 1 as a;'.length + 1,
|
||||
positionLineNumber: 1,
|
||||
positionColumn: 'select 1 as a;'.length + 1,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 2, column: 'select 2 as b;'.length + 1 };
|
||||
editorState.selection = {
|
||||
startLineNumber: 2,
|
||||
startColumn: 'select 2 as b;'.length + 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 'select 2 as b;'.length + 1,
|
||||
positionLineNumber: 2,
|
||||
positionColumn: 'select 2 as b;'.length + 1,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const runButton = findButton(renderer!, '运行');
|
||||
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
|
||||
await runButton.props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(2);
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).toContain('select 2 as b');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).not.toContain('select 1 as a');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[1][2])).not.toContain('select 3 as c');
|
||||
expect(textContent(renderer!.toJSON())).toContain('结果 1 (1)');
|
||||
expect(textContent(renderer!.toJSON())).toContain('结果 2 (1)');
|
||||
});
|
||||
|
||||
it('runs selected SQL before cursor SQL', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['selected'], rows: [{ selected: 2 }] }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'main',
|
||||
query: 'select 1;\nselect 2 as selected;\nselect 3;',
|
||||
})} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 1, column: 4 };
|
||||
editorState.selection = {
|
||||
startLineNumber: 2,
|
||||
startColumn: 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 'select 2 as selected'.length + 1,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'main', expect.stringContaining('select 2 as selected'), 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
|
||||
});
|
||||
|
||||
it('allows editable table columns while leaving expression columns out of commits', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user