mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
- 表导入与数据库导入统一提供遇错停止和继续策略,并跟随主题变量 - 增加能力校验、高级解析选项、字节进度、停止状态与持久任务历史 - 防止重复启动和运行中面板卸载,完善六语言语句统计及回归测试
1351 lines
46 KiB
TypeScript
1351 lines
46 KiB
TypeScript
import React from 'react';
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import DataImportWorkbench from './DataImportWorkbench';
|
|
import { DEFAULT_DATA_IMPORT_PREFERENCES } from './dataImportPreferences';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
dataImportCapability: vi.fn(),
|
|
dbGetDatabases: vi.fn(),
|
|
dbGetTables: vi.fn(),
|
|
importData: vi.fn(),
|
|
selectSQLFileForExecution: vi.fn(),
|
|
messageError: vi.fn(),
|
|
messageSuccess: vi.fn(),
|
|
addTab: vi.fn(),
|
|
storeState: {
|
|
theme: 'light',
|
|
connections: [] as any[],
|
|
addTab: (...args: any[]) => mocks.addTab(...args),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../store', () => ({
|
|
useStore: (selector: (state: typeof mocks.storeState) => unknown) => selector(mocks.storeState),
|
|
}));
|
|
|
|
vi.mock('../../wailsjs/go/app/App', () => ({
|
|
DataImportCapability: mocks.dataImportCapability,
|
|
DBGetDatabases: mocks.dbGetDatabases,
|
|
DBGetTables: mocks.dbGetTables,
|
|
ImportData: mocks.importData,
|
|
SelectSQLFileForExecution: mocks.selectSQLFileForExecution,
|
|
}));
|
|
|
|
vi.mock('./DatabaseImportExecutionPanel', () => ({
|
|
default: (props: Record<string, unknown>) => {
|
|
const [runnerStatus, setRunnerStatus] = React.useState('idle');
|
|
return React.createElement(
|
|
'mock-database-import-execution-panel',
|
|
{
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
'data-mock-runner-status': runnerStatus,
|
|
onMockRunnerStatusChange: setRunnerStatus,
|
|
...props,
|
|
},
|
|
);
|
|
},
|
|
}));
|
|
|
|
vi.mock('./ImportPreviewModal', () => ({
|
|
default: (props: Record<string, unknown>) => React.createElement(
|
|
'mock-import-preview',
|
|
{ 'data-import-preview-mock': 'true', ...props },
|
|
),
|
|
}));
|
|
|
|
vi.mock('./ImportJobHistoryPanel', () => ({
|
|
default: (props: Record<string, unknown>) => React.createElement(
|
|
'mock-import-job-history',
|
|
{ 'data-import-job-history-mock': 'true', ...props },
|
|
),
|
|
}));
|
|
|
|
vi.mock('antd', async () => {
|
|
const React = await import('react');
|
|
const Select = (props: Record<string, unknown>) => React.createElement('mock-select', props);
|
|
const Segmented = (props: Record<string, unknown>) => React.createElement('mock-segmented', props);
|
|
const Button = ({ children, ...props }: any) => <button {...props}>{children}</button>;
|
|
const Checkbox = ({ children, ...props }: any) => React.createElement('mock-checkbox', props, children);
|
|
const Alert = (props: Record<string, unknown>) => React.createElement('mock-alert', props);
|
|
const Empty = ({ description, ...props }: any) => React.createElement('mock-empty', props, description);
|
|
Empty.PRESENTED_IMAGE_SIMPLE = 'simple';
|
|
const Text = ({ children, ...props }: any) => <span {...props}>{children}</span>;
|
|
const Title = ({ children, ...props }: any) => <h2 {...props}>{children}</h2>;
|
|
return {
|
|
Alert,
|
|
Button,
|
|
Checkbox,
|
|
Empty,
|
|
Segmented,
|
|
Select,
|
|
Typography: { Text, Title },
|
|
message: {
|
|
error: mocks.messageError,
|
|
success: mocks.messageSuccess,
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock('@ant-design/icons', () => ({
|
|
DatabaseOutlined: () => React.createElement('mock-icon', { 'data-icon': 'database' }),
|
|
FileAddOutlined: () => React.createElement('mock-icon', { 'data-icon': 'file-add' }),
|
|
ImportOutlined: () => React.createElement('mock-icon', { 'data-icon': 'import' }),
|
|
TableOutlined: () => React.createElement('mock-icon', { 'data-icon': 'table' }),
|
|
}));
|
|
|
|
const createTab = (overrides: Record<string, unknown> = {}) => ({
|
|
id: 'data-import-workbench',
|
|
title: 'Data import',
|
|
type: 'data-import',
|
|
connectionId: 'conn-1',
|
|
dbName: 'app',
|
|
tableName: 'users',
|
|
...overrides,
|
|
} as any);
|
|
|
|
class MemoryStorage implements Storage {
|
|
private readonly values = new Map<string, string>();
|
|
|
|
get length() { return this.values.size; }
|
|
clear() { this.values.clear(); }
|
|
getItem(key: string) { return this.values.get(key) ?? null; }
|
|
key(index: number) { return Array.from(this.values.keys())[index] ?? null; }
|
|
removeItem(key: string) { this.values.delete(key); }
|
|
setItem(key: string, value: string) { this.values.set(key, value); }
|
|
}
|
|
|
|
const createImportCapability = (
|
|
tableSupported = true,
|
|
tableReason = '',
|
|
sqlFileSupported = true,
|
|
sqlFileReason = '',
|
|
) => ({
|
|
databaseType: 'mysql',
|
|
tableImport: {
|
|
supported: tableSupported,
|
|
reason: tableReason,
|
|
requiresPinnedSession: false,
|
|
supportsTransactionalBatch: tableSupported,
|
|
supportsContinue: tableSupported,
|
|
supportedConflictPolicies: tableSupported ? ['stop', 'skip_duplicates', 'upsert'] : [],
|
|
supportedFormats: tableSupported ? ['csv', 'json', 'xlsx'] : [],
|
|
supportedEncodings: tableSupported ? ['utf-8'] : [],
|
|
supportedCompressions: [],
|
|
supportedClientDirectives: [],
|
|
},
|
|
sqlFileImport: {
|
|
supported: sqlFileSupported,
|
|
reason: sqlFileReason,
|
|
requiresPinnedSession: true,
|
|
supportsTransactionalBatch: sqlFileSupported,
|
|
supportsContinue: sqlFileSupported,
|
|
supportedConflictPolicies: [],
|
|
supportedFormats: sqlFileSupported ? ['sql'] : [],
|
|
supportedEncodings: sqlFileSupported ? ['utf-8', 'utf-16le', 'utf-16be'] : [],
|
|
supportedCompressions: sqlFileSupported ? ['gzip'] : [],
|
|
supportedClientDirectives: sqlFileSupported ? ['delimiter'] : [],
|
|
},
|
|
});
|
|
|
|
const renderWorkbench = async (overrides: Record<string, unknown> = {}) => {
|
|
let renderer!: ReactTestRenderer;
|
|
await act(async () => {
|
|
renderer = create(<DataImportWorkbench tab={createTab(overrides)} />);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
return renderer;
|
|
};
|
|
|
|
describe('DataImportWorkbench', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('localStorage', new MemoryStorage());
|
|
mocks.storeState.theme = 'light';
|
|
mocks.storeState.connections = [
|
|
{
|
|
id: 'conn-1',
|
|
name: 'Primary MySQL',
|
|
config: {
|
|
type: 'mysql',
|
|
host: 'localhost',
|
|
port: 3306,
|
|
user: 'root',
|
|
database: 'app',
|
|
},
|
|
includeDatabases: ['app'],
|
|
},
|
|
{
|
|
id: 'redis-1',
|
|
name: 'Redis',
|
|
config: { type: 'redis', host: 'localhost', port: 6379 },
|
|
},
|
|
{
|
|
id: 'mongo-1',
|
|
name: 'MongoDB',
|
|
config: { type: 'mongodb', host: 'localhost', port: 27017 },
|
|
},
|
|
{
|
|
id: 'protected-1',
|
|
name: 'Protected PostgreSQL',
|
|
config: {
|
|
type: 'postgres',
|
|
host: 'localhost',
|
|
port: 5432,
|
|
protection: { restrictDataImport: true },
|
|
},
|
|
},
|
|
];
|
|
mocks.dbGetDatabases.mockReset();
|
|
mocks.dbGetDatabases.mockResolvedValue({
|
|
success: true,
|
|
data: [{ Database: 'app' }, { Database: 'hidden' }],
|
|
});
|
|
mocks.dbGetTables.mockReset();
|
|
mocks.dbGetTables.mockResolvedValue({
|
|
success: true,
|
|
data: [{ Tables_in_app: 'users' }, { Tables_in_app: 'orders' }],
|
|
});
|
|
mocks.importData.mockReset();
|
|
mocks.importData.mockResolvedValue({
|
|
success: true,
|
|
data: { filePath: '/tmp/users.csv' },
|
|
});
|
|
mocks.selectSQLFileForExecution.mockReset();
|
|
mocks.selectSQLFileForExecution.mockResolvedValue({
|
|
success: true,
|
|
data: { filePath: '/tmp/full-backup.sql', fileSizeMB: '1.25' },
|
|
});
|
|
mocks.messageError.mockReset();
|
|
mocks.messageSuccess.mockReset();
|
|
mocks.addTab.mockReset();
|
|
mocks.dataImportCapability.mockReset();
|
|
mocks.dataImportCapability.mockResolvedValue(createImportCapability());
|
|
});
|
|
|
|
it('persists independent table and database policies and passes table parser options to preview', async () => {
|
|
const renderer = await renderWorkbench();
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.onChange({ target: { checked: true } });
|
|
renderer.root.findByProps({
|
|
'data-import-option-encoding': 'true',
|
|
}).props.onChange('gb18030');
|
|
renderer.root.findByProps({
|
|
'data-import-option-delimiter': 'true',
|
|
}).props.onChange('tab');
|
|
renderer.root.findByProps({
|
|
'data-import-option-header-row': 'true',
|
|
}).props.onChange({ target: { value: '3' } });
|
|
renderer.root.findByProps({
|
|
'data-import-option-null-token': 'true',
|
|
}).props.onChange({ target: { value: '\\N' } });
|
|
renderer.root.findByProps({
|
|
'data-import-option-empty-string-as-null': 'true',
|
|
}).props.onChange({ target: { checked: true } });
|
|
renderer.root.findByProps({
|
|
'data-import-option-sheet-name': 'true',
|
|
}).props.onChange({ target: { value: 'Sheet2' } });
|
|
renderer.root.findByProps({
|
|
'data-import-option-conflict-policy': 'true',
|
|
}).props.onChange('upsert');
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-option-conflict-keys': 'true',
|
|
}).props.onChange({ target: { value: 'id, tenant_id, id' } });
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-preview-mock': 'true',
|
|
}).props.importOptions).toMatchObject({
|
|
continueOnError: true,
|
|
encoding: 'gb18030',
|
|
delimiter: 'tab',
|
|
headerRow: 3,
|
|
nullToken: '\\N',
|
|
emptyStringAsNull: true,
|
|
sheetName: 'Sheet2',
|
|
conflictPolicy: 'upsert',
|
|
conflictKeyColumns: ['id', 'tenant_id'],
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer.unmount();
|
|
});
|
|
const restored = await renderWorkbench();
|
|
expect(restored.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.checked).toBe(true);
|
|
expect(restored.root.findByProps({
|
|
'data-import-option-encoding': 'true',
|
|
}).props.value).toBe('gb18030');
|
|
|
|
await act(async () => {
|
|
restored.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
}).props.onChange('database');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(restored.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.checked).toBe(false);
|
|
expect(restored.root.findAllByProps({
|
|
'data-import-advanced-options': 'true',
|
|
})).toHaveLength(0);
|
|
});
|
|
|
|
it('keeps the controlled conflict key input aligned with normalized submitted columns', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const overlongColumn = 'x'.repeat(300);
|
|
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-option-conflict-policy': 'true',
|
|
}).props.onChange('upsert');
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-option-conflict-keys': 'true',
|
|
}).props.onChange({ target: { value: ` id, ${overlongColumn}, ID ` } });
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-option-conflict-keys': 'true',
|
|
}).props.value).toBe(`id, ${'x'.repeat(255)}`);
|
|
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-preview-mock': 'true',
|
|
}).props.importOptions.conflictKeyColumns).toEqual(['id', 'x'.repeat(255)]);
|
|
});
|
|
|
|
it('renders import job history without exposing a resume action', async () => {
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-job-history-mock': 'true',
|
|
})).toBeDefined();
|
|
expect(renderer.root.findAllByProps({
|
|
'data-import-history-resume-action': true,
|
|
})).toHaveLength(0);
|
|
});
|
|
|
|
it('refreshes durable history when an import starts and when it finishes', async () => {
|
|
const renderer = await renderWorkbench();
|
|
expect(renderer.root.findByProps({
|
|
'data-import-job-history-mock': 'true',
|
|
}).props.refreshToken).toBe(0);
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
const preview = renderer.root.findByProps({ 'data-import-preview-mock': 'true' });
|
|
|
|
await act(async () => {
|
|
preview.props.onImportingChange(true);
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-job-history-mock': 'true',
|
|
}).props.refreshToken).toBe(1);
|
|
|
|
await act(async () => {
|
|
preview.props.onImportingChange(false);
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-job-history-mock': 'true',
|
|
}).props.refreshToken).toBe(2);
|
|
});
|
|
|
|
it('fails closed when a persisted conflict policy is unsupported by the backend capability', async () => {
|
|
globalThis.localStorage.setItem(
|
|
'gonavi:data-import-preferences:v1:table',
|
|
JSON.stringify({
|
|
...DEFAULT_DATA_IMPORT_PREFERENCES,
|
|
conflictPolicy: 'upsert',
|
|
conflictKeyColumns: ['id'],
|
|
}),
|
|
);
|
|
const capability = createImportCapability();
|
|
capability.tableImport.supportedConflictPolicies = ['stop'];
|
|
mocks.dataImportCapability.mockResolvedValue(capability);
|
|
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-conflict-policy-error': 'unsupported',
|
|
})).toBeDefined();
|
|
const policyOptions = renderer.root.findByProps({
|
|
'data-import-option-conflict-policy': 'true',
|
|
}).props.options;
|
|
expect(policyOptions.find((option: any) => option.value === 'upsert').disabled).toBe(true);
|
|
});
|
|
|
|
it('loads the backend capability for the selected connection before enabling file selection', async () => {
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(mocks.dataImportCapability).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: 'mysql' }),
|
|
);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(false);
|
|
});
|
|
|
|
it('shows the file formats, encodings, compression and client directives supported by the backend', async () => {
|
|
const renderer = await renderWorkbench();
|
|
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
}).props.onChange('database');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-detail': 'formats',
|
|
}).props.children.join('')).toContain('sql');
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-detail': 'encodings',
|
|
}).props.children.join('')).toContain('utf-16le');
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-detail': 'compressions',
|
|
}).props.children.join('')).toContain('gzip');
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-detail': 'directives',
|
|
}).props.children.join('')).toContain('delimiter');
|
|
});
|
|
|
|
it('blocks the current mode and shows the backend reason when it is unsupported', async () => {
|
|
mocks.dataImportCapability.mockResolvedValueOnce(
|
|
createImportCapability(false, 'table_import_runtime_unavailable'),
|
|
);
|
|
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('table_import_runtime_unavailable');
|
|
});
|
|
|
|
it('fails closed and shows a recoverable reason when the capability RPC fails', async () => {
|
|
mocks.dataImportCapability.mockRejectedValueOnce(new Error('backend offline'));
|
|
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('rpc_failed');
|
|
});
|
|
|
|
it('retries a failed capability request while keeping import actions closed', async () => {
|
|
let resolveRetry!: (value: ReturnType<typeof createImportCapability>) => void;
|
|
mocks.dataImportCapability
|
|
.mockRejectedValueOnce(new Error('backend offline'))
|
|
.mockReturnValueOnce(new Promise((resolve) => {
|
|
resolveRetry = resolve;
|
|
}));
|
|
|
|
const renderer = await renderWorkbench();
|
|
const capabilityAlert = renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
});
|
|
const retryAction = capabilityAlert.props.action;
|
|
|
|
expect(retryAction.props['data-import-capability-retry']).toBe('true');
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
|
|
await act(async () => {
|
|
retryAction.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(mocks.dataImportCapability).toHaveBeenCalledTimes(2);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('loading');
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
|
|
await act(async () => {
|
|
resolveRetry(createImportCapability());
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findAllByProps({
|
|
'data-import-capability-alert': 'true',
|
|
})).toHaveLength(0);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(false);
|
|
});
|
|
|
|
it('fails closed when the capability binding throws synchronously', async () => {
|
|
mocks.dataImportCapability.mockImplementationOnce(() => {
|
|
throw new Error('binding unavailable');
|
|
});
|
|
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('rpc_failed');
|
|
});
|
|
|
|
it('keeps file selection closed while the capability is loading', async () => {
|
|
mocks.dataImportCapability.mockReturnValueOnce(new Promise(() => {}));
|
|
|
|
const renderer = await renderWorkbench();
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('loading');
|
|
});
|
|
|
|
it('ignores a stale capability response after the selected connection changes', async () => {
|
|
mocks.storeState.connections.push({
|
|
id: 'conn-2',
|
|
name: 'Analytics PostgreSQL',
|
|
config: {
|
|
type: 'postgres',
|
|
host: 'localhost',
|
|
port: 5432,
|
|
user: 'postgres',
|
|
database: 'app',
|
|
},
|
|
includeDatabases: ['app'],
|
|
});
|
|
let resolveFirst!: (value: unknown) => void;
|
|
mocks.dataImportCapability
|
|
.mockReturnValueOnce(new Promise((resolve) => {
|
|
resolveFirst = resolve;
|
|
}))
|
|
.mockResolvedValueOnce(
|
|
createImportCapability(false, 'table_import_runtime_unavailable'),
|
|
);
|
|
const renderer = await renderWorkbench();
|
|
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
}).props.onChange('conn-2');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('table_import_runtime_unavailable');
|
|
|
|
await act(async () => {
|
|
resolveFirst(createImportCapability());
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
}).props.value).toBe('conn-2');
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('table_import_runtime_unavailable');
|
|
});
|
|
|
|
it('does not pre-filter database imports with the SQL export capability heuristic', async () => {
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-capability-filter',
|
|
tableName: undefined,
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
}).props.options.map((option: any) => option.value)).toEqual([
|
|
'conn-1',
|
|
'redis-1',
|
|
'mongo-1',
|
|
]);
|
|
});
|
|
|
|
it('uses the SQL-file capability when database import mode is active', async () => {
|
|
mocks.dataImportCapability.mockResolvedValueOnce(
|
|
createImportCapability(true, '', false, 'pinned_session_unavailable'),
|
|
);
|
|
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-sql-capability',
|
|
tableName: undefined,
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-capability-alert': 'true',
|
|
}).props['data-import-capability-reason']).toBe('pinned_session_unavailable');
|
|
});
|
|
|
|
it('uses the shared theme tokens for the workbench surfaces', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const workbench = renderer.root.findByProps({
|
|
'data-data-import-workbench': 'true',
|
|
});
|
|
const header = renderer.root.findByType('header');
|
|
const target = renderer.root.findByProps({
|
|
'data-data-import-target-config': 'true',
|
|
});
|
|
const preview = renderer.root.findByProps({
|
|
'data-data-import-preview-panel': 'true',
|
|
});
|
|
|
|
expect(workbench.props.style.background).toContain('var(--gn-bg-panel-2');
|
|
expect(header.props.style.background).toContain('var(--gn-bg-panel');
|
|
expect(header.props.style.borderBottom).toContain('var(--gn-br-1');
|
|
expect(target.props.style.background).toContain('var(--gn-bg-panel');
|
|
expect(target.props.style.border).toContain('var(--gn-br-1');
|
|
expect(preview.props.style.background).toContain('var(--gn-bg-panel');
|
|
expect(preview.props.style.border).toContain('var(--gn-br-1');
|
|
});
|
|
|
|
it('shows the error policy in both import modes and carries it into execution', async () => {
|
|
const tableRenderer = await renderWorkbench();
|
|
const tableCheckbox = tableRenderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
});
|
|
expect(tableCheckbox.props).toMatchObject({ checked: false, disabled: false });
|
|
|
|
await act(async () => {
|
|
tableCheckbox.props.onChange({ target: { checked: true } });
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
tableRenderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
const preview = tableRenderer.root.findByProps({
|
|
'data-import-preview-mock': 'true',
|
|
});
|
|
expect(preview.props.continueOnError).toBe(true);
|
|
|
|
await act(async () => {
|
|
preview.props.onImportingChange(true);
|
|
await Promise.resolve();
|
|
});
|
|
expect(tableRenderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.disabled).toBe(true);
|
|
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
const checkbox = renderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
});
|
|
expect(checkbox.props).toMatchObject({ checked: false, disabled: false });
|
|
|
|
await act(async () => {
|
|
checkbox.props.onChange({ target: { checked: true } });
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.checked).toBe(true);
|
|
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
const executionPanel = renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
});
|
|
expect(executionPanel.props.continueOnError).toBe(true);
|
|
|
|
await act(async () => {
|
|
executionPanel.props.onRunningChange(true);
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-continue-on-error': 'true',
|
|
}).props.disabled).toBe(true);
|
|
});
|
|
|
|
it('filters non-relational and protected connections while loading the prefilled target', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const connectionSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
});
|
|
const databaseSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
});
|
|
const tableSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'table',
|
|
});
|
|
|
|
expect(connectionSelect.props.options.map((option: any) => option.value)).toEqual(['conn-1']);
|
|
expect(databaseSelect.props.options.map((option: any) => option.value)).toEqual(['app']);
|
|
expect(tableSelect.props.options.map((option: any) => option.value)).toEqual(['orders', 'users']);
|
|
expect(mocks.dbGetDatabases).toHaveBeenCalledTimes(1);
|
|
expect(mocks.dbGetTables).toHaveBeenCalledWith(expect.anything(), 'app');
|
|
});
|
|
|
|
it('applies database include and exclude patterns to import targets', async () => {
|
|
mocks.storeState.connections[0] = {
|
|
...mocks.storeState.connections[0],
|
|
includeDatabases: undefined,
|
|
includeDatabasePatterns: ['team_*'],
|
|
excludeDatabasePatterns: ['*_archive'],
|
|
};
|
|
mocks.dbGetDatabases.mockResolvedValue({
|
|
success: true,
|
|
data: [
|
|
{ Database: 'team_app' },
|
|
{ Database: 'team_archive' },
|
|
{ Database: 'other_app' },
|
|
],
|
|
});
|
|
|
|
const renderer = await renderWorkbench({ dbName: undefined, tableName: undefined });
|
|
const databaseSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
});
|
|
|
|
expect(databaseSelect.props.options.map((option: any) => option.value)).toEqual(['team_app']);
|
|
});
|
|
|
|
it('invalidates a hidden import target while the filtered database list is reloading', async () => {
|
|
let resolveReload!: (result: any) => void;
|
|
mocks.dbGetDatabases
|
|
.mockResolvedValueOnce({ success: true, data: [{ Database: 'app' }] })
|
|
.mockReturnValueOnce(new Promise((resolve) => {
|
|
resolveReload = resolve;
|
|
}));
|
|
|
|
const renderer = await renderWorkbench();
|
|
mocks.storeState.connections = [{
|
|
...mocks.storeState.connections[0],
|
|
excludeDatabasePatterns: ['app'],
|
|
}];
|
|
|
|
await act(async () => {
|
|
renderer.update(<DataImportWorkbench tab={createTab()} />);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
expect(selectFileButton.props.disabled).toBe(true);
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
}).props.value).toBeUndefined();
|
|
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
});
|
|
expect(mocks.importData).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
resolveReload({ success: true, data: [{ Database: 'app' }] });
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
}).props.options).toEqual([]);
|
|
});
|
|
|
|
it('keeps every target selector within the import target card', async () => {
|
|
const renderer = await renderWorkbench();
|
|
|
|
['connection', 'database', 'table'].forEach((field) => {
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': field,
|
|
}).props.style).toEqual({
|
|
width: '100%',
|
|
minWidth: 0,
|
|
maxWidth: '100%',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('filters every SQL import protection from database mode connections', async () => {
|
|
const primaryConnection = mocks.storeState.connections[0];
|
|
mocks.storeState.connections = [
|
|
primaryConnection,
|
|
{
|
|
id: 'data-import-protected',
|
|
name: 'Data import protected',
|
|
config: {
|
|
...primaryConnection.config,
|
|
protection: { restrictDataImport: true },
|
|
},
|
|
},
|
|
{
|
|
id: 'structure-protected',
|
|
name: 'Structure protected',
|
|
config: {
|
|
...primaryConnection.config,
|
|
protection: { restrictStructureEdit: true },
|
|
},
|
|
},
|
|
{
|
|
id: 'script-protected',
|
|
name: 'Script protected',
|
|
config: {
|
|
...primaryConnection.config,
|
|
protection: { restrictScriptExecution: true },
|
|
},
|
|
},
|
|
{
|
|
id: 'redis-1',
|
|
name: 'Redis',
|
|
config: { type: 'redis', host: 'localhost', port: 6379 },
|
|
},
|
|
];
|
|
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
const connectionSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
});
|
|
|
|
expect(connectionSelect.props.options.map((option: any) => option.value)).toEqual(['conn-1', 'redis-1']);
|
|
expect(renderer.root.findAllByProps({ 'data-import-target-field': 'table' })).toHaveLength(0);
|
|
expect(mocks.dbGetTables).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('syncs the automatic connection fallback back to the stable workbench tab', async () => {
|
|
const renderer = await renderWorkbench({
|
|
connectionId: '',
|
|
dbName: undefined,
|
|
tableName: undefined,
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
}).props.value).toBe('conn-1');
|
|
expect(mocks.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
|
id: 'data-import-workbench',
|
|
connectionId: 'conn-1',
|
|
dbName: undefined,
|
|
tableName: undefined,
|
|
dataImportRunning: false,
|
|
}));
|
|
});
|
|
|
|
it('selects a file for the target and embeds the shared preview workflow', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(mocks.importData).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: 'mysql' }),
|
|
'app',
|
|
'users',
|
|
);
|
|
const preview = renderer.root.findByProps({ 'data-import-preview-mock': 'true' });
|
|
expect(preview.props).toMatchObject({
|
|
visible: true,
|
|
presentation: 'embedded',
|
|
filePath: '/tmp/users.csv',
|
|
connectionId: 'conn-1',
|
|
dbName: 'app',
|
|
tableName: 'users',
|
|
});
|
|
|
|
const connectionSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'connection',
|
|
});
|
|
expect(connectionSelect.props.disabled).toBe(true);
|
|
|
|
await act(async () => {
|
|
preview.props.onImportingChange(true);
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.disabled).toBe(true);
|
|
});
|
|
|
|
it('selects a SQL file without running it and renders the database execution panel', async () => {
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
const modeSelector = renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
});
|
|
const databaseSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
});
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
|
|
expect(modeSelector.props.value).toBe('database');
|
|
expect(databaseSelect.props.allowClear).toBe(true);
|
|
expect(renderer.root.findAllByProps({ 'data-import-target-field': 'table' })).toHaveLength(0);
|
|
expect(mocks.dbGetTables).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(mocks.selectSQLFileForExecution).toHaveBeenCalledTimes(1);
|
|
expect(mocks.importData).not.toHaveBeenCalled();
|
|
expect(renderer.root.findAllByProps({ 'data-import-preview-mock': 'true' })).toHaveLength(0);
|
|
const executionPanel = renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
});
|
|
expect(executionPanel.props).toMatchObject({
|
|
dbName: 'app',
|
|
filePath: '/tmp/full-backup.sql',
|
|
fileSizeMB: '1.25',
|
|
darkMode: false,
|
|
});
|
|
expect(executionPanel.props.connectionConfig).toEqual(expect.objectContaining({ type: 'mysql' }));
|
|
});
|
|
|
|
it('resets the database execution state when a different SQL file is selected', async () => {
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const firstExecutionPanel = renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
});
|
|
await act(async () => {
|
|
firstExecutionPanel.props.onMockRunnerStatusChange('error');
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
}).props['data-mock-runner-status']).toBe('error');
|
|
|
|
mocks.selectSQLFileForExecution.mockResolvedValueOnce({
|
|
success: true,
|
|
data: { filePath: '/tmp/replacement.sql', fileSizeMB: '2.5' },
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const replacementExecutionPanel = renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
});
|
|
expect(replacementExecutionPanel.props).toMatchObject({
|
|
filePath: '/tmp/replacement.sql',
|
|
fileSizeMB: '2.5',
|
|
'data-mock-runner-status': 'idle',
|
|
});
|
|
});
|
|
|
|
it('allows selecting a database SQL file without a default database', async () => {
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
const databaseSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
});
|
|
|
|
await act(async () => {
|
|
databaseSelect.props.onChange(undefined);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
}).props.value).toBeUndefined();
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
expect(selectFileButton.props.disabled).toBe(false);
|
|
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
}).props.dbName).toBe('');
|
|
expect(mocks.dbGetTables).not.toHaveBeenCalled();
|
|
expect(mocks.importData).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears downstream target state when the database changes', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const databaseSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'database',
|
|
});
|
|
|
|
mocks.dbGetTables.mockResolvedValueOnce({
|
|
success: true,
|
|
data: [{ Tables_in_analytics: 'events' }],
|
|
});
|
|
await act(async () => {
|
|
databaseSelect.props.onChange('analytics');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const tableSelect = renderer.root.findByProps({
|
|
'data-import-target-field': 'table',
|
|
});
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
expect(tableSelect.props.value).toBeUndefined();
|
|
expect(selectFileButton.props.disabled).toBe(true);
|
|
expect(mocks.dbGetTables).toHaveBeenLastCalledWith(expect.anything(), 'analytics');
|
|
expect(mocks.addTab).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
id: 'data-import-workbench',
|
|
connectionId: 'conn-1',
|
|
dbName: 'analytics',
|
|
tableName: undefined,
|
|
}));
|
|
});
|
|
|
|
it('clears the selected table file and table target when switching to database mode', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findAllByProps({ 'data-import-preview-mock': 'true' })).toHaveLength(1);
|
|
|
|
const modeSelector = renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
});
|
|
expect(modeSelector.props.disabled).toBe(false);
|
|
await act(async () => {
|
|
modeSelector.props.onChange('database');
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
}).props.value).toBe('database');
|
|
expect(renderer.root.findAllByProps({ 'data-import-target-field': 'table' })).toHaveLength(0);
|
|
expect(renderer.root.findAllByProps({ title: '/tmp/users.csv' })).toHaveLength(0);
|
|
expect(renderer.root.findAllByProps({ 'data-import-preview-mock': 'true' })).toHaveLength(0);
|
|
expect(renderer.root.findAllByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
})).toHaveLength(0);
|
|
expect(mocks.addTab).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
id: 'data-import-workbench',
|
|
dataImportMode: 'database',
|
|
tableName: undefined,
|
|
}));
|
|
});
|
|
|
|
it('ignores a pending table file selection result after switching modes', async () => {
|
|
let resolveTableFile!: (result: any) => void;
|
|
mocks.importData.mockReturnValueOnce(new Promise<any>((resolve) => {
|
|
resolveTableFile = resolve;
|
|
}));
|
|
const renderer = await renderWorkbench();
|
|
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
}).props.onChange('database');
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
resolveTableFile({ success: true, data: { filePath: '/tmp/stale-users.csv' } });
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
}).props.value).toBe('database');
|
|
expect(renderer.root.findAllByProps({ title: '/tmp/stale-users.csv' })).toHaveLength(0);
|
|
expect(renderer.root.findAllByProps({ 'data-import-preview-mock': 'true' })).toHaveLength(0);
|
|
expect(renderer.root.findAllByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
})).toHaveLength(0);
|
|
});
|
|
|
|
it('does not report the native file-picker cancellation as an error', async () => {
|
|
mocks.importData.mockResolvedValueOnce({ success: false, message: '已取消' });
|
|
const renderer = await renderWorkbench();
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
|
expect(renderer.root.findAllByProps({ 'data-import-preview-mock': 'true' })).toHaveLength(0);
|
|
});
|
|
|
|
it('resets a selected SQL file when the same target gets a new launch key', async () => {
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(renderer.root.findAllByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
})).toHaveLength(1);
|
|
|
|
await act(async () => {
|
|
renderer.update(<DataImportWorkbench tab={createTab({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-2',
|
|
tableName: undefined,
|
|
})} />);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findAllByProps({ title: '/tmp/full-backup.sql' })).toHaveLength(0);
|
|
expect(renderer.root.findAllByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
})).toHaveLength(0);
|
|
expect(mocks.selectSQLFileForExecution).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not replace a running database import target when the stable tab is reopened', async () => {
|
|
const renderer = await renderWorkbench({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-launch-1',
|
|
tableName: undefined,
|
|
});
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
const executionPanel = renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
});
|
|
await act(async () => {
|
|
executionPanel.props.onRunningChange(true);
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
renderer.update(<DataImportWorkbench tab={createTab({
|
|
dataImportMode: 'table',
|
|
dataImportLaunchKey: 'table-launch-2',
|
|
dbName: 'analytics',
|
|
tableName: 'events',
|
|
})} />);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-import-mode-selector': 'true',
|
|
}).props).toMatchObject({ value: 'database', disabled: true });
|
|
expect(renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
}).props).toMatchObject({
|
|
dbName: 'app',
|
|
filePath: '/tmp/full-backup.sql',
|
|
});
|
|
expect(mocks.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
|
id: 'data-import-workbench',
|
|
connectionId: 'conn-1',
|
|
dbName: 'app',
|
|
tableName: undefined,
|
|
dataImportMode: 'database',
|
|
dataImportRunning: true,
|
|
}));
|
|
});
|
|
|
|
it('keeps an active database import mounted while its connection record changes', async () => {
|
|
const tab = createTab({
|
|
dataImportMode: 'database',
|
|
dataImportLaunchKey: 'database-connection-refresh',
|
|
tableName: undefined,
|
|
});
|
|
const renderer = await renderWorkbench(tab);
|
|
await act(async () => {
|
|
renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
}).props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
const executionPanel = renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
});
|
|
await act(async () => {
|
|
executionPanel.props.onRunningChange(true);
|
|
await Promise.resolve();
|
|
});
|
|
const capabilityCallsBeforeRefresh = mocks.dataImportCapability.mock.calls.length;
|
|
const databaseCallsBeforeRefresh = mocks.dbGetDatabases.mock.calls.length;
|
|
mocks.dataImportCapability.mockImplementation(() => new Promise(() => undefined));
|
|
mocks.storeState.connections = mocks.storeState.connections.map((connection) => (
|
|
connection.id === 'conn-1'
|
|
? { ...connection, config: { ...connection.config, host: '127.0.0.1' } }
|
|
: connection
|
|
));
|
|
|
|
await act(async () => {
|
|
renderer.update(<DataImportWorkbench tab={tab} />);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(renderer.root.findByProps({
|
|
'data-database-import-execution-panel-mock': 'true',
|
|
}).props).toMatchObject({
|
|
dbName: 'app',
|
|
filePath: '/tmp/full-backup.sql',
|
|
});
|
|
expect(mocks.dataImportCapability).toHaveBeenCalledTimes(capabilityCallsBeforeRefresh);
|
|
expect(mocks.dbGetDatabases).toHaveBeenCalledTimes(databaseCallsBeforeRefresh);
|
|
});
|
|
|
|
it('does not replace an active import target when the stable tab is reopened', async () => {
|
|
const renderer = await renderWorkbench();
|
|
const selectFileButton = renderer.root.findByProps({
|
|
'data-import-select-file-action': 'true',
|
|
});
|
|
await act(async () => {
|
|
selectFileButton.props.onClick();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
const preview = renderer.root.findByProps({ 'data-import-preview-mock': 'true' });
|
|
await act(async () => {
|
|
preview.props.onImportingChange(true);
|
|
renderer.update(<DataImportWorkbench tab={createTab({ dbName: 'analytics', tableName: 'events' })} />);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(mocks.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
|
id: 'data-import-workbench',
|
|
connectionId: 'conn-1',
|
|
dbName: 'app',
|
|
tableName: 'users',
|
|
dataImportRunning: true,
|
|
}));
|
|
|
|
const activePreview = renderer.root.findByProps({ 'data-import-preview-mock': 'true' });
|
|
expect(activePreview.props.dbName).toBe('app');
|
|
expect(activePreview.props.tableName).toBe('users');
|
|
expect(activePreview.props.filePath).toBe('/tmp/users.csv');
|
|
});
|
|
});
|