Files
MyGoNavi/frontend/src/components/DataViewer.primary-key.test.tsx
Syngnat 5beb4c3ce4 feat(data-grid): 无主键表支持全列匹配编辑并移除只读限制
- 新增 all-columns 全列匹配行定位策略,无主键/唯一索引时不再强制只读
- 元数据加载失败、索引查询失败等误报场景全部降级为全列匹配编辑
- 修复 resolveQueryLocatorPlan 同步解析异常导致 SQL 结果页签不出现的问题
- 行安全由后端影响行数校验兜底,非单行命中即回滚整个事务
- 同步更新六语言文案与相关测试用例
2026-07-05 16:00:25 +08:00

669 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { readFileSync } from 'node:fs';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { TabData } from '../types';
import { DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
import DataViewer from './DataViewer';
const storeState = vi.hoisted(() => ({
connections: [
{
id: 'conn-1',
name: 'oracle',
config: {
type: 'oracle',
host: '127.0.0.1',
port: 1521,
user: 'scott',
password: '',
database: 'ORCLPDB1',
},
},
],
languagePreference: 'zh-CN',
addSqlLog: vi.fn(),
}));
const backendApp = vi.hoisted(() => ({
DBQuery: vi.fn(),
DBGetColumns: vi.fn(),
DBGetIndexes: vi.fn(),
}));
const messageApi = vi.hoisted(() => ({
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
}));
const dataGridState = vi.hoisted(() => ({
latestProps: null as any,
}));
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('antd', () => ({
message: messageApi,
}));
vi.mock('./DataGrid', () => ({
default: (props: any) => {
dataGridState.latestProps = props;
return <div data-grid="true" />;
},
GONAVI_ROW_KEY: '__gonavi_row_key__',
}));
const createTab = (overrides: Partial<TabData> = {}): TabData => ({
id: 'tab-1',
title: 'EDC_LOG',
type: 'table',
connectionId: 'conn-1',
dbName: 'MYCIMLED',
tableName: 'EDC_LOG',
...overrides,
});
const flushPromises = async () => {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
};
const createRows = (count: number) => Array.from({ length: count }, (_, i) => ({
ID: i + 1,
NAME: `row-${i + 1}`,
}));
describe('DataViewer safe editing locator', () => {
it('memoizes the table data viewer so parent-only modal state does not repaint loaded data', () => {
const source = readFileSync(new URL('./DataViewer.tsx', import.meta.url), 'utf8');
expect(source).toContain('React.memo(({ tab, isActive = true }) => {');
});
const renderAndReload = async (tab: TabData = createTab()) => {
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DataViewer tab={tab} />);
});
await act(async () => {
await dataGridState.latestProps.onReload();
});
await flushPromises();
return renderer!;
};
beforeEach(() => {
vi.clearAllMocks();
dataGridState.latestProps = null;
storeState.connections = [
{
id: 'conn-1',
name: 'oracle',
config: {
type: 'oracle',
host: '127.0.0.1',
port: 1521,
user: 'scott',
password: '',
database: 'ORCLPDB1',
},
},
];
storeState.languagePreference = 'zh-CN';
storeState.connections[0].config.type = 'oracle';
storeState.connections[0].config.database = 'ORCLPDB1';
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['ID', 'NAME'],
data: [{ ID: 7, NAME: 'old-name' }],
});
backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] });
});
it('localizes the missing connection message through DataViewer catalog keys', async () => {
storeState.connections = [];
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DataViewer tab={createTab({ connectionId: 'missing-conn' })} />);
await Promise.resolve();
});
await flushPromises();
expect(messageApi.error).toHaveBeenCalledWith('未找到连接');
renderer!.unmount();
});
it('defers the initial table query when the table opens in embedded object design', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DataViewer tab={createTab({ initialViewMode: 'fields', initialViewModeRequestId: 'open-design-1' })} />);
});
await flushPromises();
expect(backendApp.DBQuery).not.toHaveBeenCalled();
await act(async () => {
dataGridState.latestProps?.onDataViewActivate?.();
});
await flushPromises();
expect(backendApp.DBQuery).toHaveBeenCalled();
renderer!.unmount();
});
it('keeps DataViewer message wrappers and SQL log phase labels keyed', () => {
const source = readFileSync(new URL('./DataViewer.tsx', import.meta.url), 'utf8');
expect(source).not.toMatch(/当前结果集尚未就绪|统计失败|统计总数失败|统计结果解析失败|Mongo 筛选条件无效|解析失败|主查询|复杂类型降级重试|重试\(32MB sort_buffer\)|重试\(128MB sort_buffer\)|已自动提升排序缓冲并重试成功|查询失败|查询超过连接超时时间|DuckDB 查询超过连接超时时间|超时|MongoDB 结果集中缺少 _id|加载索引失败|无法加载主键\/唯一索引元数据|无法加载唯一索引元数据|保持只读|当前结果没有可用的安全行定位方式/);
expect(source).toContain('data_viewer.message.connection_not_found');
expect(source).toContain('data_viewer.message.result_not_ready');
expect(source).toContain('data_viewer.message.query_failed');
expect(source).toContain('data_viewer.message.query_timeout');
expect(source).toContain('data_viewer.message.duckdb_query_timeout');
expect(source).toContain('data_viewer.read_only.reason.mongo_id_missing');
expect(source).toContain('data_viewer.read_only.reason.no_safe_locator');
expect(source).toContain('data_viewer.read_only.warning.table');
expect(source).toContain('data_viewer.read_only.warning.collection');
expect(source).toContain('data_viewer.sql_log.phase.main_query');
expect(source).toContain('data_viewer.sql_log.phase.sort_buffer_retry');
});
it('caps viewer filter snapshots so long-running sessions do not retain unbounded table state', () => {
const source = readFileSync(new URL('./DataViewer.tsx', import.meta.url), 'utf8');
expect(source).toContain('const MAX_VIEWER_FILTER_SNAPSHOTS = 64;');
expect(source).toContain('const trimViewerFilterSnapshots = () => {');
expect(source).toContain('setViewerFilterSnapshot(normalizedTabId, {');
});
it('enables table preview editing after primary keys are loaded', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ Name: 'ID', Key: 'PRI' }, { Name: 'NAME', Key: '' }],
});
const renderer = await renderAndReload();
expect(dataGridState.latestProps?.pkColumns).toEqual(['ID']);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'primary-key',
columns: ['ID'],
valueColumns: ['ID'],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
renderer.unmount();
});
it('enables table preview editing when primary key metadata uses boolean aliases', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ column_name: 'ID', isPrimary: true }, { column_name: 'NAME' }],
});
const renderer = await renderAndReload();
expect(dataGridState.latestProps?.pkColumns).toEqual(['ID']);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'primary-key',
columns: ['ID'],
valueColumns: ['ID'],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
renderer.unmount();
});
it('uses a unique index when the table has no primary key', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'EMAIL', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBGetIndexes.mockResolvedValue({
success: true,
data: [{ name: 'UK_EMAIL', columnName: 'EMAIL', nonUnique: 0, seqInIndex: 1, indexType: 'BTREE' }],
});
const renderer = await renderAndReload();
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'unique-key',
columns: ['EMAIL'],
valueColumns: ['EMAIL'],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
renderer.unmount();
});
it('keeps DuckDB table preview writable when unique index metadata arrives as a safe locator', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'slug', key: '' }, { name: 'name', key: '' }],
});
backendApp.DBGetIndexes.mockResolvedValue({
success: true,
data: [{ name: 'events_slug_key', columnName: 'slug', nonUnique: 0, seqInIndex: 1, indexType: 'UNIQUE' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-unique', dbName: 'main', tableName: 'main.events', title: 'events' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'unique-key',
columns: ['slug'],
valueColumns: ['slug'],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
renderer.unmount();
});
it('uses hidden DuckDB rowid when no primary or unique key is available', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'name', key: '' }],
});
backendApp.DBGetIndexes.mockResolvedValue({
success: true,
data: [],
});
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['name', DUCKDB_ROWID_LOCATOR_COLUMN],
data: [{ name: 'launch', [DUCKDB_ROWID_LOCATOR_COLUMN]: 17 }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-rowid', dbName: 'main', tableName: 'main.events', title: 'events' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'duckdb-rowid',
columns: ['rowid'],
valueColumns: [DUCKDB_ROWID_LOCATOR_COLUMN],
hiddenColumns: [DUCKDB_ROWID_LOCATOR_COLUMN],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
expect(backendApp.DBQuery.mock.calls.some((call: any[]) => String(call[2]).includes(`rowid AS "${DUCKDB_ROWID_LOCATOR_COLUMN}"`))).toBe(true);
renderer.unmount();
});
it('enables MongoDB table preview editing through the _id locator', async () => {
storeState.connections[0].config.type = 'mongodb';
storeState.connections[0].config.database = 'app';
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['_id', '__gonavi_mongodb_id_locator__', 'name', 'age'],
data: [{
_id: '507f1f77bcf86cd799439011',
__gonavi_mongodb_id_locator__: { $oid: '507f1f77bcf86cd799439011' },
name: 'old-name',
age: 18,
}],
});
const renderer = await renderAndReload(createTab({ id: 'tab-mongo', dbName: 'app', tableName: 'users', title: 'users' }));
expect(backendApp.DBGetColumns).not.toHaveBeenCalled();
expect(backendApp.DBGetIndexes).not.toHaveBeenCalled();
expect(dataGridState.latestProps?.pkColumns).toEqual(['_id']);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'primary-key',
columns: ['_id'],
valueColumns: ['__gonavi_mongodb_id_locator__'],
hiddenColumns: ['__gonavi_mongodb_id_locator__'],
writableColumns: {
name: 'name',
age: 'age',
},
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
const mongoFindCall = backendApp.DBQuery.mock.calls.find((call: any[]) => String(call[2] || '').includes('"find":"users"'));
expect(mongoFindCall).toBeTruthy();
expect(JSON.parse(String(mongoFindCall?.[2] || '{}'))).toMatchObject({
find: 'users',
sort: { _id: 1 },
__gonaviIncludeObjectIDLocator: true,
});
renderer.unmount();
});
it('keeps MongoDB results read-only when _id is missing', async () => {
storeState.languagePreference = 'en-US';
storeState.connections[0].config.type = 'mongodb';
storeState.connections[0].config.database = 'app';
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['name'],
data: [{ name: 'orphan-doc' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-mongo-no-id', dbName: 'app', tableName: 'users', title: 'users' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'none',
readOnly: true,
reason: 'MongoDB result set is missing _id, so changes cannot be submitted safely.',
});
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(messageApi.warning).toHaveBeenCalledWith('Collection app.users remains read-only: MongoDB result set is missing _id, so changes cannot be submitted safely.');
renderer.unmount();
});
it('uses hidden Oracle ROWID when no primary or unique key is available', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['ID', 'NAME', ORACLE_ROWID_LOCATOR_COLUMN],
data: [{ ID: 7, NAME: 'old-name', [ORACLE_ROWID_LOCATOR_COLUMN]: 'AAAA' }],
});
const renderer = await renderAndReload();
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'oracle-rowid',
columns: ['ROWID'],
valueColumns: [ORACLE_ROWID_LOCATOR_COLUMN],
hiddenColumns: [ORACLE_ROWID_LOCATOR_COLUMN],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
expect(backendApp.DBQuery.mock.calls.some((call: any[]) => String(call[2]).includes(`ROWID AS "${ORACLE_ROWID_LOCATOR_COLUMN}"`))).toBe(true);
renderer.unmount();
});
it('uses hidden OceanBase Oracle ROWID when no primary or unique key is available', async () => {
storeState.connections[0].config.type = 'oceanbase';
(storeState.connections[0].config as any).oceanBaseProtocol = 'oracle';
storeState.connections[0].config.user = 'dev';
storeState.connections[0].config.database = 'ORCLPDB1';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['ID', 'NAME', ORACLE_ROWID_LOCATOR_COLUMN],
data: [{ ID: 7, NAME: 'old-name', [ORACLE_ROWID_LOCATOR_COLUMN]: 'AAAA' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-ob-oracle-rowid', dbName: 'ORCLPDB1', tableName: 'EDC_LOG', title: 'EDC_LOG' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'oracle-rowid',
columns: ['ROWID'],
valueColumns: [ORACLE_ROWID_LOCATOR_COLUMN],
hiddenColumns: [ORACLE_ROWID_LOCATOR_COLUMN],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(dataGridState.latestProps?.showRowNumberColumn).toBe(true);
expect(messageApi.warning).not.toHaveBeenCalled();
expect(backendApp.DBQuery.mock.calls.some((call: any[]) => String(call[2]).includes(`ROWID AS "${ORACLE_ROWID_LOCATOR_COLUMN}"`))).toBe(true);
renderer.unmount();
});
it('does not add fallback ORDER BY for DuckDB table preview when a primary key is available', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-order', dbName: 'main', tableName: 'events', title: 'events' }));
const tableQueries = backendApp.DBQuery.mock.calls
.map((call: any[]) => String(call[2] || ''))
.filter((sql: string) => sql.includes('FROM "events"'));
expect(tableQueries.length).toBeGreaterThan(0);
expect(tableQueries.every((sql: string) => !/\border\s+by\b/i.test(sql))).toBe(true);
expect(tableQueries[tableQueries.length - 1]).toContain('LIMIT 101 OFFSET 0');
renderer.unmount();
});
it('requeries table preview when a column header filter is applied', async () => {
storeState.connections[0].config.type = 'mysql';
storeState.connections[0].config.database = 'missav_bot';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'id', key: 'PRI' }, { name: 'code', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['id', 'code'],
data: [{ id: 2, code: 'EROFV-3551' }],
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DataViewer tab={createTab({ dbName: 'missav_bot', tableName: 'videos', title: 'videos' })} />);
});
await flushPromises();
backendApp.DBQuery.mockClear();
await act(async () => {
dataGridState.latestProps?.onApplyFilter([{
id: 1,
enabled: true,
logic: 'AND',
column: 'code',
op: 'CONTAINS',
value: '3551',
value2: '',
}]);
await Promise.resolve();
await Promise.resolve();
});
await flushPromises();
const filteredSelectSql = backendApp.DBQuery.mock.calls
.map((call: any[]) => String(call[2] || ''))
.find((sql: string) => /select\s+\*\s+from\s+`videos`/i.test(sql) && /where/i.test(sql));
expect(filteredSelectSql).toContain("`code` LIKE '%3551%'");
renderer!.unmount();
});
it('keeps DuckDB table preview writable when primary key metadata arrives for a qualified table name', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'id', key: 'PRI' }, { name: 'name', key: '' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-pri', dbName: 'main', tableName: 'main.events', title: 'events' }));
expect(dataGridState.latestProps?.pkColumns).toEqual(['id']);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'primary-key',
columns: ['id'],
valueColumns: ['id'],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
renderer.unmount();
});
it('invalidates a stale known total when table data grows after a manual refresh', async () => {
storeState.connections[0].config.type = 'mysql';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
});
let pageQueryCount = 0;
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
if (/count\s*\(/i.test(String(sql))) {
return {
success: true,
fields: ['total'],
data: [{ total: 500 }],
};
}
pageQueryCount += 1;
return {
success: true,
fields: ['ID', 'NAME'],
data: pageQueryCount === 1 ? createRows(100) : createRows(101),
};
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DataViewer tab={createTab({ dbName: 'main', tableName: 'users', title: 'users' })} />);
});
await flushPromises();
expect(dataGridState.latestProps?.pagination).toMatchObject({
total: 100,
totalKnown: true,
});
await act(async () => {
dataGridState.latestProps?.onReload();
await Promise.resolve();
await Promise.resolve();
});
await flushPromises();
expect(backendApp.DBQuery.mock.calls.some((call: any[]) => /count\s*\(/i.test(String(call[2] || '')))).toBe(true);
expect(dataGridState.latestProps?.pagination).toMatchObject({
total: 500,
totalKnown: true,
});
expect(dataGridState.latestProps?.data).toHaveLength(100);
renderer!.unmount();
});
it('shows an actionable message for DuckDB timeout interruption errors', async () => {
storeState.languagePreference = 'en-US';
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: false,
message: 'context deadline exceeded INTERRUPT Error: Interrupted!',
fields: [],
data: [],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-timeout', dbName: 'main', tableName: 'events', title: 'events' }));
expect(messageApi.error).toHaveBeenCalledWith('DuckDB query exceeded the connection timeout and was interrupted. Increase the connection timeout, or reduce the sort/filter scope and retry.');
expect(storeState.addSqlLog.mock.calls.some((call: any[]) => String(call[0]?.message || '').includes('context deadline exceeded'))).toBe(true);
renderer.unmount();
});
it.each([
[
'zh-TW',
'資料庫連線逾時mysql 127.0.0.1:3306/crm網路逾時',
'查詢超過連線逾時時間,已中斷。請調高連線逾時時間,或縮小查詢範圍後再試。',
],
[
'ja-JP',
'データベース接続がタイムアウトしました: mysql 127.0.0.1:3306/crm: ネットワークタイムアウト',
'クエリが接続タイムアウトを超えたため中断されました。接続タイムアウトを延長するか、クエリ範囲を絞って再試行してください。',
],
[
'de-DE',
'Zeitüberschreitung bei der Datenbankverbindung: mysql 127.0.0.1:3306/crm: Netzwerk-Timeout',
'Die Abfrage hat das Verbindungstimeout überschritten und wurde unterbrochen. Erhöhen Sie das Verbindungstimeout oder verkleinern Sie den Abfragebereich und versuchen Sie es erneut.',
],
[
'ru-RU',
'Тайм-аут подключения к базе данных: mysql 127.0.0.1:3306/crm: тайм-аут сети',
'Запрос превысил тайм-аут подключения и был прерван. Увеличьте тайм-аут подключения или сократите область запроса и повторите попытку.',
],
])('maps localized connection-timeout wrappers to viewer timeout copy for %s', async (locale, backendMessage, expectedMessage) => {
storeState.languagePreference = locale;
storeState.connections[0].config.type = 'mysql';
storeState.connections[0].config.database = 'crm';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: false,
message: backendMessage,
fields: [],
data: [],
});
const renderer = await renderAndReload(createTab({ id: `tab-timeout-${locale}`, dbName: 'crm', tableName: 'orders', title: 'orders' }));
expect(messageApi.error).toHaveBeenCalledWith(expectedMessage);
expect(storeState.addSqlLog.mock.calls.some((call: any[]) => String(call[0]?.message || '').includes(backendMessage))).toBe(true);
renderer.unmount();
});
it('falls back to all-columns editing when no safe locator exists', async () => {
storeState.languagePreference = 'en-US';
storeState.connections[0].config.type = 'mysql';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
const renderer = await renderAndReload(createTab({ dbName: 'main', tableName: 'users', title: 'users' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'all-columns',
columns: ['ID', 'NAME'],
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(false);
expect(messageApi.info).toHaveBeenCalledWith('No primary key or unique index was detected, so rows will be located by matching all columns. Edit with care.');
renderer.unmount();
});
});