test(query-editor): 修复结果测试隔离与过期断言

This commit is contained in:
mango
2026-07-25 23:49:15 +08:00
parent cf553fda48
commit 2d9e92fbf9
2 changed files with 121 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { readFileSync } from 'node:fs';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { act, create as createRenderer, type ReactTestRenderer } from 'react-test-renderer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { readV2ThemeCss } from '../test/readV2ThemeCss';
@@ -11,6 +11,7 @@ import { I18nProvider } from '../i18n/provider';
import type { SavedQuery, TabData } from '../types';
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
import { setGlobalImeCompositionActive } from '../utils/shortcuts';
import { clearQueryEditorResultSession } from '../utils/queryEditorResultSessionCache';
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import { clearQueryEditorInlineRuntimeReadinessCache } from './queryEditor/QueryEditorAiAssist';
import QueryEditor, {
@@ -20,6 +21,17 @@ import QueryEditor, {
} from './QueryEditor';
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
const mountedRenderers = new Set<ReactTestRenderer>();
const create = (...args: Parameters<typeof createRenderer>): ReactTestRenderer => {
const renderer = createRenderer(...args);
mountedRenderers.add(renderer);
const unmount = renderer.unmount.bind(renderer);
renderer.unmount = () => {
mountedRenderers.delete(renderer);
unmount();
};
return renderer;
};
const storeState = vi.hoisted(() => ({
connections: [
@@ -604,6 +616,10 @@ const textContent = (node: any): string =>
.map((item: any) => (typeof item === 'string' ? item : textContent(item)))
.join('');
const findSqlLogTab = (renderer: ReactTestRenderer) => renderer.root.findAll(
(node) => node.props?.['data-tab-key'] === '__gonavi_sql_execution_log__',
);
const queryResultMessageText = (renderer: ReactTestRenderer): string => {
const values: string[] = [];
const walk = (node: any) => {
@@ -925,6 +941,11 @@ describe('QueryEditor external SQL save', () => {
});
afterEach(() => {
act(() => {
[...mountedRenderers].forEach((renderer) => renderer.unmount());
});
clearQueryEditorResultSession('tab-1');
clearQueryEditorResultSession('tab-2');
vi.unstubAllGlobals();
vi.clearAllMocks();
});
@@ -1024,7 +1045,7 @@ describe('QueryEditor external SQL save', () => {
findButton(renderer, '结果').props.onClick();
});
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
expect(findSqlLogTab(renderer)).toHaveLength(1);
expect(storeState.updateQueryTabDraft).toHaveBeenCalledWith('tab-1', {
resultPanelVisible: true,
});
@@ -1041,7 +1062,7 @@ describe('QueryEditor external SQL save', () => {
await act(async () => {
findButton(renderer, '结果').props.onClick();
});
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
expect(findSqlLogTab(renderer)).toHaveLength(1);
await act(async () => {
findButton(renderer, '隐藏').props.onClick();
@@ -1172,7 +1193,7 @@ describe('QueryEditor external SQL save', () => {
expect(firstEvent.preventDefault).toHaveBeenCalled();
expect(firstEvent.stopPropagation).toHaveBeenCalled();
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
expect(findSqlLogTab(renderer)).toHaveLength(1);
const secondEvent = createToggleEvent();
await act(async () => {
@@ -2189,7 +2210,7 @@ describe('QueryEditor external SQL save', () => {
});
expect(toggleEvent.preventDefault).toHaveBeenCalled();
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
expect(findSqlLogTab(renderer)).toHaveLength(1);
expect(storeState.updateQueryTabDraft).toHaveBeenLastCalledWith('tab-1', {
resultPanelVisible: true,
});
@@ -2230,7 +2251,7 @@ describe('QueryEditor external SQL save', () => {
renderer = create(<QueryEditor tab={createTab()} />);
});
expect(textContent(renderer.toJSON())).not.toContain('SQL 执行日志');
expect(findSqlLogTab(renderer)).toHaveLength(1);
await act(async () => {
windowListeners['gonavi:show-sql-execution-log']?.forEach((listener) => listener());
@@ -2245,7 +2266,7 @@ describe('QueryEditor external SQL save', () => {
windowListeners['gonavi:show-sql-execution-log']?.forEach((listener) => listener());
});
expect(textContent(renderer.toJSON())).not.toContain('SQL 执行日志');
expect(findSqlLogTab(renderer)).toHaveLength(0);
expect(storeState.updateQueryTabDraft).toHaveBeenLastCalledWith('tab-1', {
resultPanelVisible: false,
});
@@ -2475,7 +2496,7 @@ describe('QueryEditor external SQL save', () => {
renderer.update(<QueryEditor tab={createTab({ id: 'tab-2', resultPanelVisible: true })} />);
});
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
expect(findSqlLogTab(renderer)).toHaveLength(1);
renderer.unmount();
});
@@ -10499,7 +10520,7 @@ describe('QueryEditor external SQL save', () => {
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
});
it('renders V2 empty state copy for the active non-Chinese language', async () => {
it('renders the V2 SQL log tab for the active non-Chinese language', async () => {
storeState.appearance.uiVersion = 'v2';
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
@@ -10514,8 +10535,10 @@ describe('QueryEditor external SQL save', () => {
});
const rendered = textContent(renderer!.toJSON());
expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.title']);
expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.description']);
expect(findSqlLogTab(renderer!)).toHaveLength(1);
expect(rendered).toContain(catalogs['en-US']['log_panel.short_title']);
expect(rendered).not.toContain(catalogs['en-US']['query_editor.empty_state.title']);
expect(rendered).not.toContain(catalogs['en-US']['query_editor.empty_state.description']);
expect(rendered).not.toContain('等待执行 SQL');
expect(rendered).not.toContain('运行查询后,结果会在下方以新版数据网格展示。');
});
@@ -12385,7 +12408,6 @@ describe('QueryEditor external SQL save', () => {
async (dbType) => {
storeState.connections[0].config.type = dbType;
storeState.connections[0].config.database = dbType === 'oracle' || dbType === 'dameng' ? 'APP' : 'main';
const forceReadOnlyQueryResult = dbType === 'tdengine' || dbType === 'clickhouse';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['COUNT'], rows: [{ COUNT: 1 }] }],
@@ -12408,7 +12430,7 @@ describe('QueryEditor external SQL save', () => {
});
const expectedTableName = dbType === 'oracle' || dbType === 'dameng' ? 'USERS' : 'users';
expect(dataGridState.latestProps?.tableName).toBe(forceReadOnlyQueryResult ? undefined : expectedTableName);
expect(dataGridState.latestProps?.tableName).toBe(expectedTableName);
expect(dataGridState.latestProps?.editLocator).toBeUndefined();
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(backendApp.DBGetColumns).not.toHaveBeenCalled();

View File

@@ -1,12 +1,13 @@
import React from 'react';
import { readFileSync } from 'node:fs';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { act, create as createRenderer, type ReactTestRenderer } from 'react-test-renderer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { readV2ThemeCss } from '../test/readV2ThemeCss';
import { setCurrentLanguage } from '../i18n';
import type { SavedQuery, TabData } from '../types';
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
import { clearQueryEditorResultSession } from '../utils/queryEditorResultSessionCache';
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import {
@@ -25,6 +26,18 @@ import QueryEditorResultsPanel, {
shouldActivateResultTabDetachPointer,
} from './QueryEditorResultsPanel';
const mountedRenderers = new Set<ReactTestRenderer>();
const create = (...args: Parameters<typeof createRenderer>): ReactTestRenderer => {
const renderer = createRenderer(...args);
mountedRenderers.add(renderer);
const unmount = renderer.unmount.bind(renderer);
renderer.unmount = () => {
mountedRenderers.delete(renderer);
unmount();
};
return renderer;
};
const storeState = vi.hoisted(() => ({
connections: [
{
@@ -117,7 +130,9 @@ const backendApp = vi.hoisted(() => ({
DBQueryMulti: vi.fn(),
DBQueryMultiTransactional: vi.fn(),
DBCommitTransaction: vi.fn(),
DBCommitTransactionWithTrigger: vi.fn(),
DBRollbackTransaction: vi.fn(),
DBRollbackTransactionWithTrigger: vi.fn(),
DBGetTables: vi.fn(),
DBGetAllColumns: vi.fn(),
DBGetDatabases: vi.fn(),
@@ -745,6 +760,10 @@ describe('QueryEditor external SQL save', () => {
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
vi.stubGlobal('navigator', {
platform: 'MacIntel',
userAgent: 'Vitest',
});
setCurrentLanguage('zh-CN');
storeState.languagePreference = 'zh-CN';
storeState.shortcutOptions.runQuery.mac = { enabled: false, combo: '' };
@@ -818,7 +837,9 @@ describe('QueryEditor external SQL save', () => {
backendApp.DBQueryMulti.mockResolvedValue({ success: true, data: [] });
backendApp.DBQueryMultiTransactional.mockResolvedValue({ success: true, data: [] });
backendApp.DBCommitTransaction.mockResolvedValue({ success: true, message: '事务已提交' });
backendApp.DBCommitTransactionWithTrigger.mockResolvedValue({ success: true, message: '事务已提交' });
backendApp.DBRollbackTransaction.mockResolvedValue({ success: true, message: '事务已回滚' });
backendApp.DBRollbackTransactionWithTrigger.mockResolvedValue({ success: true, message: '事务已回滚' });
backendApp.DBGetColumns.mockResolvedValue({ success: true, data: [] });
backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] });
backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] });
@@ -876,6 +897,11 @@ describe('QueryEditor external SQL save', () => {
});
afterEach(() => {
act(() => {
[...mountedRenderers].forEach((renderer) => renderer.unmount());
});
clearQueryEditorResultSession('tab-1');
clearQueryEditorResultSession('tab-2');
vi.unstubAllGlobals();
vi.clearAllMocks();
});
@@ -883,8 +909,10 @@ describe('QueryEditor external SQL save', () => {
it('keeps Oracle anonymous PL/SQL blocks intact when running from the editor', async () => {
storeState.connections[0].config.type = 'oracle';
storeState.connections[0].config.database = 'ORCLPDB1';
backendApp.DBQueryMulti.mockResolvedValueOnce({
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
success: true,
transactionId: 'tx-oracle-block',
transactionPending: true,
data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 1 }] }],
});
const plsql = [
@@ -908,7 +936,13 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'ORCLPDB1', plsql, 'query-1');
expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalledWith(expect.anything(), 'ORCLPDB1', plsql, 'query-1');
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
expect(storeState.sqlEditorPendingTransactions['tab-1']).toMatchObject({
id: 'tx-oracle-block',
dbType: 'oracle',
statements: [plsql],
});
expect(storeState.addSqlLog).toHaveBeenCalledWith(expect.objectContaining({
sql: plsql,
status: 'success',
@@ -1228,7 +1262,7 @@ describe('QueryEditor external SQL save', () => {
expect(textContent(renderer!.toJSON())).toContain('消息 1');
expect(findResultMessageTextarea(renderer!).props.value).toBe("Table 'users'. Scan count 1, logical reads 3.");
expect(dataGridState.latestProps).toBeNull();
expect(renderer!.root.findAll((node) => node.props?.['data-grid'] === 'true')).toHaveLength(0);
});
it('preserves sqlserver message indentation and blank lines after stripping mssql prefixes', () => {
@@ -1729,7 +1763,7 @@ describe('QueryEditor external SQL save', () => {
expect(dataGridState.latestProps?.data).not.toEqual(expect.arrayContaining([expect.objectContaining({ name: 'master' })]));
});
it('localizes the non-Oracle no-safe-locator read-only warning in English while preserving the raw table name', async () => {
it('localizes the non-Oracle all-columns locator warning in English while preserving the raw table name', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
backendApp.DBQueryMulti.mockResolvedValueOnce({
@@ -1757,12 +1791,12 @@ describe('QueryEditor external SQL save', () => {
expect(dataGridState.latestProps?.tableName).toBe('users');
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'none',
readOnly: true,
reason: 'No primary key or usable unique index was detected, so changes cannot be committed safely.',
strategy: 'all-columns',
readOnly: false,
reason: 'No primary key or unique index was detected, so rows will be located by matching all columns. Edit with care.',
});
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(messageApi.warning).toHaveBeenCalledWith(
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalledWith(
'Query results remain read-only: main.users No primary key or usable unique index was detected, so changes cannot be committed safely.',
);
expect(messageApi.warning).not.toHaveBeenCalledWith(
@@ -1770,7 +1804,7 @@ describe('QueryEditor external SQL save', () => {
);
});
it('localizes the non-Oracle index-metadata-unavailable read-only warning in English while preserving the raw table name', async () => {
it('uses all-columns editing when non-Oracle unique-index metadata is unavailable', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
backendApp.DBQueryMulti.mockResolvedValueOnce({
@@ -1801,12 +1835,11 @@ describe('QueryEditor external SQL save', () => {
expect(dataGridState.latestProps?.tableName).toBe('users');
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'none',
readOnly: true,
reason: 'Unable to load unique index metadata, so changes cannot be committed safely.',
strategy: 'all-columns',
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(messageApi.warning).toHaveBeenCalledWith(
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalledWith(
'Query results remain read-only: main.users Unable to load unique index metadata, so changes cannot be committed safely.',
);
expect(messageApi.warning).not.toHaveBeenCalledWith(
@@ -1814,7 +1847,7 @@ describe('QueryEditor external SQL save', () => {
);
});
it('localizes the table-locator-metadata-unavailable read-only warning in English while preserving the raw table name', async () => {
it('uses all-columns editing when non-Oracle table locator metadata is unavailable', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
backendApp.DBQueryMulti.mockResolvedValueOnce({
@@ -1841,12 +1874,12 @@ describe('QueryEditor external SQL save', () => {
expect(dataGridState.latestProps?.tableName).toBe('users');
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'none',
readOnly: true,
reason: 'Unable to load primary key/unique index metadata for main.users, so changes cannot be committed safely.',
strategy: 'all-columns',
columns: [],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(messageApi.warning).toHaveBeenCalledWith(
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalledWith(
'Query results remain read-only: Unable to load primary key/unique index metadata for main.users, so changes cannot be committed safely.',
);
expect(messageApi.warning).not.toHaveBeenCalledWith(
@@ -1854,7 +1887,7 @@ describe('QueryEditor external SQL save', () => {
);
});
it('falls back to read-only results when query locator metadata stalls', async () => {
it('falls back to all-columns editing when query locator metadata stalls', async () => {
vi.useFakeTimers();
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
@@ -1891,7 +1924,11 @@ describe('QueryEditor external SQL save', () => {
);
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ NAME: 'alpha' });
expect(dataGridState.latestProps?.tableName).toBe('users');
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'all-columns',
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
} finally {
vi.useRealTimers();
}
@@ -2004,6 +2041,10 @@ describe('QueryEditor external SQL save', () => {
});
it('registers Windows Ctrl+R with Monaco CtrlCmd and runs the selected SQL', async () => {
vi.stubGlobal('navigator', {
platform: 'Win32',
userAgent: 'Vitest',
});
storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+R' };
const windowListeners: Record<string, ((event?: any) => void)[]> = {};
vi.stubGlobal('window', {
@@ -2063,7 +2104,7 @@ describe('QueryEditor external SQL save', () => {
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('select 3');
});
it('does not run SQL from the run shortcut when nothing is selected', async () => {
it('runs the cursor SQL from the run shortcut when nothing is selected', async () => {
storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Enter' };
storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+Enter' };
const windowListeners: Record<string, ((event?: any) => void)[]> = {};
@@ -2090,6 +2131,9 @@ describe('QueryEditor external SQL save', () => {
});
editorState.position = { lineNumber: 2, column: 8 };
editorState.selection = null;
editorState.cursorPositionListeners.forEach((listener) => {
listener({ position: editorState.position });
});
backendApp.DBQueryMulti.mockClear();
const event = createRunShortcutEvent();
@@ -2103,8 +2147,14 @@ describe('QueryEditor external SQL save', () => {
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
expect(messageApi.info).toHaveBeenCalledWith('没有可选择的 SQL 语句。');
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('runs selected SQL from the run shortcut', async () => {
@@ -4535,7 +4585,7 @@ describe('QueryEditor external SQL save', () => {
expect(messageApi.warning).not.toHaveBeenCalled();
});
it('gives a multiline single-table result an independent column pin scope without making it editable', async () => {
it('keeps a multiline single-table result tied to its editable all-columns locator', async () => {
const sql = [
'SELECT a.COMPID, a.MEMCARDNO,',
' a.MODIFYUSER, a.MODIFYTIME',
@@ -4562,10 +4612,13 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
expect(dataGridState.latestProps?.tableName).toBeUndefined();
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(dataGridState.latestProps?.columnPinScope).toMatch(/^query-result:[a-f0-9]+$/);
expect(dataGridState.latestProps?.columnPinScope).not.toContain('D_MEMBER_CARDTYPE_MODFIY_LOG');
expect(dataGridState.latestProps?.tableName).toBe('D_MEMBER_CARDTYPE_MODFIY_LOG');
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'all-columns',
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(dataGridState.latestProps?.columnPinScope).toBeUndefined();
renderer!.unmount();
});
@@ -4593,7 +4646,6 @@ describe('QueryEditor external SQL save', () => {
async (dbType) => {
storeState.connections[0].config.type = dbType;
storeState.connections[0].config.database = dbType === 'oracle' || dbType === 'dameng' ? 'APP' : 'main';
const forceReadOnlyQueryResult = dbType === 'tdengine' || dbType === 'clickhouse';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['COUNT'], rows: [{ COUNT: 1 }] }],
@@ -4616,7 +4668,7 @@ describe('QueryEditor external SQL save', () => {
});
const expectedTableName = dbType === 'oracle' || dbType === 'dameng' ? 'USERS' : 'users';
expect(dataGridState.latestProps?.tableName).toBe(forceReadOnlyQueryResult ? undefined : expectedTableName);
expect(dataGridState.latestProps?.tableName).toBe(expectedTableName);
expect(dataGridState.latestProps?.editLocator).toBeUndefined();
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(backendApp.DBGetColumns).not.toHaveBeenCalled();