mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 00:42:47 +08:00
🔥 remove(frontend-test): 删除源码文本断言型测试并补齐全局 i18n 校验
- 删除 105 个纯静态测试文件(6624 行):仅把源码当字符串断言、不执行被测代码 - 新增 testPolicy 守卫:禁止新增读源码文本的测试,131 个存量文件记入只减不增的基线 - 新增 i18n keyResolution:校验源码 t() 引用的 key 均可经完整解析链得到译文 - 新增 i18n catalogIntegrity:校验占位符跨语言一致、无空译文 - 补齐 6 个语言包缺失的 common.retry,修复审计面板重试按钮显示字面量 key Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { unzlibSync } from 'fflate';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
const brandIconsSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./brand/brandIcons.ts', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
const brandIconsDirectory = fileURLToPath(
|
||||
new globalThis.URL('../public/brand-icons/', import.meta.url),
|
||||
);
|
||||
const defaultTitlebarMark = fileURLToPath(
|
||||
new globalThis.URL('../public/brand-marks/02-database-search-transparent.png', import.meta.url),
|
||||
);
|
||||
|
||||
const readFirstPngPixelAlpha = (assetBase64: string): number => {
|
||||
const binary = globalThis.atob(assetBase64);
|
||||
const pngBytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
const idatChunks: Uint8Array[] = [];
|
||||
|
||||
for (let offset = 8; offset + 12 <= pngBytes.length;) {
|
||||
const chunkLength = new DataView(
|
||||
pngBytes.buffer,
|
||||
pngBytes.byteOffset + offset,
|
||||
4,
|
||||
).getUint32(0);
|
||||
const chunkType = String.fromCharCode(...pngBytes.slice(offset + 4, offset + 8));
|
||||
if (chunkType === 'IDAT') {
|
||||
idatChunks.push(pngBytes.slice(offset + 8, offset + 8 + chunkLength));
|
||||
}
|
||||
offset += chunkLength + 12;
|
||||
}
|
||||
|
||||
const compressedLength = idatChunks.reduce((total, chunk) => total + chunk.length, 0);
|
||||
const compressedData = new Uint8Array(compressedLength);
|
||||
let compressedOffset = 0;
|
||||
for (const chunk of idatChunks) {
|
||||
compressedData.set(chunk, compressedOffset);
|
||||
compressedOffset += chunk.length;
|
||||
}
|
||||
|
||||
const scanlines = unzlibSync(compressedData);
|
||||
return scanlines[4] ?? -1;
|
||||
};
|
||||
|
||||
describe('about brand lockup', () => {
|
||||
it('uses a transparent lockup without a tile background on the about page', () => {
|
||||
expect(appSource).toContain('resolveBrandAboutSrc');
|
||||
expect(appSource).toContain('src={resolveBrandAboutSrc(brandIconId)}');
|
||||
|
||||
const aboutLogoStart = appSource.indexOf('src={resolveBrandAboutSrc(brandIconId)}');
|
||||
const aboutLogoSnippet = appSource.slice(aboutLogoStart, aboutLogoStart + 640);
|
||||
expect(aboutLogoSnippet).toContain("background: 'transparent'");
|
||||
expect(aboutLogoSnippet).toContain("boxShadow: 'none'");
|
||||
});
|
||||
|
||||
it('uses the transparent compact mark without a forced titlebar tile', () => {
|
||||
expect(appSource).toContain('src={resolveBrandTitlebarSrc(brandIconId)}');
|
||||
|
||||
const titlebarLogoStart = appSource.indexOf('src={resolveBrandTitlebarSrc(brandIconId)}');
|
||||
const titlebarLogoSnippet = appSource.slice(titlebarLogoStart, titlebarLogoStart + 640);
|
||||
expect(titlebarLogoSnippet).toContain("background: 'transparent'");
|
||||
expect(readFileSync(defaultTitlebarMark, 'base64')).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps a tile asset and a transparent about lockup for every selectable mascot', () => {
|
||||
const iconAssetPaths = brandIconsSource.match(/iconPath: '\/brand-icons\/\d{2}-.+\.webp'/g) || [];
|
||||
const aboutAssetPaths = brandIconsSource.match(/aboutPath: '\/brand-icons\/\d{2}-.+-about\.png'/g) || [];
|
||||
expect(iconAssetPaths).toHaveLength(10);
|
||||
expect(aboutAssetPaths).toHaveLength(10);
|
||||
|
||||
for (const declaration of iconAssetPaths) {
|
||||
const assetPath = declaration.match(/'([^']+)'/)?.[1];
|
||||
expect(assetPath).toBeTruthy();
|
||||
expect(readFileSync(`${brandIconsDirectory}${assetPath?.replace('/brand-icons/', '')}`, 'base64')).not.toBe('');
|
||||
}
|
||||
|
||||
for (const declaration of aboutAssetPaths) {
|
||||
const assetPath = declaration.match(/'([^']+)'/)?.[1];
|
||||
expect(assetPath).toBeTruthy();
|
||||
const assetBase64 = readFileSync(
|
||||
`${brandIconsDirectory}${assetPath?.replace('/brand-icons/', '')}`,
|
||||
'base64',
|
||||
);
|
||||
expect(assetBase64).not.toBe('');
|
||||
expect(globalThis.atob(assetBase64).charCodeAt(25)).toBe(6);
|
||||
expect(readFirstPngPixelAlpha(assetBase64)).toBe(0);
|
||||
}
|
||||
|
||||
const webpFiles = readdirSync(brandIconsDirectory).filter((file) => file.endsWith('.webp'));
|
||||
expect(webpFiles).toHaveLength(10);
|
||||
const aboutPngFiles = readdirSync(brandIconsDirectory).filter((file) => file.endsWith('-about.png'));
|
||||
expect(aboutPngFiles).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const appSource = readFileSync(new URL('./App.tsx', import.meta.url), 'utf8');
|
||||
const modalSource = readFileSync(
|
||||
new URL('./components/common/ResizableDraggableModal.tsx', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const floatingResultSource = readFileSync(
|
||||
new URL('./components/FloatingQueryResultWindows.tsx', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('App close-tab shortcut routing', () => {
|
||||
it('tracks only explicit workspace, result, and blocked interaction scopes', () => {
|
||||
expect(appSource).toContain("const closeShortcutScopeRef = useRef<CloseShortcutScope>('workspace');");
|
||||
expect(appSource).toContain('resolveCloseShortcutScopeFromTarget(event.target)');
|
||||
expect(appSource).toContain("document.addEventListener('pointerdown', handleExplicitCloseShortcutScope, true);");
|
||||
expect(appSource).toContain("document.addEventListener('focusin', handleExplicitCloseShortcutScope, true);");
|
||||
expect(appSource).toContain('data-gonavi-close-shortcut-scope="workspace"');
|
||||
});
|
||||
|
||||
it('gives shortcut recording priority over every global action', () => {
|
||||
const recorderGuardIndex = appSource.indexOf('if (capturingShortcutAction) {');
|
||||
const closeDecisionIndex = appSource.indexOf('const closeDecision = resolveCloseShortcutKeydownDecision({');
|
||||
expect(recorderGuardIndex).toBeGreaterThan(-1);
|
||||
expect(closeDecisionIndex).toBeGreaterThan(recorderGuardIndex);
|
||||
expect(appSource).toContain('setGlobalShortcutCaptureActive(Boolean(capturingShortcutAction));');
|
||||
});
|
||||
|
||||
it('uses a single close decision before dispatching exactly one scoped command', () => {
|
||||
expect(appSource).toContain('interactionBlocked: isCloseShortcutInteractionBlocked(event.target, document)');
|
||||
expect(appSource).toContain("if (closeDecision.kind === 'consume') {");
|
||||
expect(appSource).toContain('event.stopImmediatePropagation();');
|
||||
expect(appSource).toContain("if (closeShortcutScopeRef.current === 'workspace') {");
|
||||
expect(appSource).toContain('dispatchCloseActiveWorkspaceTab();');
|
||||
expect(appSource).toContain("} else if (closeShortcutScopeRef.current === 'result') {");
|
||||
expect(appSource).toContain('const targetTabId = resolveDockedActiveTabId(');
|
||||
expect(appSource).toContain('const outcome = dispatchCloseActiveResultTab(targetTabId);');
|
||||
});
|
||||
|
||||
it('enters blocked synchronously when the log tab hides the result area', () => {
|
||||
const dispatchIndex = appSource.indexOf('const outcome = dispatchCloseActiveResultTab(targetTabId);');
|
||||
const hiddenIndex = appSource.indexOf("if (outcome === 'hidden') {", dispatchIndex);
|
||||
const blockedIndex = appSource.indexOf("closeShortcutScopeRef.current = 'blocked';", hiddenIndex);
|
||||
expect(dispatchIndex).toBeGreaterThan(-1);
|
||||
expect(hiddenIndex).toBeGreaterThan(dispatchIndex);
|
||||
expect(blockedIndex).toBeGreaterThan(hiddenIndex);
|
||||
});
|
||||
|
||||
it('does not let the close router steal a migrated shortcut from its prior owner', () => {
|
||||
expect(appSource).toContain("const delegatedAction = closeDecision.kind === 'delegate'");
|
||||
expect(appSource).toContain('if (delegatedAction && action !== delegatedAction) {');
|
||||
expect(appSource).toContain("if (action === 'closeActiveTab') {");
|
||||
});
|
||||
});
|
||||
|
||||
describe('close shortcut interaction guards', () => {
|
||||
it('marks active reusable modals as background blockers', () => {
|
||||
expect(modalSource).toContain("data-gonavi-close-shortcut-guard={active ? 'true' : undefined}");
|
||||
expect(modalSource).toContain("data-gonavi-close-shortcut-blocks-background={active ? 'true' : undefined}");
|
||||
expect(modalSource).toContain('data-gonavi-close-shortcut-blocks-background="true"');
|
||||
});
|
||||
|
||||
it('marks detached result windows as blocked without globally blocking their existence', () => {
|
||||
expect(floatingResultSource).toContain('data-gonavi-close-shortcut-guard="true"');
|
||||
expect(floatingResultSource).toContain('data-gonavi-close-shortcut-scope="blocked"');
|
||||
expect(floatingResultSource).not.toContain('data-gonavi-close-shortcut-blocks-background="true"');
|
||||
});
|
||||
});
|
||||
@@ -1,431 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const appCssSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.css', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const aiSettingsModalSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./components/AISettingsModal.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('settings center layout', () => {
|
||||
it('hosts settings and tools in one split navigation shell', () => {
|
||||
expect(appSource).toContain("type SettingsCenterGroupKey = 'preferences' | 'services' | ToolCenterGroupKey | 'about';");
|
||||
expect(appSource).toContain('type SettingsCenterPaneKey =');
|
||||
expect(appSource).toContain('| ToolCenterPaneKey');
|
||||
expect(appSource).toContain("const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState<SettingsCenterGroupKey>('preferences');");
|
||||
expect(appSource).toContain("const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState<SettingsCenterPaneState | null>(null);");
|
||||
expect(appSource).toContain('style={toolCenterModalWorkspaceStyle}');
|
||||
expect(appSource).toContain('style={toolCenterModalSplitStyle}');
|
||||
expect(appSource).toContain('style={toolCenterNavPanelStyle}');
|
||||
expect(appSource).toContain('style={toolCenterNavScrollStyle}');
|
||||
expect(appSource).toContain('style={toolCenterContentPanelStyle}');
|
||||
expect(appSource).toContain('style={activeSettingsCenterDetailPanelStyle}');
|
||||
expect(appSource).toContain('style={isActiveToolCenterPane ? toolCenterDetailBodyStyle : settingsCenterDetailBodyStyle}');
|
||||
expect(appSource).toContain('style={toolCenterScrollableListStyle}');
|
||||
expect(appSource).toContain("title: t('app.settings.group.preferences.title')");
|
||||
expect(appSource).toContain("title: t('app.settings.group.services.title')");
|
||||
expect(appSource).toContain("title: t('app.tools.group.config.title')");
|
||||
expect(appSource).toContain("title: t('app.tools.group.workflow.title')");
|
||||
expect(appSource).toContain("title: t('app.tools.group.workspace.title')");
|
||||
expect(appSource).toContain("title: t('app.settings.group.about.title')");
|
||||
expect(appSource).toContain('const combinedSettingsCenterGroups = [');
|
||||
expect(appSource).not.toContain('const [isToolsModalOpen');
|
||||
expect(appSource).not.toContain('{isToolsModalOpen &&');
|
||||
});
|
||||
|
||||
it('moves sidebar table metadata configuration into the settings center', () => {
|
||||
expect(appSource).toContain("key: 'sidebar-metadata'");
|
||||
expect(appSource).toContain("title: t('app.settings.sidebar_metadata.title')");
|
||||
expect(appSource).toContain("description: t('app.settings.sidebar_metadata.description')");
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('preferences', 'sidebar-metadata')");
|
||||
expect(appSource).toContain("setSidebarTableMetadataFieldSelected(");
|
||||
expect(appSource).toContain('DndContext');
|
||||
expect(appSource).toContain('SortableContext');
|
||||
expect(appSource).toContain('handleSidebarMetadataDragEnd');
|
||||
expect(appSource).toContain('sidebarTableMetadataFieldOrder');
|
||||
expect(appSource).toContain('data-sidebar-metadata-field={field}');
|
||||
expect(appSource).toContain("sidebarTableMetadataFields: DEFAULT_SIDEBAR_TABLE_METADATA_FIELDS");
|
||||
expect(appSource).toContain("t('sidebar.v2_table_group_menu.display_table_rows')");
|
||||
expect(appSource).not.toContain("setIsLanguageModalOpen(true)");
|
||||
});
|
||||
|
||||
it('removes redundant framing lines from settings detail pages', () => {
|
||||
const detailPanelStyleStart = appSource.indexOf('const activeSettingsCenterDetailPanelStyle');
|
||||
const detailPanelStyleEnd = appSource.indexOf('const settingsCenterDetailBodyStyle', detailPanelStyleStart);
|
||||
const detailPanelStyleSource = appSource.slice(detailPanelStyleStart, detailPanelStyleEnd);
|
||||
const detailShellStart = appSource.indexOf('style={activeSettingsCenterDetailPanelStyle}');
|
||||
const detailShellEnd = appSource.indexOf('className="gonavi-settings-center-entry"', detailShellStart);
|
||||
const detailShellSource = appSource.slice(detailShellStart, detailShellEnd);
|
||||
|
||||
expect(detailPanelStyleSource).toContain("borderBottom: 'none'");
|
||||
expect(detailShellSource).toContain('<div style={{ paddingBottom: 10 }}>');
|
||||
expect(detailShellSource).not.toContain('borderTop: `1px solid ${overlayTheme.divider}`');
|
||||
});
|
||||
|
||||
it('adds persistent sidebar object visibility controls to preferences', () => {
|
||||
expect(appSource).toContain("key: 'sidebar-objects'");
|
||||
expect(appSource).toContain("title: t('app.settings.sidebar_objects.title')");
|
||||
expect(appSource).toContain("description: t('app.settings.sidebar_objects.description')");
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('preferences', 'sidebar-objects')");
|
||||
expect(appSource).toContain("if (activeSettingsCenterPane.key === 'sidebar-objects')");
|
||||
expect(appSource).toContain('renderSidebarObjectVisibilitySettingsPane();');
|
||||
expect(appSource).toContain('sidebarHiddenObjectGroups');
|
||||
expect(appSource).toContain('SIDEBAR_OBJECT_GROUP_KEYS.filter((key) => key !== \'tables\')');
|
||||
});
|
||||
|
||||
it('adds browser auth management into the services settings group', () => {
|
||||
expect(appSource).toContain("key: 'web-auth' as const");
|
||||
expect(appSource).toContain("title: t('app.settings.entry.web_auth.title')");
|
||||
expect(appSource).toContain("description: t('app.settings.entry.web_auth.description')");
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('services', 'web-auth')");
|
||||
expect(appSource).toContain("<WebAuthSettingsPanel");
|
||||
});
|
||||
|
||||
it('adds global proxy connection testing controls', () => {
|
||||
expect(appSource).toContain("const DEFAULT_GLOBAL_PROXY_TEST_URL = 'https://api.github.com/';");
|
||||
expect(appSource).toContain('const [proxyTestUrl, setProxyTestUrl]');
|
||||
expect(appSource).toContain('const [proxyTesting, setProxyTesting]');
|
||||
expect(appSource).toContain('const [proxyTestResult, setProxyTestResult]');
|
||||
expect(appSource).toContain('handleTestGlobalProxyDraft');
|
||||
expect(appSource).toContain('TestGlobalProxyConnection');
|
||||
expect(appSource).toContain('https://github.com/Syngnat/GoNavi/releases/latest');
|
||||
expect(appSource).toContain("t('app.proxy.test.action')");
|
||||
expect(appSource).toContain("t('app.proxy.test.target_placeholder')");
|
||||
});
|
||||
|
||||
it('keeps log directory controls inside the data-root detail page', () => {
|
||||
const embeddedDataRootStart = appSource.indexOf("if (activeSettingsCenterPane.key === 'data-root')");
|
||||
const embeddedDataRootEnd = appSource.indexOf(
|
||||
"if (activeSettingsCenterPane.key === 'security-update')",
|
||||
embeddedDataRootStart,
|
||||
);
|
||||
const embeddedDataRootSource = appSource.slice(embeddedDataRootStart, embeddedDataRootEnd);
|
||||
const standaloneDataRootStart = appSource.indexOf('{isDataRootModalOpen && (');
|
||||
const standaloneDataRootEnd = appSource.indexOf(
|
||||
'<ConnectionPackagePasswordModal',
|
||||
standaloneDataRootStart,
|
||||
);
|
||||
const standaloneDataRootSource = appSource.slice(standaloneDataRootStart, standaloneDataRootEnd);
|
||||
const logDirectoryRendererStart = appSource.indexOf('const renderLogDirectorySettings = () => {');
|
||||
const logDirectoryRendererEnd = appSource.indexOf('\n const {', logDirectoryRendererStart);
|
||||
const logDirectoryRendererSource = appSource.slice(logDirectoryRendererStart, logDirectoryRendererEnd);
|
||||
|
||||
expect(embeddedDataRootStart).toBeGreaterThan(-1);
|
||||
expect(embeddedDataRootEnd).toBeGreaterThan(embeddedDataRootStart);
|
||||
expect(standaloneDataRootStart).toBeGreaterThan(-1);
|
||||
expect(standaloneDataRootEnd).toBeGreaterThan(standaloneDataRootStart);
|
||||
expect(appSource).toContain('ApplyLogDirectory');
|
||||
expect(appSource).toContain('OpenLogDirectory');
|
||||
expect(appSource).toContain('SelectLogDirectory');
|
||||
expect(appSource).toContain("const [selectedLogDirectoryPath, setSelectedLogDirectoryPath] = useState('');");
|
||||
expect(appSource).toContain('const directorySettingsApplying = dataRootApplying || logDirectoryApplying || savedQueryDirectoryApplying;');
|
||||
expect(appSource).toContain('const handleSelectLogDirectory = useCallback(async () => {');
|
||||
expect(appSource).toContain('const handleApplyLogDirectory = useCallback(async (useDefaultPath = false) => {');
|
||||
expect(appSource).toContain('const handleOpenLogDirectory = useCallback(async () => {');
|
||||
expect(appSource).toContain('const renderLogDirectorySettings = () => {');
|
||||
expect(appSource).toContain('data-log-directory-settings="true"');
|
||||
expect(appSource).toContain("dataRootInfo?.logDirectorySource === 'environment'");
|
||||
expect(appSource).toContain('dataRootInfo?.logDirectoryRestartRequired === true');
|
||||
expect(appSource).toContain("t('app.data_root.log_directory.environment_hint')");
|
||||
expect(appSource).toContain("t('app.data_root.log_directory.pending_restart')");
|
||||
expect(appSource).toContain("t('app.data_root.log_directory.restart_hint')");
|
||||
expect(logDirectoryRendererSource).not.toContain(
|
||||
"<div style={{ fontWeight: 600 }}>{t('app.data_root.log_directory.title')}</div>",
|
||||
);
|
||||
expect(logDirectoryRendererSource).not.toContain("t('app.data_root.log_directory.target_directory')");
|
||||
expect(logDirectoryRendererSource.match(/disabled={!editable \|\| directorySettingsApplying}/g)).toHaveLength(3);
|
||||
expect(embeddedDataRootSource.match(/disabled={directorySettingsApplying}/g)).toHaveLength(4);
|
||||
expect(standaloneDataRootSource.match(/disabled={directorySettingsApplying}/g)).toHaveLength(4);
|
||||
expect(embeddedDataRootSource).toContain('{renderLogDirectorySettings()}');
|
||||
expect(standaloneDataRootSource).toContain('{renderLogDirectorySettings()}');
|
||||
expect(appSource).not.toContain("| 'log-directory'");
|
||||
expect(appSource).not.toContain("key: 'log-directory'");
|
||||
expect(appSource).not.toContain("handleOpenToolCenterPane('config', 'log-directory')");
|
||||
});
|
||||
|
||||
it('keeps configurable saved query file storage inside the data-root detail page', () => {
|
||||
const selectHandlerStart = appSource.indexOf('const handleSelectSavedQueryDirectory = useCallback(async () => {');
|
||||
const selectHandlerEnd = appSource.indexOf('const handleApplySavedQueryDirectory = useCallback', selectHandlerStart);
|
||||
const selectHandlerSource = appSource.slice(selectHandlerStart, selectHandlerEnd);
|
||||
const rendererStart = appSource.indexOf('const renderSavedQueryDirectorySettings = (readOnly = false) => (');
|
||||
const rendererEnd = appSource.indexOf('const renderLogDirectorySettings = () => {', rendererStart);
|
||||
const rendererSource = appSource.slice(rendererStart, rendererEnd);
|
||||
const embeddedDataRootStart = appSource.indexOf("if (activeSettingsCenterPane.key === 'data-root')");
|
||||
const embeddedDataRootEnd = appSource.indexOf(
|
||||
"if (activeSettingsCenterPane.key === 'security-update')",
|
||||
embeddedDataRootStart,
|
||||
);
|
||||
const embeddedDataRootSource = appSource.slice(embeddedDataRootStart, embeddedDataRootEnd);
|
||||
const standaloneDataRootStart = appSource.indexOf('{isDataRootModalOpen && (');
|
||||
const standaloneDataRootEnd = appSource.indexOf(
|
||||
'<ConnectionPackagePasswordModal',
|
||||
standaloneDataRootStart,
|
||||
);
|
||||
const standaloneDataRootSource = appSource.slice(standaloneDataRootStart, standaloneDataRootEnd);
|
||||
|
||||
expect(appSource).toContain('ApplySavedQueryDirectory');
|
||||
expect(appSource).toContain('OpenSavedQueryDirectory');
|
||||
expect(appSource).toContain('SelectSavedQueryDirectory');
|
||||
expect(appSource).toContain('GetSavedQueries');
|
||||
expect(appSource).toContain("const [selectedSavedQueryDirectoryPath, setSelectedSavedQueryDirectoryPath] = useState('');");
|
||||
expect(appSource).toContain("const [savedQueryDirectoryApplying, setSavedQueryDirectoryApplying] = useState(false);");
|
||||
expect(appSource).toContain('const handleSelectSavedQueryDirectory = useCallback(async () => {');
|
||||
expect(selectHandlerSource).toContain('if (data.cancelled === true) return;');
|
||||
expect(selectHandlerSource).not.toContain('已取消');
|
||||
expect(appSource).toContain('const handleApplySavedQueryDirectory = useCallback(async (useDefaultPath = false) => {');
|
||||
expect(appSource).toContain('const handleOpenSavedQueryDirectory = useCallback(async () => {');
|
||||
expect(rendererStart).toBeGreaterThan(-1);
|
||||
expect(rendererEnd).toBeGreaterThan(rendererStart);
|
||||
expect(rendererSource).toContain('data-saved-query-directory-settings="true"');
|
||||
expect(rendererSource).toContain("t('app.data_root.saved_query_directory.title')");
|
||||
expect(rendererSource).toContain('dataRootInfo?.defaultSavedQueryDirectory');
|
||||
expect(rendererSource).toContain('{!readOnly && (');
|
||||
expect(appSource).toContain('replaceSavedQueries(Array.isArray(queries) ? queries : []);');
|
||||
expect(appSource).toContain('await reloadSavedQueryGroups();');
|
||||
expect(embeddedDataRootSource).toContain('{renderSavedQueryDirectorySettings(true)}');
|
||||
expect(embeddedDataRootSource).toContain('{renderSavedQueryDirectorySettings()}');
|
||||
expect(standaloneDataRootSource).toContain('{renderSavedQueryDirectorySettings()}');
|
||||
});
|
||||
|
||||
it('adds close and back-to-settings actions to settings center detail panes', () => {
|
||||
expect(appSource).toContain('const handleBackFromSettingsCenterPane = useCallback(() => {');
|
||||
expect(appSource).toContain('const handleCancelSettingsCenterPane = useCallback(() => {');
|
||||
expect(appSource).toContain('onClick={handleCancelSettingsCenterPane}');
|
||||
expect(appSource).toContain("t('common.close')");
|
||||
expect(appSource).toContain('onClick={handleBackFromSettingsCenterPane}');
|
||||
expect(appSource).toContain("t('common.back_to_settings')");
|
||||
});
|
||||
|
||||
it('clears embedded tool transient state before switching settings groups or panes', () => {
|
||||
const cleanupStart = appSource.indexOf('const clearSettingsCenterTransientPaneState = useCallback(() => {');
|
||||
const cleanupSource = appSource.slice(
|
||||
cleanupStart,
|
||||
appSource.indexOf('const handleOpenToolsModal', cleanupStart),
|
||||
);
|
||||
|
||||
expect(cleanupStart).toBeGreaterThan(-1);
|
||||
expect(cleanupSource).toContain('setCapturingShortcutAction(null);');
|
||||
expect(cleanupSource).toContain("activeSettingsCenterPaneRef.current?.key === 'connection-package'");
|
||||
expect(cleanupSource).toContain('closeConnectionPackageDialog();');
|
||||
expect(cleanupSource).toContain("activeSettingsCenterPaneRef.current?.key === 'ai'");
|
||||
expect(cleanupSource).toContain('setFocusedAIProviderId(undefined);');
|
||||
expect(cleanupSource).toContain('setSecurityUpdateRepairSource(null);');
|
||||
expect(appSource).toContain('const handleOpenSettingsModal = useCallback');
|
||||
expect(appSource).toContain('const handleOpenToolCenterPane = useCallback');
|
||||
expect(appSource.match(/clearSettingsCenterTransientPaneState\(\);/g)?.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it('routes every security-update detail and repair return through settings center', () => {
|
||||
const openDetailsStart = appSource.indexOf('const openSecurityUpdateSettings = useCallback(');
|
||||
const openDetailsSource = appSource.slice(
|
||||
openDetailsStart,
|
||||
appSource.indexOf('const handleOpenSecurityUpdateSettings', openDetailsStart),
|
||||
);
|
||||
|
||||
expect(openDetailsStart).toBeGreaterThan(-1);
|
||||
expect(openDetailsSource).toContain("setActiveSettingsCenterGroupKey('config');");
|
||||
expect(openDetailsSource).toContain("setActiveSettingsCenterPane({ key: 'security-update', group: 'config' });");
|
||||
expect(openDetailsSource).toContain('setIsSettingsModalOpen(true);');
|
||||
expect(appSource).toContain("const detailsWereOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'security-update';");
|
||||
expect(appSource.match(/<SecurityUpdateSettingsModal/g)?.length).toBe(1);
|
||||
expect(appSource).toMatch(/<SecurityUpdateSettingsModal\r?\n\s+embedded/);
|
||||
expect(appSource).not.toContain('isSecurityUpdateSettingsOpen');
|
||||
expect(appSource).not.toContain('setIsSecurityUpdateSettingsOpen');
|
||||
});
|
||||
|
||||
it('uses a consistent close footer while keeping the theme instant-apply hint', () => {
|
||||
expect(appSource).toContain("t('common.back_to_settings')");
|
||||
expect(appSource).toContain("t('common.close')");
|
||||
expect(appSource).toContain("t('app.theme.instant_apply_hint')");
|
||||
});
|
||||
|
||||
it('gates the new theme layout to v2 and keeps legacy side nav for old UI', () => {
|
||||
expect(appSource).toContain('renderThemeSettingsContentV2');
|
||||
expect(appSource).toContain('renderThemeSettingsContentLegacy');
|
||||
expect(appSource).toContain('isV2Ui ? renderThemeSettingsContentV2() : renderThemeSettingsContentLegacy()');
|
||||
expect(aiSettingsModalSource).toContain("gridTemplateColumns: '168px minmax(0, 1fr)', gap: 0, padding: '10px 0'");
|
||||
expect(aiSettingsModalSource).toContain('className="ai-settings-body gonavi-ai-settings-flat"');
|
||||
expect(appSource).toContain('className="gonavi-theme-settings"');
|
||||
expect(appSource).toContain('ThemeSettingsSlider');
|
||||
expect(appSource).toContain("t('app.theme.custom.title')");
|
||||
expect(appSource).toContain('<CustomThemeManager />');
|
||||
expect(appSource).toContain('<CustomThemeManager legacyMode />');
|
||||
expect(appSource).toContain("value: 'workspace'");
|
||||
expect(appSource).toContain('gonavi-settings-tabs');
|
||||
expect(appSource).toContain('setThemeModalSection(item.value)');
|
||||
});
|
||||
|
||||
it('resolves custom-theme base mode synchronously and bridges its surfaces into Ant Design', () => {
|
||||
expect(appSource).toContain("const resolvedThemeMode = effectiveThemePreference === 'system'");
|
||||
expect(appSource).toContain("const darkMode = resolvedThemeMode === 'dark';");
|
||||
expect(appSource).toContain('const customThemeStyleContextKey = `${resolvedThemeMode}:${appearance.uiVersion}`;');
|
||||
expect(appSource).toContain('colorBgContainer: (isV2Ui ? v2AntBgContainer : undefined)');
|
||||
expect(appSource).toContain('colorBgElevated: (isV2Ui ? v2AntBgElevated : undefined)');
|
||||
expect(appSource).toContain('colorTextSecondary: v2AntTextSecondary');
|
||||
expect(appSource).toContain('rowHoverBg: (isV2Ui ? v2AntRowHoverBg : undefined)');
|
||||
});
|
||||
|
||||
it('opens theme, AI, and about entries inside settings center detail panes', () => {
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('preferences', 'theme')");
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('services', 'ai')");
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('about', 'about-go-navi')");
|
||||
expect(appSource).toContain("if (activeSettingsCenterPane.key === 'theme')");
|
||||
expect(appSource).toContain("if (activeSettingsCenterPane.key === 'ai')");
|
||||
expect(appSource).toContain('<LazyAISettingsContent');
|
||||
expect(appSource).toContain("if (activeSettingsCenterPane.key === 'about-go-navi')");
|
||||
expect(appSource).toContain('renderSettingsCenterAboutPane()');
|
||||
});
|
||||
|
||||
it('opens AI settings from the chat panel via settings center instead of a standalone modal', () => {
|
||||
expect(appSource).toContain('const handleOpenAISettings = useCallback((providerId?: string) => {');
|
||||
expect(appSource).toContain("setActiveSettingsCenterPane({ key: 'ai', group: 'services' })");
|
||||
expect(appSource).toContain('setIsSettingsModalOpen(true)');
|
||||
expect(appSource).not.toContain('setIsAISettingsOpen(true)');
|
||||
expect(appSource).not.toContain('<AISettingsModal');
|
||||
});
|
||||
|
||||
it('keeps the settings center above the in-webview detached AI fallback', () => {
|
||||
const settingsModalStart = appSource.indexOf(
|
||||
"title={renderUtilityModalTitle(<SettingOutlined />, t('app.settings.title')",
|
||||
);
|
||||
const settingsModalSource = appSource.slice(settingsModalStart, settingsModalStart + 900);
|
||||
|
||||
expect(settingsModalStart).toBeGreaterThan(-1);
|
||||
expect(appSource).toContain('APP_FOREGROUND_MODAL_Z_INDEX,');
|
||||
expect(appSource).toContain('APP_NESTED_MODAL_Z_INDEX,');
|
||||
expect(appSource).toContain('const settingsCenterModalZIndex = Math.max(');
|
||||
expect(appSource).toContain('Number.isFinite(detachedAIChatZIndex) ? detachedAIChatZIndex + 1 : APP_FOREGROUND_MODAL_Z_INDEX');
|
||||
expect(appSource).toContain('const settingsChildModalZIndex = Math.max(');
|
||||
expect(appSource).toContain('settingsCenterModalZIndex + 100');
|
||||
expect(settingsModalSource).toContain('zIndex={settingsCenterModalZIndex}');
|
||||
});
|
||||
|
||||
it('opens the about group directly instead of showing a one-item list', () => {
|
||||
expect(appSource).toContain('const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => (');
|
||||
expect(appSource).toContain("group === 'about' ? { key: 'about-go-navi', group: 'about' } : null");
|
||||
expect(appSource).toContain('setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group));');
|
||||
expect(appSource).toContain('handleOpenSettingsModal(group.key);');
|
||||
});
|
||||
|
||||
it('routes silent update discovery to the settings center about pane via bridge', () => {
|
||||
expect(appSource).toContain('const updateCenterBridgeRef = useRef<{');
|
||||
expect(appSource).toContain('updateCenterBridgeRef,');
|
||||
expect(appSource).toContain('updateCenterBridgeRef.current = {');
|
||||
expect(appSource).toContain("handleOpenSettingsCenterPane('about', 'about-go-navi')");
|
||||
expect(appSource).toContain('prepareAboutSurface');
|
||||
expect(appSource).toContain('isSettingsAboutPaneOpen');
|
||||
});
|
||||
|
||||
it('renders the settings center about page as flat sections without nested cards', () => {
|
||||
const projectEntryStart = appSource.indexOf('const renderSettingsCenterAboutProjectEntry = ({');
|
||||
const aboutPaneStart = appSource.indexOf('const renderSettingsCenterAboutPane = () => {');
|
||||
const aboutPaneEnd = appSource.indexOf('const renderSettingsCenterAboutFooter = () => (', aboutPaneStart);
|
||||
const projectEntrySource = appSource.slice(projectEntryStart, aboutPaneStart);
|
||||
const aboutPaneSource = appSource.slice(aboutPaneStart, aboutPaneEnd);
|
||||
|
||||
expect(projectEntryStart).toBeGreaterThan(-1);
|
||||
expect(aboutPaneStart).toBeGreaterThan(projectEntryStart);
|
||||
expect(aboutPaneEnd).toBeGreaterThan(aboutPaneStart);
|
||||
expect(appSource).toContain('const renderSettingsCenterAboutPane = () => {');
|
||||
expect(appSource).toContain('const renderSettingsCenterAboutProjectEntry = ({');
|
||||
expect(appSource).toContain("activeSettingsCenterPane?.key === 'about-go-navi'");
|
||||
expect(appSource).toContain("padding: '0 4px 0 0'");
|
||||
expect(appSource).toContain("border: 'none'");
|
||||
expect(appSource).toContain("background: 'transparent'");
|
||||
expect(aboutPaneSource).toContain('className="gonavi-about-pane"');
|
||||
expect(aboutPaneSource).toContain("style={{ display: 'flex', flexDirection: 'column' }}");
|
||||
expect(aboutPaneSource).not.toContain("padding: '0 0 18px'");
|
||||
expect(aboutPaneSource).toContain('aria-labelledby="gonavi-about-version-heading"');
|
||||
expect(aboutPaneSource).toContain('aria-labelledby="gonavi-about-project-heading"');
|
||||
expect(aboutPaneSource).toContain("gridTemplateColumns: 'minmax(0, 1.15fr) minmax(260px, 0.85fr)'");
|
||||
expect(aboutPaneSource).not.toContain('cardBorder');
|
||||
expect(aboutPaneSource).not.toContain('cardBg');
|
||||
expect(aboutPaneSource).not.toContain('borderRadius');
|
||||
expect(projectEntrySource).toContain('className="gonavi-about-project-entry"');
|
||||
expect(projectEntrySource).toContain("border: 'none'");
|
||||
expect(projectEntrySource).toContain("background: 'transparent'");
|
||||
expect(projectEntrySource).not.toContain('borderRadius');
|
||||
expect(appCssSource).toContain('.gonavi-about-project-entry:hover:not(:disabled)');
|
||||
expect(appSource).toContain('width={92}');
|
||||
expect(appSource).toContain('height={92}');
|
||||
expect(appSource).toContain('width: 92');
|
||||
expect(appSource).toContain('height: 92');
|
||||
expect(appSource).toContain('const releaseTimeText = formatAboutReleaseTime(lastUpdateInfo?.releasePublishedAt);');
|
||||
expect(appSource).toContain("[t('app.about.version.release_time'), releaseTimeText]");
|
||||
expect(appSource).toContain("installMode === 'msi' || installMode === 'portable'");
|
||||
expect(appSource).toContain("[t('app.about.version.install_mode'), t(`app.about.install_mode.${installMode}`)]");
|
||||
expect(appSource).toContain("hasUpdate && packageType !== 'unknown'");
|
||||
expect(appSource).toContain("[t('app.about.version.package_type'), t(`app.about.package_type.${packageType}`)]");
|
||||
expect(appSource).toContain('className="gonavi-about-update-channel"');
|
||||
expect(appSource).toContain('<Segmented');
|
||||
expect(appSource).toContain("t('app.about.version_update.channel_hint.latest')");
|
||||
expect(appSource).toContain("t('app.about.version_update.channel_hint.dev')");
|
||||
expect(appSource).not.toContain("t('app.about.version_update.channel_hint')");
|
||||
expect(appSource).toMatch(/updateChannel === 'dev'\s*\? t\('app\.about\.version_update\.channel_hint\.dev'\)\s*: t\('app\.about\.version_update\.channel_hint\.latest'\)/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-pane\s*\{[^}]*--gn-about-update-control-width:\s*200px;/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-update-channel\.ant-segmented\.ant-segmented\s*\{[^}]*width:\s*var\(--gn-about-update-control-width\);/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-update-channel\.ant-segmented\.ant-segmented\s*\{[^}]*max-width:\s*100%;/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-update-channel\.ant-segmented\.ant-segmented\s*\{[^}]*justify-self:\s*end;/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-update-channel \.ant-segmented-item\s*\{[^}]*flex:\s*1 1 0;/s);
|
||||
expect(appSource).toContain("t('app.about.field.auto_check_updates')");
|
||||
expect(appSource).toContain("t('app.about.field.auto_check_interval')");
|
||||
expect(appSource).toContain("t('app.about.version_update.auto_check_hint')");
|
||||
expect(appSource).toContain("t('app.about.version_update.auto_check_disabled_hint')");
|
||||
expect(appSource).toContain('className="gonavi-about-auto-check-interval"');
|
||||
const autoCheckIntervalSelectStart = aboutPaneSource.indexOf('className="gonavi-about-auto-check-interval"');
|
||||
const autoCheckIntervalSelectSource = aboutPaneSource.slice(
|
||||
autoCheckIntervalSelectStart,
|
||||
aboutPaneSource.indexOf('/>', autoCheckIntervalSelectStart),
|
||||
);
|
||||
expect(autoCheckIntervalSelectStart).toBeGreaterThan(-1);
|
||||
expect(autoCheckIntervalSelectSource).not.toContain("width: '100%'");
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-auto-check-interval\.ant-select\s*\{[^}]*width:\s*var\(--gn-about-update-control-width\);/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-auto-check-interval\.ant-select\s*\{[^}]*max-width:\s*100%;/s);
|
||||
expect(appCssSource).toMatch(/\.gonavi-about-auto-check-interval\.ant-select\s*\{[^}]*justify-self:\s*end;/s);
|
||||
expect(appSource).toContain('checked={autoCheckForUpdates}');
|
||||
expect(appSource).toContain('setAutoCheckForUpdates(checked)');
|
||||
expect(appSource).toContain('setAutoCheckForUpdatesIntervalMinutes(Number(value))');
|
||||
expect(appSource).toContain('maxWidth: 360');
|
||||
expect(appSource).toContain("alignItems: 'start'");
|
||||
expect(appSource).toContain("overflowWrap: 'anywhere'");
|
||||
expect(appSource).toContain("t('app.about.version_update.title')");
|
||||
expect(appSource).toContain("t('app.about.project.github.title')");
|
||||
expect(appSource).toContain("t('app.about.project.issues.title')");
|
||||
expect(appSource).toContain("t('app.about.project.releases.title')");
|
||||
expect(appSource).toContain('const renderSettingsCenterAboutFooter = () => (');
|
||||
expect(appSource).toContain("t('app.about.last_checked_at', { time: aboutLastCheckedAt })");
|
||||
expect(appSource).toContain('renderAboutUpdateActions()');
|
||||
});
|
||||
|
||||
it('uses one flat detail shell across every settings page', () => {
|
||||
expect(appSource).toContain('const activeSettingsCenterDetailPanelStyle: React.CSSProperties = {');
|
||||
expect(appSource).toContain('style={activeSettingsCenterDetailPanelStyle}');
|
||||
expect(appSource).toContain("padding: '0 4px 0 0'");
|
||||
expect(appSource).toContain("background: 'transparent'");
|
||||
expect(aiSettingsModalSource).toContain('className="ai-settings-body gonavi-ai-settings-flat"');
|
||||
expect(aiSettingsModalSource).toContain("gap: 0, padding: '10px 0'");
|
||||
expect(aiSettingsModalSource).toContain("padding: '0 6px 24px 22px'");
|
||||
});
|
||||
|
||||
it('keeps embedded split-pane settings stable at scroll boundaries', () => {
|
||||
expect(appSource).toContain('const isSettingsCenterContainedScrollPane =');
|
||||
expect(appSource).toContain("activeSettingsCenterPane?.key === 'theme' || activeSettingsCenterPane?.key === 'ai'");
|
||||
expect(appSource).toContain('const settingsCenterDetailBodyStyle: React.CSSProperties = isSettingsCenterContainedScrollPane');
|
||||
expect(appSource).toContain("overflowY: 'hidden'");
|
||||
expect(appSource).toContain('style={isActiveToolCenterPane ? toolCenterDetailBodyStyle : settingsCenterDetailBodyStyle}');
|
||||
expect(appSource).toContain("boxSizing: 'border-box'");
|
||||
expect(appSource).toContain("overscrollBehavior: 'contain'");
|
||||
expect(aiSettingsModalSource).toContain("boxSizing: 'border-box'");
|
||||
expect(aiSettingsModalSource).toContain("overscrollBehavior: 'contain'");
|
||||
});
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const appSource = readFileSync(new URL('./App.tsx', import.meta.url), 'utf8');
|
||||
const appCssSource = readFileSync(new URL('./App.css', import.meta.url), 'utf8');
|
||||
const sidebarSource = readFileSync(new URL('./components/Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const connectionRailSource = readFileSync(new URL('./components/sidebar/SidebarConnectionRail.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('app sidebar tree panel collapse', () => {
|
||||
it('uses the v2 themed sidebar surface for the custom titlebar', () => {
|
||||
const titlebarStart = appSource.indexOf('{/* Custom Title Bar */}');
|
||||
const titlebarEnd = appSource.indexOf('{showLinuxCJKFontBanner && (', titlebarStart);
|
||||
const titlebarSource = appSource.slice(titlebarStart, titlebarEnd);
|
||||
|
||||
expect(titlebarStart).toBeGreaterThan(-1);
|
||||
expect(titlebarEnd).toBeGreaterThan(titlebarStart);
|
||||
expect(titlebarSource).toContain("background: isV2Ui ? 'var(--gn-bg-panel-2)' : bgMain,");
|
||||
});
|
||||
|
||||
it('collapses v2 to the scaled fixed rail while preserving the saved expanded width', () => {
|
||||
expect(appSource).toContain('const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);');
|
||||
expect(appSource).toContain('const sidebarCollapsedWidth = isV2Ui ? 38 * effectiveUiScale * effectiveSidebarRailScale : 0;');
|
||||
expect(appSource).toContain('const renderedSidebarWidth = isSidebarCollapsed ? sidebarCollapsedWidth : sidebarWidth;');
|
||||
expect(appSource).toContain('width={sidebarWidth}');
|
||||
expect(appSource).toContain('collapsed={isSidebarCollapsed}');
|
||||
expect(appSource).toContain('collapsedWidth={sidebarCollapsedWidth}');
|
||||
expect(appSource).toContain("['--gonavi-sidebar-collapsed-width' as any]: `${sidebarCollapsedWidth}px`");
|
||||
expect(appSource).toContain('trigger={null}');
|
||||
expect(appSource).not.toContain('setSidebarWidth(0)');
|
||||
});
|
||||
|
||||
it('keeps loaded tree nodes mounted and Sidebar props stable during the width transition', () => {
|
||||
expect(appSource).toContain("const sidebarPanelCollapseLabel = t('app.sidebar.collapse');");
|
||||
expect(appSource).toContain("const sidebarPanelExpandLabel = t('app.sidebar.expand');");
|
||||
expect(appSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
|
||||
expect(appSource).toContain('onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}');
|
||||
expect(appSource).not.toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
|
||||
expect(sidebarSource).not.toContain('isTreePanelCollapsed?: boolean;');
|
||||
expect(sidebarSource).not.toContain("display: isV2Ui && isTreePanelCollapsed ? 'none' : 'flex'");
|
||||
expect(appSource).toContain('data-sidebar-content="true"');
|
||||
expect(appCssSource).toContain('--gonavi-sidebar-collapse-duration: 200ms;');
|
||||
expect(appCssSource).toMatch(
|
||||
/\.ant-layout-sider\[data-sidebar-collapsed='true'\]\s+\[data-sidebar-tree-panel='true'\][^{]*\{[^}]*visibility:\s*hidden;[^}]*transition:\s*visibility 0s linear var\(--gonavi-sidebar-collapse-duration\);/s,
|
||||
);
|
||||
expect(appCssSource).toMatch(
|
||||
/\.ant-layout-sider\[data-sidebar-collapsed='false'\]\s+\.gn-v2-rail-sidebar-toggle-slot\s*\{[^}]*display:\s*none;/s,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the fixed rail visible and hides only the v2 explorer tree panel', () => {
|
||||
expect(sidebarSource).toContain("id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}");
|
||||
expect(sidebarSource).toContain("data-sidebar-tree-panel={isV2Ui ? 'true' : undefined}");
|
||||
expect(sidebarSource).toContain("style={{ display: 'flex', flexDirection: 'column'");
|
||||
expect(connectionRailSource).toContain('data-sidebar-fixed-rail="true"');
|
||||
expect(appSource).not.toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
|
||||
expect(appSource).not.toContain("visibility: !isV2Ui && isSidebarCollapsed ? 'hidden' : 'visible'");
|
||||
expect(appSource).toContain('data-sidebar-collapse-trigger="true"');
|
||||
expect(appSource).toContain('data-titlebar-brand-region="true"');
|
||||
expect(appSource).toContain('data-sidebar-toggle-placement="titlebar"');
|
||||
expect(connectionRailSource).toContain('data-sidebar-toggle-placement="fixed-rail"');
|
||||
expect(appSource).toContain('data-no-titlebar-toggle="true"');
|
||||
expect(appSource).toContain('aria-controls="gonavi-sidebar-tree-panel"');
|
||||
expect(appSource).toContain('aria-expanded={!isSidebarCollapsed}');
|
||||
expect(appSource).toContain('<MenuFoldOutlined />');
|
||||
expect(appSource).toContain('<MenuUnfoldOutlined />');
|
||||
expect(appSource).toContain("'app.sidebar.collapse'");
|
||||
expect(appSource).toContain("'app.sidebar.expand'");
|
||||
expect(appSource).toContain("case 'focusSidebarSearch':");
|
||||
expect(appSource).toContain('handleFocusSidebarSearch();');
|
||||
expect(appSource).toContain('<TabManager onFocusSidebarSearch={handleFocusSidebarSearch} />');
|
||||
expect(appSource).toContain('{!isV2Ui && (');
|
||||
expect(appSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
|
||||
expect(appSource).toContain('onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}');
|
||||
expect(appSource).toContain('collapseSidebarButtonRef={sidebarExplorerToggleRef}');
|
||||
expect(appSource).toContain('expandSidebarButtonRef={sidebarCollapsedToggleRef}');
|
||||
expect(appSource).toContain('ref={sidebarCollapsedToggleRef}');
|
||||
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'collapsed'");
|
||||
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'explorer'");
|
||||
expect(appSource).toContain("(target === 'collapsed' ? sidebarCollapsedToggleRef : sidebarExplorerToggleRef).current?.focus()");
|
||||
expect(sidebarSource).toContain('data-sidebar-toggle-placement="explorer-header"');
|
||||
|
||||
const titlebarToggleIndex = appSource.indexOf('data-sidebar-collapse-trigger="true"');
|
||||
const siderIndex = appSource.indexOf('<Sider');
|
||||
const siderEndIndex = appSource.indexOf('</Sider>', siderIndex);
|
||||
const triggerStartIndex = appSource.lastIndexOf('<Button', titlebarToggleIndex);
|
||||
const triggerEndIndex = appSource.indexOf('</Tooltip>', titlebarToggleIndex);
|
||||
const triggerSource = appSource.slice(triggerStartIndex, triggerEndIndex);
|
||||
const siderSource = appSource.slice(siderIndex, siderEndIndex);
|
||||
const explorerActionsIndex = sidebarSource.indexOf('<div className="gn-v2-active-connection-actions">');
|
||||
const fixedRailIndex = sidebarSource.indexOf('<SidebarConnectionRail');
|
||||
const explorerPanelIndex = sidebarSource.indexOf("id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}");
|
||||
const connectionMenuIndex = sidebarSource.indexOf('<Tooltip title={v2ConnectionActionsLabel}>', explorerActionsIndex);
|
||||
const explorerToggleIndex = sidebarSource.indexOf('data-sidebar-toggle-placement="explorer-header"', explorerActionsIndex);
|
||||
const explorerToggleEndIndex = sidebarSource.indexOf('</Tooltip>', explorerToggleIndex);
|
||||
const explorerToggleStartIndex = sidebarSource.lastIndexOf('<Button', explorerToggleIndex);
|
||||
const explorerToggleSource = sidebarSource.slice(explorerToggleStartIndex, explorerToggleEndIndex);
|
||||
const fixedRailToggleIndex = connectionRailSource.indexOf('data-sidebar-toggle-placement="fixed-rail"');
|
||||
const railItemsIndex = connectionRailSource.indexOf('<div className="gn-v2-rail-items">');
|
||||
const railPrimaryActionsIndex = connectionRailSource.indexOf('<div className="gn-v2-rail-primary-actions"');
|
||||
const firstRailObjectActionIndex = connectionRailSource.indexOf('data-sidebar-create-group-action="true"');
|
||||
expect(titlebarToggleIndex).toBeGreaterThan(appSource.indexOf('data-titlebar-brand-region="true"'));
|
||||
expect(titlebarToggleIndex).toBeLessThan(siderIndex);
|
||||
expect(triggerSource).toContain('type="text"');
|
||||
expect(triggerSource).toContain("WebkitAppRegion: 'no-drag'");
|
||||
expect(triggerSource).toContain("'--wails-draggable': 'no-drag'");
|
||||
expect(siderSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
|
||||
expect(fixedRailIndex).toBeGreaterThan(-1);
|
||||
expect(explorerPanelIndex).toBeGreaterThan(fixedRailIndex);
|
||||
expect(explorerToggleIndex).toBeGreaterThan(connectionMenuIndex);
|
||||
expect(explorerToggleSource).toContain('ref={collapseSidebarButtonRef}');
|
||||
expect(explorerToggleSource).not.toContain('disabled=');
|
||||
expect(fixedRailToggleIndex).toBeGreaterThan(-1);
|
||||
expect(fixedRailToggleIndex).toBeGreaterThan(railItemsIndex);
|
||||
expect(fixedRailToggleIndex).toBeGreaterThan(railPrimaryActionsIndex);
|
||||
expect(fixedRailToggleIndex).toBeLessThan(firstRailObjectActionIndex);
|
||||
});
|
||||
|
||||
it('overrides normal resize bounds with the retained rail width and removes the collapsed resize target', () => {
|
||||
expect(appCssSource).toMatch(
|
||||
/body\[data-ui-version\]\s+\.ant-layout-sider\[data-sidebar-collapsed='true'\]\s*\{[^}]*min-width:\s*var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;[^}]*max-width:\s*var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;[^}]*width:\s*var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;[^}]*flex:\s*0 0 var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;/s,
|
||||
);
|
||||
expect(appSource).toContain('paddingRight: isSidebarCollapsed ? 0 : sidebarResizeHandleWidth');
|
||||
expect(appSource).toContain('{!isSidebarCollapsed && <div');
|
||||
expect(appCssSource).toMatch(
|
||||
/body\[data-ui-version\]\s+\.gonavi-sidebar-collapse-trigger\.ant-btn\s*\{[^}]*width:\s*26px;[^}]*height:\s*26px(?:\s*!important)?;[^}]*border:\s*0\s*!important;/s,
|
||||
);
|
||||
expect(appCssSource).toMatch(
|
||||
/body\[data-ui-version='v2'\]\s+\.gonavi-sidebar-collapse-trigger\.ant-btn\[data-sidebar-toggle-placement='explorer-header'\]\s*\{[^}]*width:\s*24px;[^}]*height:\s*24px(?:\s*!important)?;[^}]*border:\s*1px solid var\(--gn-br-2\)\s*!important;[^}]*border-radius:\s*7px\s*!important;/s,
|
||||
);
|
||||
expect(appCssSource).not.toMatch(
|
||||
/\.ant-layout-sider\[data-sidebar-panel='true'\]\s*\{[^}]*(?:min-width|max-width|\bwidth\s*:|\bflex\s*:)/s,
|
||||
);
|
||||
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*position:\s*absolute/s);
|
||||
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*right:\s*-\d+px/s);
|
||||
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*translateY/s);
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const tabDisplaySource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./utils/tabDisplay.ts', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('App tab display i18n guards', () => {
|
||||
it('localizes the tab display settings copy and preview labels', () => {
|
||||
[
|
||||
'app.theme.tab_display.title',
|
||||
'app.theme.tab_display.description',
|
||||
'app.theme.tab_display.layout.single',
|
||||
'app.theme.tab_display.layout.double',
|
||||
'app.theme.tab_display.badge.current',
|
||||
'app.theme.tab_display.row.primary',
|
||||
'app.theme.tab_display.row.secondary',
|
||||
'app.theme.tab_display.action.move_up',
|
||||
'app.theme.tab_display.action.move_down',
|
||||
'app.theme.tab_display.preview.prefix',
|
||||
'app.theme.tab_display.preview.default_label',
|
||||
'app.theme.tab_display.preview.secondary',
|
||||
'app.theme.tab_display.preview.focused',
|
||||
].forEach((key) => {
|
||||
expect(appSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'Tab 标签展示',
|
||||
'自定义连接名、对象类型、对象名、数据库、Schema 和 Host/IP 的展示顺序',
|
||||
"'单行'",
|
||||
"'双行'",
|
||||
'当前预览:',
|
||||
'默认标签',
|
||||
',副行',
|
||||
';当前选中',
|
||||
'上移',
|
||||
'下移',
|
||||
].forEach((legacyText) => {
|
||||
expect(appSource).not.toContain(legacyText);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps tab display element metadata as i18n keys', () => {
|
||||
[
|
||||
'connection',
|
||||
'kind',
|
||||
'object',
|
||||
'database',
|
||||
'schema',
|
||||
'host',
|
||||
].forEach((elementKey) => {
|
||||
expect(tabDisplaySource).toContain(`labelKey: 'app.theme.tab_display.element.${elementKey}.label'`);
|
||||
expect(tabDisplaySource).toContain(`descriptionKey: 'app.theme.tab_display.element.${elementKey}.description'`);
|
||||
});
|
||||
|
||||
[
|
||||
'连接名',
|
||||
'连接简称或环境名',
|
||||
'对象类型',
|
||||
'对象名',
|
||||
'当前 DB / catalog 名称',
|
||||
'连接目标地址摘要',
|
||||
].forEach((legacyText) => {
|
||||
expect(tabDisplaySource).not.toContain(legacyText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('App tools entry i18n guards', () => {
|
||||
it('localizes compare tool entry titles and descriptions', () => {
|
||||
expect(appSource).toContain("t('app.tools.entry.schema_compare.title')");
|
||||
expect(appSource).toContain("t('app.tools.entry.schema_compare.description')");
|
||||
expect(appSource).toContain("t('app.tools.entry.data_compare.title')");
|
||||
expect(appSource).toContain("t('app.tools.entry.data_compare.description')");
|
||||
|
||||
expect(appSource).not.toContain("title: '表结构比对'");
|
||||
expect(appSource).not.toContain("description: '对比源表与目标表结构差异,只预览不执行。'");
|
||||
expect(appSource).not.toContain("title: '数据比对'");
|
||||
expect(appSource).not.toContain("description: '按主键分析新增、更新、删除和相同行。'");
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('UI version switch placement', () => {
|
||||
it('loads the v2 theme stylesheet with the app shell', () => {
|
||||
expect(appSource).toContain("import './App.css';");
|
||||
expect(appSource).toContain("import './v2-theme.css';");
|
||||
});
|
||||
|
||||
it('keeps light/dark first with compact previews and UI version preview tiles', () => {
|
||||
const themeBranchIndex = appSource.indexOf("{themeModalSection === 'theme' ? (");
|
||||
const lightThemeIndex = appSource.indexOf("t('app.theme.mode.light.label')", themeBranchIndex);
|
||||
const customThemeIndex = appSource.indexOf("t('app.theme.custom.title')", lightThemeIndex);
|
||||
const uiVersionIndex = appSource.indexOf("t('app.theme.ui_version.title')", themeBranchIndex);
|
||||
const appearanceBranchIndex = appSource.indexOf(") : themeModalSection === 'appearance' ? (", themeBranchIndex);
|
||||
const macWindowIndex = appSource.indexOf("t('app.theme.mac_window.title')");
|
||||
|
||||
expect(themeBranchIndex).toBeGreaterThan(-1);
|
||||
expect(lightThemeIndex).toBeGreaterThan(themeBranchIndex);
|
||||
expect(customThemeIndex).toBeGreaterThan(lightThemeIndex);
|
||||
expect(customThemeIndex).toBeLessThan(uiVersionIndex);
|
||||
expect(uiVersionIndex).toBeGreaterThan(lightThemeIndex);
|
||||
expect(uiVersionIndex).toBeLessThan(appearanceBranchIndex);
|
||||
expect(macWindowIndex).toBeGreaterThan(uiVersionIndex);
|
||||
expect(appSource).toContain('renderUiVersionPreview');
|
||||
expect(appSource).toContain('gonavi-settings-ui-version-grid');
|
||||
expect(appSource).toContain("onClick={() => setAppearance({ uiVersion: item.key })}");
|
||||
expect(appSource).toContain("t('app.theme.ui_version.beta_warning')");
|
||||
expect(appSource).toContain("t('app.theme.ui_version.platform_hint')");
|
||||
expect(appSource).toContain("t('app.theme.ui_version.sidebar_search.title')");
|
||||
expect(appSource).toContain("value={appearance.v2SidebarSearchMode ?? 'command'}");
|
||||
expect(appSource).toContain("setAppearance({ v2SidebarSearchMode: value as 'command' | 'filter' })");
|
||||
expect(appSource).toContain("appearance.uiVersion === 'v2' ? (");
|
||||
});
|
||||
|
||||
it('uses compact previews only in v2 theme settings layout', () => {
|
||||
expect(appSource).toContain('renderThemeSettingsContentV2');
|
||||
expect(appSource).toContain('renderThemeSettingsContentLegacy');
|
||||
expect(appSource).toContain('isV2Ui ? renderThemeSettingsContentV2() : renderThemeSettingsContentLegacy()');
|
||||
expect(appSource).toContain('className="gonavi-theme-settings"');
|
||||
expect(appSource).toContain('gonavi-settings-mode-grid');
|
||||
expect(appSource).toContain('gonavi-settings-mode-tile');
|
||||
expect(appSource).toContain('renderThemeModePreview');
|
||||
expect(appSource).toContain("preview: 'light' as const");
|
||||
expect(appSource).toContain('ThemeSettingsSlider');
|
||||
expect(appSource).toContain("unit=\"percent\"");
|
||||
expect(appSource).toContain('gonavi-settings-tabs');
|
||||
expect(appSource).toContain('gonavi-settings-tab');
|
||||
expect(appSource).toContain('gonavi-settings-pill');
|
||||
expect(appSource).toContain('<CustomThemeManager />');
|
||||
expect(appSource).toContain('<CustomThemeManager legacyMode />');
|
||||
// 旧版布局仍保留侧栏导航
|
||||
expect(appSource).toContain("gridTemplateColumns: '180px minmax(0, 1fr)', gap: 16, padding: '12px 0'");
|
||||
});
|
||||
|
||||
it('keeps theme and UI version radio tiles on one tab stop with complete keyboard navigation', () => {
|
||||
const themeBranchIndex = appSource.indexOf("{themeModalSection === 'theme' ? (");
|
||||
const appearanceBranchIndex = appSource.indexOf(") : themeModalSection === 'appearance' ? (", themeBranchIndex);
|
||||
const themeSource = appSource.slice(themeBranchIndex, appearanceBranchIndex);
|
||||
|
||||
expect(themeSource).toContain(']).map((item, itemIndex, themeItems) => {');
|
||||
expect(themeSource).toContain('tabIndex={themePreference === item.key ? 0 : -1}');
|
||||
expect(themeSource).toContain('selectPresetTheme(themeItems[nextIndex].key);');
|
||||
expect(themeSource).toContain(']).map((item, itemIndex, uiVersionItems) => {');
|
||||
expect(themeSource).toContain('tabIndex={active ? 0 : -1}');
|
||||
expect(themeSource).toContain('setAppearance({ uiVersion: uiVersionItems[nextIndex].key });');
|
||||
expect(themeSource.match(/\['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', 'Home', 'End'\]/g)).toHaveLength(2);
|
||||
expect(themeSource.match(/querySelectorAll<HTMLElement>\('\[role="radio"\]'\)/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('isolates workspace settings and remembers the active section', () => {
|
||||
expect(appSource).toContain("value: 'workspace'");
|
||||
expect(appSource).toContain("t('app.theme.nav.workspace.title')");
|
||||
expect(appSource).toContain("setThemeModalSection('workspace')");
|
||||
expect(appSource).toContain("themeModalSection !== 'workspace'");
|
||||
expect(appSource).toContain('gonavi.themeSettingsSection');
|
||||
});
|
||||
|
||||
it('localizes the v2 sidebar search mode copy', () => {
|
||||
expect(appSource).toContain("t('app.theme.ui_version.sidebar_search.title')");
|
||||
expect(appSource).toContain("t('app.theme.ui_version.sidebar_search.command')");
|
||||
expect(appSource).toContain("t('app.theme.ui_version.sidebar_search.filter')");
|
||||
expect(appSource).toContain("t('app.theme.ui_version.sidebar_search.hint')");
|
||||
expect(appSource).not.toContain('新版左侧搜索模式');
|
||||
expect(appSource).not.toContain('新版命令搜索');
|
||||
expect(appSource).not.toContain('旧版侧栏筛选');
|
||||
expect(appSource).not.toContain('新版命令搜索适合跳转连接、表和动作');
|
||||
});
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('restart-to-update unsaved SQL guard', () => {
|
||||
it('runs the confirmed update action through every application quit path', () => {
|
||||
expect(appSource).toContain('type ApplicationQuitConfirmedAction = () => Promise<boolean>;');
|
||||
expect(appSource).toContain('const handleApplicationQuitRequest = useCallback(async (');
|
||||
expect(appSource).toContain('confirmedAction?: ApplicationQuitConfirmedAction,');
|
||||
expect(appSource).toContain('cancelledAction?: () => void,');
|
||||
expect(appSource).toContain('const runConfirmedAction = async (): Promise<boolean> => {');
|
||||
const runnerStart = appSource.indexOf('const runConfirmedAction = async (): Promise<boolean> => {');
|
||||
const runnerEnd = appSource.indexOf('\n let targets;', runnerStart);
|
||||
const runnerSource = appSource.slice(runnerStart, runnerEnd);
|
||||
expect(runnerSource).toContain('accepted = await confirmedAction();');
|
||||
expect(runnerSource).toContain('await forceQuitApplication();\n accepted = true;');
|
||||
expect(runnerSource).toContain('} catch (error) {\n cancelRequest();');
|
||||
expect(runnerSource).toContain('if (!accepted) {\n cancelRequest();');
|
||||
expect(appSource).toContain('resetApplicationQuitRequest();\n cancelledAction?.();');
|
||||
expect(runnerSource).toContain('return accepted;');
|
||||
expect(appSource).toContain('if (targets.length === 0) {\n await runConfirmedAction();');
|
||||
expect(appSource).toContain('void runConfirmedAction();');
|
||||
const saveIndex = appSource.indexOf('await saveApplicationQuitUnsavedSQLTargets(targets, saveQuery);');
|
||||
const actionAfterSaveIndex = appSource.indexOf('await runConfirmedAction();', saveIndex);
|
||||
expect(saveIndex).toBeGreaterThan(-1);
|
||||
expect(actionAfterSaveIndex).toBeGreaterThan(saveIndex);
|
||||
});
|
||||
|
||||
it('waits for saved queries and flushes recovery state before quitting', () => {
|
||||
const quitHandlerStart = appSource.indexOf('const handleApplicationQuitRequest = useCallback');
|
||||
const quitHandlerEnd = appSource.indexOf('\n\n const handleInstallUpdateRequest', quitHandlerStart);
|
||||
const quitHandlerSource = appSource.slice(quitHandlerStart, quitHandlerEnd);
|
||||
const ensureLoadedIndex = quitHandlerSource.indexOf('await ensureSavedQueriesLoaded();');
|
||||
const readLatestStateIndex = quitHandlerSource.indexOf('const latestState = useStore.getState();');
|
||||
const flushDraftsIndex = quitHandlerSource.indexOf('flushQueryTabDraftSnapshots();');
|
||||
const flushStoreIndex = quitHandlerSource.indexOf('await flushAppStatePersistence();');
|
||||
const confirmedActionIndex = quitHandlerSource.indexOf('accepted = await confirmedAction();');
|
||||
const forceQuitIndex = quitHandlerSource.indexOf('await forceQuitApplication();');
|
||||
|
||||
expect(ensureLoadedIndex).toBeGreaterThan(-1);
|
||||
expect(readLatestStateIndex).toBeGreaterThan(ensureLoadedIndex);
|
||||
expect(flushDraftsIndex).toBeGreaterThan(-1);
|
||||
expect(flushStoreIndex).toBeGreaterThan(flushDraftsIndex);
|
||||
expect(confirmedActionIndex).toBeGreaterThan(flushStoreIndex);
|
||||
expect(forceQuitIndex).toBeGreaterThan(flushStoreIndex);
|
||||
});
|
||||
|
||||
it('does not block application quit on saved-query group refresh failures', () => {
|
||||
const loaderStart = appSource.indexOf('const ensureSavedQueriesLoaded = useCallback');
|
||||
const loaderEnd = appSource.indexOf('\n\n useEffect(() => {', loaderStart);
|
||||
const loaderSource = appSource.slice(loaderStart, loaderEnd);
|
||||
|
||||
expect(loaderSource).toContain('savedQueriesLoadedRef.current = true;');
|
||||
expect(loaderSource).toContain('void reloadSavedQueryGroups().catch((error) => {');
|
||||
expect(loaderSource).not.toContain('await reloadSavedQueryGroups();');
|
||||
});
|
||||
|
||||
it('lets the backend confirm only actually running Windows instances after the unsaved SQL guard', () => {
|
||||
expect(appSource).toContain('const handleInstallUpdateRequest = useCallback(async () => {');
|
||||
const installRequestStart = appSource.indexOf('const handleInstallUpdateRequest = useCallback(async () => {');
|
||||
const installRequestEnd = appSource.indexOf('\n\n useEffect(() => {', installRequestStart);
|
||||
const installRequestSource = appSource.slice(installRequestStart, installRequestEnd);
|
||||
expect(installRequestSource.indexOf('hideUpdateDownloadProgress();')).toBeGreaterThan(-1);
|
||||
expect(installRequestSource.indexOf('await handleApplicationQuitRequest(')).toBeGreaterThan(-1);
|
||||
expect(installRequestSource.indexOf('hideUpdateDownloadProgress();')).toBeLessThan(
|
||||
installRequestSource.indexOf('await handleApplicationQuitRequest('),
|
||||
);
|
||||
expect(installRequestSource).toContain('() => handleInstallFromProgress(false),');
|
||||
expect(installRequestSource).toContain('showUpdateDownloadProgress,');
|
||||
expect(appSource).not.toContain("title: t('app.about.update_install_confirm.close_instances_title')");
|
||||
expect(appSource).not.toContain('handleInstallFromProgress(true)');
|
||||
expect(appSource.match(/void handleInstallUpdateRequest\(\);/g)).toHaveLength(2);
|
||||
expect(appSource).not.toContain('onClick={handleInstallFromProgress}');
|
||||
expect(appSource).toContain("updateInstallAction === 'install-and-restart'");
|
||||
expect(appSource).toContain("updateInstallAction === 'launch-installer'");
|
||||
expect(appSource.match(/\{updateInstallActionLabel\}/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps the unsaved SQL quit confirmation above active settings and update dialogs', () => {
|
||||
const unsavedConfirmStart = appSource.indexOf('const confirmRef = Modal.confirm({');
|
||||
const installRequestStart = appSource.indexOf('const handleInstallUpdateRequest = useCallback', unsavedConfirmStart);
|
||||
const installRequestEnd = appSource.indexOf('\n\n useEffect(() => {', installRequestStart);
|
||||
const unsavedConfirmSource = appSource.slice(unsavedConfirmStart, installRequestStart);
|
||||
const installRequestSource = appSource.slice(installRequestStart, installRequestEnd);
|
||||
|
||||
expect(unsavedConfirmStart).toBeGreaterThan(-1);
|
||||
expect(installRequestStart).toBeGreaterThan(unsavedConfirmStart);
|
||||
expect(installRequestEnd).toBeGreaterThan(installRequestStart);
|
||||
expect(appSource).toContain('APP_APPLICATION_QUIT_MODAL_Z_INDEX,');
|
||||
expect(appSource).toContain('const applicationQuitModalZIndex = Math.max(');
|
||||
expect(appSource).toContain('settingsChildModalZIndex + 100,');
|
||||
expect(unsavedConfirmSource).toContain('zIndex: applicationQuitModalZIndex');
|
||||
expect(installRequestSource).not.toContain('Modal.confirm({');
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const appSource = readFileSync(new URL('./App.tsx', import.meta.url), 'utf8');
|
||||
const appCssSource = readFileSync(new URL('./App.css', import.meta.url), 'utf8');
|
||||
|
||||
describe('Windows titlebar window controls spacing', () => {
|
||||
it('adds a small platform-scoped gap between the custom window buttons', () => {
|
||||
expect(appSource).toContain('className="titlebar-window-controls"');
|
||||
expect(appSource).toContain("document.body.setAttribute('data-platform', runtimePlatform || '')");
|
||||
expect(appCssSource).toMatch(
|
||||
/body\[data-platform='windows'\]\s+\.titlebar-window-controls\s*\{[^}]*gap:\s*8px;/s,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,412 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const connectionModalSource = readFileSync(new URL('./ConnectionModal.tsx', import.meta.url), 'utf8');
|
||||
const connectionModalConfigSource = readFileSync(new URL('./connectionModal/connectionModalConfig.ts', import.meta.url), 'utf8');
|
||||
const connectionModalStep2Source = readFileSync(new URL('./connectionModal/ConnectionModalStep2.tsx', import.meta.url), 'utf8');
|
||||
const connectionModalNetworkSecuritySource = readFileSync(new URL('./connectionModal/ConnectionModalNetworkSecuritySection.tsx', import.meta.url), 'utf8');
|
||||
const connectionModalUriSource = readFileSync(new URL('./connectionModal/connectionModalUri.ts', import.meta.url), 'utf8');
|
||||
const redisSectionsSource = readFileSync(new URL('./ConnectionModalRedisSections.tsx', import.meta.url), 'utf8');
|
||||
const mongoSectionsSource = readFileSync(new URL('./ConnectionModalMongoSections.tsx', import.meta.url), 'utf8');
|
||||
const connectionTypeCatalogSource = readFileSync(new URL('../utils/connectionTypeCatalog.ts', import.meta.url), 'utf8');
|
||||
const connectionTypeCapabilitiesSource = readFileSync(new URL('../utils/connectionTypeCapabilities.ts', import.meta.url), 'utf8');
|
||||
const source = `${connectionModalSource}\n${connectionModalConfigSource}\n${connectionModalStep2Source}\n${connectionModalNetworkSecuritySource}\n${connectionModalUriSource}\n${redisSectionsSource}\n${mongoSectionsSource}\n${connectionTypeCatalogSource}\n${connectionTypeCapabilitiesSource}`;
|
||||
|
||||
describe('ConnectionModal edit password behavior', () => {
|
||||
it('keeps the prefilled primary password masked by default', () => {
|
||||
expect(source).toContain('const [primaryPasswordVisible, setPrimaryPasswordVisible] = useState(false);');
|
||||
expect(source).not.toContain('setPrimaryPasswordVisible(String(config.password || "").trim() !== "")');
|
||||
expect(source).toContain('visible: primaryPasswordVisible,');
|
||||
});
|
||||
|
||||
it('does not render the primary-password clear helper block anymore', () => {
|
||||
expect(source).not.toContain('description:\n "当前已保存主连接密码。留空表示继续沿用,输入新值表示替换。"');
|
||||
expect(source).not.toContain('description:\n "当前已保存 Redis 密码。留空表示继续沿用,输入新值表示替换。"');
|
||||
expect(source).toContain('String(config.password || "") === ""');
|
||||
});
|
||||
|
||||
it('reuses the shared backend-cancel helper for file and certificate pickers', () => {
|
||||
expect(source).not.toContain('res?.message !== "已取消"');
|
||||
expect(source.match(/isBackendCancelledResult\(res\)/g) ?? []).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('uses localized SSL mode labels instead of hardcoded English strings', () => {
|
||||
expect(source).not.toContain('label: "Preferred"');
|
||||
expect(source).not.toContain('label: "Required"');
|
||||
expect(source).not.toContain('label: "Skip Verify"');
|
||||
expect(source).toMatch(
|
||||
/label:\s*t\(\s*"connection\.modal\.network\.ssl_mode\.preferred",\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/label:\s*t\(\s*"connection\.modal\.network\.ssl_mode\.required",\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/label:\s*t\(\s*"connection\.modal\.network\.ssl_mode\.skip_verify",\s*\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConnectionModal data source registry', () => {
|
||||
it('exposes Elasticsearch in the create-connection picker with HTTP defaults', () => {
|
||||
expect(source).toContain("case 'elasticsearch':");
|
||||
expect(source).toContain('return 9200;');
|
||||
expect(source).toContain('elasticsearch: ["http", "https"]');
|
||||
expect(source).toContain("key: 'elasticsearch'");
|
||||
expect(source).toContain("name: 'Elasticsearch'");
|
||||
expect(source).toContain('icon: getDbIcon(item.key, undefined, 36)');
|
||||
expect(source).toContain('type === "elasticsearch"');
|
||||
expect(source).toContain("'connection_modal.step1.hint.elasticsearch'");
|
||||
expect(source).toContain(
|
||||
"'Index browsing, Mapping inspection, JSON DSL, and query_string queries'",
|
||||
);
|
||||
expect(source).toContain('const PRIMARY_USERNAME_OPTIONAL_TYPES = new Set([');
|
||||
expect(source).toContain('"mqtt",');
|
||||
expect(source).toContain(
|
||||
'type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant" || type === "milvus" || type === "rocketmq" || type === "mqtt" || type === "kafka" || type === "rabbitmq") ? "" : "root";',
|
||||
);
|
||||
expect(source).toContain('PRIMARY_USERNAME_OPTIONAL_TYPES.has(dbType)');
|
||||
expect(source).toContain('connection.modal.field.displayDatabases.label');
|
||||
});
|
||||
|
||||
it('keeps MQTT username optional during test-connection validation', () => {
|
||||
expect(source).toContain('"mqtt",');
|
||||
expect(source).toContain('PRIMARY_USERNAME_OPTIONAL_TYPES.has(dbType)');
|
||||
expect(source).toContain('connection.modal.field.username.required');
|
||||
expect(source).toContain('connection.modal.field.username.optional_placeholder');
|
||||
});
|
||||
|
||||
it('exposes Chroma in the create-connection picker with vector defaults', () => {
|
||||
expect(source).toContain("case 'chroma':");
|
||||
expect(source).toContain('return 8000;');
|
||||
expect(source).toContain('chroma: ["http", "https", "chroma"]');
|
||||
expect(source).toContain("key: 'chroma'");
|
||||
expect(source).toContain("name: 'Chroma'");
|
||||
expect(source).toContain('type === "chroma"');
|
||||
expect(source).toContain("'connection_modal.step1.hint.chroma'");
|
||||
expect(source).toContain(
|
||||
"'Collection browsing, vector retrieval, and metadata filtering'",
|
||||
);
|
||||
expect(source).toContain('return "http://127.0.0.1:8000/default_database?tenant=default_tenant";');
|
||||
expect(source).toContain('return "tenant=default_tenant&apiKey=...";');
|
||||
});
|
||||
|
||||
it('exposes Qdrant in the create-connection picker with vector defaults', () => {
|
||||
expect(source).toContain("case 'qdrant':");
|
||||
expect(source).toContain('return 6333;');
|
||||
expect(source).toContain('qdrant: ["http", "https", "qdrant"]');
|
||||
expect(source).toContain("key: 'qdrant'");
|
||||
expect(source).toContain("name: 'Qdrant'");
|
||||
expect(source).toContain('type === "qdrant"');
|
||||
expect(source).toContain("'connection_modal.step1.hint.qdrant'");
|
||||
expect(source).toContain(
|
||||
"'Collection browsing, vector search, and Payload filtering'",
|
||||
);
|
||||
expect(source).toContain('return "http://127.0.0.1:6333";');
|
||||
expect(source).toContain('return "apiKey=...";');
|
||||
});
|
||||
|
||||
it('exposes Milvus in the create-connection picker with vector defaults', () => {
|
||||
expect(source).toContain("case 'milvus':");
|
||||
expect(source).toContain('return 19530;');
|
||||
expect(source).toContain('milvus: ["http", "https", "milvus"]');
|
||||
expect(source).toContain("key: 'milvus'");
|
||||
expect(source).toContain("name: 'Milvus'");
|
||||
expect(source).toContain('type === "milvus"');
|
||||
expect(source).toContain("'connection_modal.step1.hint.milvus'");
|
||||
expect(source).toContain(
|
||||
"'Collection browsing, vector search, and scalar filtering'",
|
||||
);
|
||||
expect(source).toContain('return "http://127.0.0.1:19530/default";');
|
||||
expect(source).toContain('return "token=...";');
|
||||
});
|
||||
|
||||
it('exposes Apache IoTDB in the create-connection picker with timeseries defaults', () => {
|
||||
expect(source).toContain("case 'iotdb':");
|
||||
expect(source).toContain('return 6667;');
|
||||
expect(source).toContain('iotdb: ["iotdb"]');
|
||||
expect(source).toContain("key: 'iotdb'");
|
||||
expect(source).toContain("name: 'Apache IoTDB'");
|
||||
expect(source).toContain('dbType === "iotdb"');
|
||||
expect(source).toContain("return 'Storage Group / Device / Timeseries';");
|
||||
expect(source).toContain('return "iotdb://root:root@127.0.0.1:6667/root.sg";');
|
||||
expect(source).toContain('return "fetchSize=1024&timeZone=Asia%2FShanghai";');
|
||||
});
|
||||
|
||||
it('exposes RocketMQ in the create-connection picker with nameserver and topic defaults', () => {
|
||||
expect(source).toContain("case 'rocketmq':");
|
||||
expect(source).toContain('return 9876;');
|
||||
expect(source).toContain('rocketmq: ["rocketmq", "rmq"]');
|
||||
expect(source).toContain("key: 'rocketmq'");
|
||||
expect(source).toContain("name: 'RocketMQ'");
|
||||
expect(source).toContain('dbType === "rocketmq"');
|
||||
expect(source).toContain("return 'NameServer / Topic / Consumer Group';");
|
||||
expect(source).toContain('return "rocketmq://accessKey:secretKey@127.0.0.1:9876,127.0.0.2:9876/orders.events?topology=cluster&groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";');
|
||||
expect(source).toContain('return "groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";');
|
||||
expect(source).toContain('t("connection.modal.messageQueue.rocketmq.defaultTopic.label")');
|
||||
expect(source).toContain('connection.modal.field.username.label');
|
||||
expect(source).toContain('connection.modal.field.password.label');
|
||||
expect(source).toContain('connection.modal.field.username.optional_placeholder');
|
||||
expect(source).toContain('connection.modal.field.password.retained');
|
||||
});
|
||||
|
||||
it('exposes MQTT in the create-connection picker with broker and topic-filter defaults', () => {
|
||||
expect(source).toContain("case 'mqtt':");
|
||||
expect(source).toContain('return 1883;');
|
||||
expect(source).toContain('mqtt: ["mqtt", "mqtts", "tcp", "ssl", "tls"]');
|
||||
expect(source).toContain("key: 'mqtt'");
|
||||
expect(source).toContain("name: 'MQTT'");
|
||||
expect(source).toContain('dbType === "mqtt"');
|
||||
expect(source).toContain("return 'Broker / Topic Filter / QoS';");
|
||||
expect(source).toContain('return "mqtt://user:pass@127.0.0.1:1883/devices%2F%2B%2Ftelemetry?topology=cluster&clientId=gonavi-desktop&qos=1";');
|
||||
expect(source).toContain('return "topics=devices%2F%2B%2Ftelemetry,%24SYS%2F%23&clientId=gonavi-desktop&qos=1&cleanSession=true&fetchWaitMs=4000";');
|
||||
expect(source).toContain('t("connection.modal.messageQueue.mqtt.defaultTopicFilter.label")');
|
||||
});
|
||||
|
||||
it('exposes Kafka in the create-connection picker with broker and topic defaults', () => {
|
||||
expect(source).toContain("case 'kafka':");
|
||||
expect(source).toContain('return 9092;');
|
||||
expect(source).toContain("key: 'kafka'");
|
||||
expect(source).toContain("name: 'Kafka'");
|
||||
expect(source).toContain('dbType === "kafka"');
|
||||
expect(source).toContain("return 'Broker / Topic / Consumer Group';");
|
||||
expect(source).toContain('return "kafka://user:pass@127.0.0.1:9092,127.0.0.2:9092/orders.events?topology=cluster&groupId=analytics&mechanism=scram-sha-256";');
|
||||
expect(source).toContain('return "groupId=gonavi&mechanism=scram-sha-256&clientId=gonavi-desktop&startOffset=latest";');
|
||||
expect(source).toContain('t("connection.modal.messageQueue.kafka.defaultTopic.label")');
|
||||
});
|
||||
|
||||
it('exposes RabbitMQ in the create-connection picker with management-api and vhost defaults', () => {
|
||||
expect(source).toContain("case 'rabbitmq':");
|
||||
expect(source).toContain('return 15672;');
|
||||
expect(source).toContain('rabbitmq: ["rabbitmq", "http", "https"]');
|
||||
expect(source).toContain("key: 'rabbitmq'");
|
||||
expect(source).toContain("name: 'RabbitMQ'");
|
||||
expect(source).toContain('dbType === "rabbitmq"');
|
||||
expect(source).toContain("return 'Management API / Virtual Host / Queue';");
|
||||
expect(source).toContain('return "rabbitmq://guest:guest@127.0.0.1:15672/%2F?defaultQueue=orders.queue&exchange=events.topic&timeout=30";');
|
||||
expect(source).toContain('return "defaultQueue=orders.queue&exchange=events.topic&managementPathPrefix=/rabbitmq";');
|
||||
expect(source).toContain('t("connection.modal.messageQueue.rabbitmq.defaultVirtualHost.label")');
|
||||
});
|
||||
|
||||
it('exposes GaussDB in the create-connection picker with PostgreSQL-family defaults', () => {
|
||||
expect(source).toContain("case 'gaussdb':");
|
||||
expect(source).toContain('return 5432;');
|
||||
expect(source).toContain('gaussdb: ["gaussdb", "postgresql", "postgres"]');
|
||||
expect(source).toContain("key: 'gaussdb'");
|
||||
expect(source).toContain("name: 'GaussDB'");
|
||||
expect(source).toContain('type === "gaussdb"');
|
||||
expect(source).toContain('return "gaussdb://user:pass@127.0.0.1:5432/db_name";');
|
||||
expect(source).toContain('return "application_name=GoNavi&statement_timeout=30000";');
|
||||
expect(source).toContain('? "gaussdb"');
|
||||
expect(source).toContain('dbType === "gaussdb"');
|
||||
});
|
||||
|
||||
it('exposes GoldenDB in the create-connection picker with MySQL-compatible defaults', () => {
|
||||
expect(source).toContain("case 'goldendb':");
|
||||
expect(source).toContain('return 1523;');
|
||||
expect(source).toContain("key: 'goldendb'");
|
||||
expect(source).toContain("name: 'GoldenDB'");
|
||||
expect(source).toContain('type === "goldendb"');
|
||||
expect(source).toContain("'connection_modal.step1.hint.goldendb'");
|
||||
expect(source).toContain("'MySQL compatible / distributed transactions'");
|
||||
expect(source).toContain('dbType === "goldendb" ? "goldendb" : "mysql"');
|
||||
expect(source).toContain('type === "goldendb" ? "goldendb" : "mysql"');
|
||||
expect(source).toContain('? "goldendb"');
|
||||
});
|
||||
|
||||
it('keeps OceanBase Oracle service name optional for OBClient/MySQL-wire connections', () => {
|
||||
expect(source).toContain('connection.modal.field.oceanBaseServiceName.label');
|
||||
expect(source).toMatch(
|
||||
/isOceanBaseOracle\s*\?\s*\[\]\s*:\s*\[\s*createUriAwareRequiredRule\(\s*t\("connection\.modal\.field\.serviceName\.required"/,
|
||||
);
|
||||
expect(source).toContain('connection.modal.field.oceanBaseServiceName.help');
|
||||
expect(source).toContain('connection.modal.field.serviceName.help');
|
||||
expect(source).toContain('connection.modal.field.serviceName.required');
|
||||
expect(source).not.toContain('请输入 OceanBase Oracle 服务名');
|
||||
expect(source).not.toContain('Oracle 租户必须填写监听器注册的 SERVICE_NAME');
|
||||
});
|
||||
|
||||
it('uses localized message queue service, topology, and extra host copy', () => {
|
||||
[
|
||||
'label="默认 Topic(可选)"',
|
||||
'label="默认 Topic / Filter(可选)"',
|
||||
'label="默认 Virtual Host(可选)"',
|
||||
'label: "单 Broker"',
|
||||
'label: "单 NameServer"',
|
||||
'label="额外 Broker 地址"',
|
||||
'label="额外 NameServer 地址"',
|
||||
'help="可输入多个 broker 地址,格式:host:port(回车确认)"',
|
||||
'help="可输入多个 NameServer 地址,格式:host:port(回车确认)"',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'connection.modal.messageQueue.kafka.defaultTopic.help',
|
||||
'connection.modal.messageQueue.rocketmq.defaultTopic.help',
|
||||
'connection.modal.messageQueue.mqtt.defaultTopicFilter.help',
|
||||
'connection.modal.messageQueue.rabbitmq.defaultVirtualHost.help',
|
||||
'connection.modal.messageQueue.kafka.topology.single.label',
|
||||
'connection.modal.messageQueue.rocketmq.topology.single.label',
|
||||
'connection.modal.messageQueue.mqtt.topology.cluster.description',
|
||||
'connection.modal.messageQueue.kafka.extraBrokers.placeholder',
|
||||
'connection.modal.messageQueue.rocketmq.extraNameServers.placeholder',
|
||||
'connection.modal.messageQueue.mqtt.extraBrokers.placeholder',
|
||||
].forEach((key) => {
|
||||
expect(source).toContain(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConnectionModal Redis Sentinel configuration', () => {
|
||||
it('exposes Sentinel topology fields and safe defaults', () => {
|
||||
expect(source).toContain('connection.modal.redis.topology.sentinel.label');
|
||||
expect(source).toContain('name="redisSentinelMaster"');
|
||||
expect(source).toContain('connection.modal.redis.sentinel.master.label');
|
||||
expect(source).toContain('name="redisSentinelPassword"');
|
||||
expect(source).toContain('hasRedisSentinelPassword');
|
||||
expect(source).toContain('clearKey: "redisSentinelPassword"');
|
||||
expect(source).toContain('form.setFieldValue("port", 26379)');
|
||||
expect(source).toContain('form.setFieldValue("port", 6379)');
|
||||
});
|
||||
|
||||
it('uses localized Redis topology, sentinel, credential, and database-scope copy', () => {
|
||||
[
|
||||
'label: "单机模式"',
|
||||
'description: "只连接一个 Redis 节点。"',
|
||||
'label: "集群模式"',
|
||||
'description: "Redis Cluster,配置多个种子节点。"',
|
||||
'label: "哨兵模式"',
|
||||
'description: "通过 Sentinel 发现主节点,适合主从高可用。"',
|
||||
'? "Sentinel 附加节点地址"',
|
||||
': "集群附加节点地址"',
|
||||
'? "上方主机地址作为第一个 Sentinel;这里填写其他 Sentinel 节点,格式:host:port"',
|
||||
': "主节点使用上方主机地址;这里填写其他种子节点,格式:host:port"',
|
||||
'label="Sentinel master 名称"',
|
||||
'help="填写 Sentinel 配置中的 monitor 名称,例如 mymaster。"',
|
||||
'label="密码 (可选)"',
|
||||
'emptyPlaceholder: "Redis 密码(如果设置了 requirepass)"',
|
||||
'retainedLabel: "已保存 Redis 密码"',
|
||||
'label="Sentinel 用户名(可选)"',
|
||||
'placeholder="留空表示 Sentinel 不使用 ACL 用户名"',
|
||||
'label="Sentinel 密码(可选)"',
|
||||
'emptyPlaceholder: "Sentinel 自身认证密码,留空则不发送"',
|
||||
'retainedLabel: "已保存 Sentinel 密码"',
|
||||
'clearLabel: "清除已保存 Sentinel 密码"',
|
||||
'label="显示数据库 (留空显示全部)"',
|
||||
'help="连接测试成功后可选择"',
|
||||
'placeholder="选择显示的数据库"',
|
||||
].forEach((snippet) => {
|
||||
expect(redisSectionsSource).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'connection.modal.redis.topology.single.label',
|
||||
'connection.modal.redis.topology.cluster.description',
|
||||
'connection.modal.redis.topology.sentinel.label',
|
||||
'connection.modal.redis.hosts.sentinel.label',
|
||||
'connection.modal.redis.hosts.cluster.help',
|
||||
'connection.modal.redis.sentinel.master.required',
|
||||
'connection.modal.redis.credentials.primary.placeholder.empty',
|
||||
'connection.modal.redis.credentials.sentinelPassword.clear',
|
||||
'connection.modal.redis.databaseScope.placeholder',
|
||||
].forEach((key) => {
|
||||
expect(redisSectionsSource).toContain(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses localized Redis test feedback and optional-auth placeholders', () => {
|
||||
[
|
||||
'测试连接前请填写新的 Sentinel 密码,或取消清除已保存 Sentinel 密码',
|
||||
'连接成功但拉取 Redis 数据库列表超时',
|
||||
'连接成功,但获取 Redis 数据库列表失败',
|
||||
'未知错误',
|
||||
'? "未开启认证可留空"',
|
||||
].forEach((snippet) => {
|
||||
expect(connectionModalSource).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'connection.modal.secret.blocking.redis_sentinel',
|
||||
'connection.modal.test.redis_database_list_timeout',
|
||||
'connection.modal.test.redis_database_list_failure',
|
||||
'connection.modal.error.unknown',
|
||||
'connection.modal.field.username.optional_placeholder',
|
||||
].forEach((key) => {
|
||||
expect(source).toContain(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the saved host as the primary Redis node when editing multi-node configs', () => {
|
||||
expect(source).toContain('const savedPrimaryAddress = isFileDbConfigType');
|
||||
expect(source).toContain('savedPrimaryAddress,');
|
||||
expect(source).toContain('...(Array.isArray(config.hosts) ? config.hosts : [])');
|
||||
expect(source).toContain('const redisHosts =');
|
||||
expect(source).toContain('configType === "redis" ? normalizedHosts.slice(1) : [];');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConnectionModal MongoDB configuration', () => {
|
||||
it('keeps replica, SRV, and read preference fields in the split Mongo sections', () => {
|
||||
expect(source).toContain('ConnectionModalMongoSections');
|
||||
expect(source).toContain('name="mongoSrv"');
|
||||
expect(source).toContain('connection.modal.mongodb.discovery.srv_ssh_warning');
|
||||
expect(source).toContain('name="mongoReplicaPassword"');
|
||||
expect(source).toContain('clearKey: "mongoReplicaPassword"');
|
||||
expect(source).toContain('connection.modal.action.discover_members');
|
||||
expect(source).toContain('fieldName: "mongoReadPreference"');
|
||||
});
|
||||
|
||||
it('uses localized MongoDB topology, discovery, replica, and policy copy', () => {
|
||||
[
|
||||
'label: "单机模式"',
|
||||
'description: "只连接一个 MongoDB 节点。"',
|
||||
'label: "副本集 / 多节点"',
|
||||
'description: "配置副本集名称和多个候选节点。"',
|
||||
'label: "标准地址"',
|
||||
'description: "使用 host:port 直连或副本集节点列表。"',
|
||||
'label: "SRV 地址"',
|
||||
'description: "使用 mongodb+srv,由 DNS 发现目标节点。"',
|
||||
'<Tag color="blue">当前</Tag>',
|
||||
'message="SRV 与 SSH 隧道同时启用时,可能依赖本地 DNS 解析能力"',
|
||||
'label={mongoSrv ? "附加 SRV 主机(可选)" : "附加节点地址"}',
|
||||
'? "可输入多个候选主机名,格式:host;若留空则仅使用上方主机。"',
|
||||
': "可输入多个节点地址,格式:host:port(回车确认)"',
|
||||
'label="副本集名称(可选)"',
|
||||
'label="副本集用户名(可选)"',
|
||||
'placeholder="留空沿用主用户名"',
|
||||
'label="副本集密码(可选)"',
|
||||
'emptyPlaceholder: "留空沿用主密码"',
|
||||
'retainedLabel: "已保存副本集密码"',
|
||||
'clearLabel: "清除已保存副本集密码"',
|
||||
'当前已保存副本集密码。留空表示继续沿用,输入新值表示替换。',
|
||||
'自动发现成员',
|
||||
'title: "角色"',
|
||||
'title: "健康"',
|
||||
'? "正常" : "异常"',
|
||||
'label="认证库 (authSource)"',
|
||||
'placeholder="默认使用 database 或 admin"',
|
||||
'<Text strong>读偏好 (readPreference)</Text>',
|
||||
'description: "只读主节点。"',
|
||||
'description: "主节点优先。"',
|
||||
'description: "只读从节点。"',
|
||||
'description: "从节点优先。"',
|
||||
'description: "选择最近节点。"',
|
||||
].forEach((snippet) => {
|
||||
expect(mongoSectionsSource).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'connection.modal.mongodb.topology.single.label',
|
||||
'connection.modal.mongodb.discovery.standard.label',
|
||||
'connection.modal.mongodb.discovery.srv_ssh_warning',
|
||||
'connection.modal.mongodb.replica.hosts.srv.label',
|
||||
'connection.modal.mongodb.replica.password.description',
|
||||
'connection.modal.action.discover_members',
|
||||
'connection.modal.mongodb.members.role',
|
||||
'connection.modal.mongodb.policy.auth_source.label',
|
||||
'connection.modal.mongodb.read_preference.primary',
|
||||
].forEach((key) => {
|
||||
expect(mongoSectionsSource).toContain(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const componentFiles = [
|
||||
'./DataExportDialog.tsx',
|
||||
'./ExportProgressModal.tsx',
|
||||
'./TableExportWorkbench.tsx',
|
||||
'./useExportProgressRunner.ts',
|
||||
'../utils/tableExportTab.ts',
|
||||
] as const;
|
||||
|
||||
const localeFiles = [
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'en-US',
|
||||
'ja-JP',
|
||||
'de-DE',
|
||||
'ru-RU',
|
||||
] as const;
|
||||
|
||||
const sources = componentFiles.map((file) => readFileSync(new URL(file, import.meta.url), 'utf8'));
|
||||
const combinedSource = sources.join('\n');
|
||||
const userFacingSource = combinedSource
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
.replace(/'已取消'/g, '');
|
||||
const catalogs = Object.fromEntries(localeFiles.map((locale) => [
|
||||
locale,
|
||||
JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
|
||||
])) as Record<typeof localeFiles[number], Record<string, string>>;
|
||||
|
||||
const extractKeys = (source: string): string[] => (
|
||||
Array.from(new Set(source.match(/data_export(?:\.[a-z0-9_]+)+/g) || [])).sort()
|
||||
);
|
||||
|
||||
const placeholdersOf = (value: string): string[] => (
|
||||
Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), (match) => match[1]).sort()
|
||||
);
|
||||
|
||||
describe('data export i18n', () => {
|
||||
it('routes dialog, progress modal, and workbench copy through translation keys instead of inline Han literals', () => {
|
||||
expect(sources[0]).toContain("t('data_export.dialog.field.format')");
|
||||
expect(sources[1]).toContain("t('data_export.progress.title.error')");
|
||||
expect(sources[2]).toContain("t('data_export.workbench.title')");
|
||||
expect(sources[3]).toContain("t('data_export.progress.title.done')");
|
||||
expect(sources[3]).toContain("t('data_export.progress.title.error')");
|
||||
expect(sources[4]).toContain("t('data_export.workbench.scope.all.label')");
|
||||
expect(sources[4]).toContain("t('data_export.workbench.scope.all.description')");
|
||||
expect(sources[4]).toContain("t('data_export.progress.value.target_fallback')");
|
||||
expect(sources[4]).toContain("t('data_export.workbench.task.export_target'");
|
||||
expect(userFacingSource).not.toMatch(/\p{Script=Han}/u);
|
||||
});
|
||||
|
||||
it('keeps all extracted data_export keys present in every supported locale with matching placeholders', () => {
|
||||
const keys = extractKeys(combinedSource);
|
||||
const baseline = catalogs['zh-CN'];
|
||||
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
|
||||
keys.forEach((key) => {
|
||||
expect(baseline, `zh-CN:${key}`).toHaveProperty(key);
|
||||
const expectedPlaceholders = placeholdersOf(baseline[key]);
|
||||
localeFiles.forEach((locale) => {
|
||||
expect(catalogs[locale], `${locale}:${key}`).toHaveProperty(key);
|
||||
expect(placeholdersOf(catalogs[locale][key]), `${locale}:${key}`).toEqual(expectedPlaceholders);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const dataGridSource = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid auto commit delay i18n guards', () => {
|
||||
it('localizes auto commit delay option labels', () => {
|
||||
expect(dataGridSource).toContain("translateDataGrid('data_grid.toolbar.commit_delay.seconds', { seconds: item.seconds })");
|
||||
|
||||
[
|
||||
"label: '3 秒'",
|
||||
"label: '5 秒'",
|
||||
"label: '10 秒'",
|
||||
"label: '30 秒'",
|
||||
].forEach((legacyText) => {
|
||||
expect(dataGridSource).not.toContain(legacyText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const dataGridSource = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid auto commit i18n guards', () => {
|
||||
it('localizes auto commit toast wrappers while preserving raw details', () => {
|
||||
expect(dataGridSource).toContain("translateDataGrid('data_grid.message.auto_commit_success')");
|
||||
expect(dataGridSource).toContain("translateDataGrid('data_grid.message.auto_commit_failed', { detail: res.message })");
|
||||
|
||||
expect(dataGridSource).not.toContain("'自动提交成功'");
|
||||
expect(dataGridSource).not.toContain('`自动提交失败: ${res.message}`');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const legacyMenuSource = readFileSync(new URL('./DataGridLegacyCellContextMenu.tsx', import.meta.url), 'utf8');
|
||||
const v2MenuSource = readFileSync(new URL('./V2TableContextMenu.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid cell undo menu i18n guards', () => {
|
||||
it('localizes cell undo action labels in legacy and v2 menus', () => {
|
||||
[
|
||||
legacyMenuSource,
|
||||
v2MenuSource,
|
||||
].forEach((source) => {
|
||||
expect(source).toContain("data_grid.context_menu.undo_cell_change");
|
||||
expect(source).not.toContain('撤销此单元格修改');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const dataGridSource = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid cell undo i18n guards', () => {
|
||||
it('localizes cell undo toast wrappers', () => {
|
||||
[
|
||||
"translateDataGrid('data_grid.message.undo_added_row_hint')",
|
||||
"translateDataGrid('data_grid.message.undo_cell_original_missing')",
|
||||
"translateDataGrid('data_grid.message.undo_cell_success')",
|
||||
].forEach((expected) => {
|
||||
expect(dataGridSource).toContain(expected);
|
||||
});
|
||||
|
||||
[
|
||||
'新增行请使用删除选中或整表回滚撤销',
|
||||
'未找到该单元格的原始数据,无法撤销',
|
||||
'已撤销单元格修改',
|
||||
].forEach((legacyText) => {
|
||||
expect(dataGridSource).not.toContain(legacyText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const dataGridSource = readFileSync(new URL('./DataGridShell.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid embedded designer title i18n guards', () => {
|
||||
it('localizes the embedded table designer tab title while preserving the raw table name parameter', () => {
|
||||
expect(dataGridSource).toContain("translateDataGrid('data_grid.embedded_designer.title'");
|
||||
expect(dataGridSource).toContain('tableName: tableName ||');
|
||||
expect(dataGridSource).not.toContain('title: `设计表 (${tableName || \'\'}');
|
||||
});
|
||||
|
||||
it('keeps the embedded designer title key in every locale catalog with the tableName placeholder', () => {
|
||||
(['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const).forEach((locale) => {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
|
||||
expect(catalog['data_grid.embedded_designer.title']).toEqual(expect.any(String));
|
||||
expect(catalog['data_grid.embedded_designer.title']).toContain('{{tableName}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const actionsSource = readFileSync(new URL('./useDataGridV2Actions.ts', import.meta.url), 'utf8');
|
||||
const gridSource = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid export columns', () => {
|
||||
it('offers result columns in the export dialog and forwards the selected order', () => {
|
||||
expect(actionsSource).toContain('availableColumns: displayOutputColumnNames');
|
||||
expect(actionsSource).toContain('columns: values.columns');
|
||||
});
|
||||
|
||||
it('uses selected columns for local row projection and backend ExportData arguments', () => {
|
||||
expect(gridSource).toContain('resolveDataExportColumns(options.columns, displayOutputColumnNames)');
|
||||
expect(gridSource).toContain('pickDataGridOutputRows(rows, exportColumns)');
|
||||
expect(gridSource).toMatch(/ExportDataWithOptions\(\s*cleanRows,\s*exportColumns,/);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const rowNumberAriaKey = 'data_grid.aria.row_number';
|
||||
|
||||
describe('DataGrid row number i18n', () => {
|
||||
it('localizes the row number column aria label', () => {
|
||||
expect(source).toContain(`aria-label={translateDataGrid('${rowNumberAriaKey}')}`);
|
||||
expect(source).not.toContain('aria-label="行号"');
|
||||
});
|
||||
|
||||
it('keeps the row number aria label available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog[rowNumberAriaKey], `${locale}:${rowNumberAriaKey}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const secondaryActionsSource = readFileSync(new URL('./DataGridSecondaryActions.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGrid secondary actions i18n guards', () => {
|
||||
it('localizes the object design action label', () => {
|
||||
expect(secondaryActionsSource).toContain("translate('data_grid.secondary.object_design')");
|
||||
expect(secondaryActionsSource).not.toContain("'对象设计'");
|
||||
expect(secondaryActionsSource).not.toContain('>对象设计<');
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const toolbarSource = readFileSync(new URL('./DataGridToolbarFrame.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('DataGridToolbarFrame i18n guards', () => {
|
||||
it('localizes data edit commit mode controls', () => {
|
||||
[
|
||||
'data_grid.toolbar.commit_mode.tooltip',
|
||||
'data_grid.toolbar.commit_mode.manual',
|
||||
'data_grid.toolbar.commit_mode.auto',
|
||||
'data_grid.toolbar.commit_mode.auto_countdown',
|
||||
].forEach((key) => {
|
||||
expect(toolbarSource).toContain(`translate('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'控制表数据编辑后的提交方式',
|
||||
"label: '手动提交'",
|
||||
"label: '自动提交'",
|
||||
's 后提交',
|
||||
].forEach((legacyText) => {
|
||||
expect(toolbarSource).not.toContain(legacyText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'];
|
||||
const requiredKeys = [
|
||||
'sidebar.action.data_import',
|
||||
'data_import.workbench.title',
|
||||
'data_import.workbench.description',
|
||||
'data_import.workbench.description.database',
|
||||
'data_import.workbench.section.target',
|
||||
'data_import.workbench.mode.table',
|
||||
'data_import.workbench.mode.database',
|
||||
'data_import.workbench.label.connection',
|
||||
'data_import.workbench.label.database',
|
||||
'data_import.workbench.label.default_database',
|
||||
'data_import.workbench.label.table',
|
||||
'data_import.workbench.label.file',
|
||||
'data_import.workbench.label.sql_file',
|
||||
'data_import.workbench.placeholder.select_connection',
|
||||
'data_import.workbench.placeholder.loading_databases',
|
||||
'data_import.workbench.placeholder.select_database',
|
||||
'data_import.workbench.placeholder.select_default_database',
|
||||
'data_import.workbench.placeholder.select_database_first',
|
||||
'data_import.workbench.placeholder.loading_tables',
|
||||
'data_import.workbench.placeholder.select_table',
|
||||
'data_import.workbench.action.select_file',
|
||||
'data_import.workbench.action.change_file',
|
||||
'data_import.workbench.action.select_sql_file',
|
||||
'data_import.workbench.action.change_sql_file',
|
||||
'data_import.workbench.action.start_database_import',
|
||||
'data_import.workbench.action.retry_database_import',
|
||||
'data_import.workbench.action.cancel_database_import',
|
||||
'data_import.workbench.helper.file_formats',
|
||||
'data_import.workbench.helper.sql_file',
|
||||
'data_import.workbench.notice.partial_execution',
|
||||
'data_import.workbench.notice.gonavi_mysql_restore',
|
||||
'data_import.workbench.state.awaiting_file_title',
|
||||
'data_import.workbench.state.awaiting_file_description',
|
||||
'data_import.workbench.state.awaiting_sql_title',
|
||||
'data_import.workbench.state.awaiting_sql_description',
|
||||
'data_import.workbench.state.ready_sql_title',
|
||||
'data_import.workbench.state.ready_sql_description',
|
||||
'data_import.workbench.state.running',
|
||||
'data_import.workbench.state.cancelling',
|
||||
'data_import.workbench.state.completed',
|
||||
'data_import.workbench.state.failed',
|
||||
'data_import.workbench.state.cancelled',
|
||||
'data_import.workbench.progress.statements',
|
||||
'data_import.workbench.progress.bytes',
|
||||
'data_import.workbench.message.load_databases_failed',
|
||||
'data_import.workbench.message.load_tables_failed',
|
||||
'data_import.workbench.message.select_file_failed',
|
||||
'data_import.workbench.message.import_done',
|
||||
'data_import.workbench.message.database_import_done',
|
||||
'data_import.workbench.message.database_import_failed',
|
||||
'data_import.workbench.message.database_import_cancelled',
|
||||
'tab_manager.kind_badge.data_import',
|
||||
'tab_manager.hover.kind.data_import',
|
||||
];
|
||||
|
||||
describe('DataImportWorkbench i18n', () => {
|
||||
it('keeps the import workbench contract available across locales', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(
|
||||
new URL("./DataSyncModal.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
describe("DataSyncModal i18n", () => {
|
||||
it("localizes fixed workflow chrome while preserving raw table and SQL details as params", () => {
|
||||
[
|
||||
"差异分析完成",
|
||||
"确认全量覆盖",
|
||||
"全量覆盖会清空目标表数据后再插入,请确认已备份目标库。",
|
||||
"跨库迁移工作台",
|
||||
"数据同步工作台",
|
||||
"请选择需要同步的表:",
|
||||
"差异预览:",
|
||||
"SQL 已复制",
|
||||
"复制失败,请手动复制",
|
||||
"复制 SQL",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain("useOptionalI18n()");
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.message\.analysis_complete\1\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.modal\.full_overwrite_title\1\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.preview\.title\1,\s*\{\s*table:\s*previewTable\s*\}\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.preview\.message\.sql_copied\1\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.preview\.message\.copy_failed\1\s*\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("wraps backend details in localized shells without translating raw detail values", () => {
|
||||
expect(source).not.toContain(
|
||||
'message.error(res.message || "差异分析失败")',
|
||||
);
|
||||
expect(source).not.toContain(
|
||||
'message.error("差异分析失败: " + (e?.message || ""))',
|
||||
);
|
||||
expect(source).not.toContain(
|
||||
'message.error(res.message || "加载差异预览失败")',
|
||||
);
|
||||
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.message\.analysis_failed_detail\1,\s*\{\s*detail:/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.message\.preview_load_failed_detail\1,\s*\{\s*detail:/,
|
||||
);
|
||||
expect(source).toMatch(/tr\(\s*(['"])data_sync\.field\.schema\1\s*\)/);
|
||||
expect(source).toMatch(
|
||||
/t\(\s*(['"])data_sync\.message\.fetch_target_schemas_failed_detail\1,\s*\{\s*detail:/,
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes the next-step selection guards before loading table lists', () => {
|
||||
expect(source).not.toContain('message.error("Select connections first")');
|
||||
expect(source).not.toContain('message.error("Select source database")');
|
||||
expect(source).not.toContain('message.error("Select target database")');
|
||||
|
||||
expect(source).toContain("message.error(tr('data_sync.message.select_connections_first'))");
|
||||
expect(source).toContain("message.error(tr('data_sync.message.select_source_database'))");
|
||||
expect(source).toContain("message.error(tr('data_sync.message.select_target_database'))");
|
||||
});
|
||||
|
||||
it('localizes compare-entry only chrome without translating SQL preview or raw table names', () => {
|
||||
[
|
||||
"当前入口只做差异分析和预览",
|
||||
"按表比对",
|
||||
"按 SQL 结果集比对",
|
||||
"当前为“表结构比对”入口",
|
||||
"当前为“数据比对”入口",
|
||||
"生成目标表缺失字段的兼容变更 SQL",
|
||||
"正在比对",
|
||||
"比对完成",
|
||||
"比对失败",
|
||||
"当前阶段:",
|
||||
"成功比对 ",
|
||||
"分析日志",
|
||||
"返回比对",
|
||||
"行选择只影响 SQL 预览范围",
|
||||
"SQL 预览会按当前勾选的插入/更新/删除",
|
||||
"SQL 预览展示结构差异建议语句",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.compare_entry\.workflow_help\1\s*\)/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.compare_entry\.option\.source_dataset\.table\1/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.compare_entry\.result\.running_description\1/,
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/tr\(\s*(['"])data_sync\.compare_entry\.preview\.sql\.data_help\1/,
|
||||
);
|
||||
});
|
||||
|
||||
it('hides the modal hero when embedded in the tool center', () => {
|
||||
expect(source).toContain('{!embedded && (');
|
||||
expect(source).toContain('<div style={heroPanelStyle}>');
|
||||
expect(source).toMatch(/embedded\s*\?\s*\(\s*dataSyncContent\s*\)/);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('DefinitionViewer i18n', () => {
|
||||
it('keeps DefinitionViewer shell and validation copy localized', () => {
|
||||
const source = readFileSync(new URL('./DefinitionViewer.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).not.toMatch(/setError\('未找到数据库连接'|setError\('视图名称为空'|setError\('事件名称为空'|setError\('函数\/存储过程名称为空'/);
|
||||
expect(source).not.toMatch(/setError\(result\.message \|\| '查询定义失败'|setError\('查询定义失败: '/);
|
||||
expect(source).not.toMatch(/<Spin tip=\{`加载|message="加载失败"|>数据库:|>类型:/);
|
||||
expect(source).not.toMatch(/objectLabel = tab\.viewKind === 'materialized' \? '物化视图' : '视图'|objectLabel = '事件'|objectLabel = '函数\/存储过程'/);
|
||||
expect(source).not.toContain('对象修改');
|
||||
expect(source).not.toContain('刷新最新定义失败');
|
||||
expect(source).not.toContain('title: `修改${objectLabel}: ${normalizedObjectName}`');
|
||||
|
||||
expect(source).toContain('definition_viewer.error.connection_not_found');
|
||||
expect(source).toContain('definition_viewer.error.view_name_empty');
|
||||
expect(source).toContain('definition_viewer.error.event_name_empty');
|
||||
expect(source).toContain('definition_viewer.error.routine_name_empty');
|
||||
expect(source).toContain('definition_viewer.error.query_failed');
|
||||
expect(source).toContain('definition_viewer.error.query_failed_detail');
|
||||
expect(source).toContain('definition_viewer.loading.view_definition');
|
||||
expect(source).toContain('definition_viewer.field.database');
|
||||
expect(source).toContain('definition_viewer.field.type');
|
||||
expect(source).toContain('definition_viewer.action.edit_object');
|
||||
expect(source).toContain('definition_viewer.warning.refresh_latest_failed');
|
||||
expect(source).toContain('definition_viewer.edit.tab_title');
|
||||
});
|
||||
|
||||
it('keeps DefinitionViewer editor fallback comments localized', () => {
|
||||
const source = readFileSync(new URL('./DefinitionViewer.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).not.toMatch(/暂不支持该数据库类型的视图定义查看|SQLite 不支持函数\/存储过程定义管理|暂不支持该数据库类型的函数\/存储过程定义查看|暂不支持该数据库类型的事件定义查看/);
|
||||
expect(source).not.toMatch(/未找到视图定义|未找到函数\/存储过程定义|未找到事件定义|暂不支持该对象定义查看/);
|
||||
expect(source).not.toMatch(/当前数据源未返回可执行定义文本|当前数据源未返回完整 CREATE EVENT 语句|名称: |类型: /);
|
||||
expect(source).not.toMatch(/当前 Sphinx 实例|已执行多套兼容查询|返回失败信息: |unknown error/);
|
||||
expect(source).not.toContain('修改${objectLabel}');
|
||||
expect(source).not.toContain('请确认语法兼容当前数据库后执行');
|
||||
expect(source).not.toContain('当前对象定义为空,请补全');
|
||||
expect(source).not.toContain('/^\\s*--\\s*(未找到|暂不支持|当前)/');
|
||||
|
||||
expect(source).toContain('definition_viewer.editor.unsupported_view_definition');
|
||||
expect(source).toContain('definition_viewer.editor.unsupported_sqlite_routine_definition');
|
||||
expect(source).toContain('definition_viewer.editor.unsupported_routine_definition');
|
||||
expect(source).toContain('definition_viewer.editor.event_definition_not_found');
|
||||
expect(source).toContain('definition_viewer.editor.sphinx.failed_message_unknown');
|
||||
expect(source).toContain('definition_viewer.edit.comment_title');
|
||||
expect(source).toContain('definition_viewer.edit.comment_compatibility');
|
||||
expect(source).toContain('definition_viewer.edit.comment_empty_definition');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const dataGridSource = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
const tableOverviewSource = readFileSync(new URL('./TableOverview.tsx', import.meta.url), 'utf8');
|
||||
const sidebarObjectActionsSource = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('export title i18n guards', () => {
|
||||
it('keeps export progress and export tab titles on translation keys instead of inline Chinese copy', () => {
|
||||
[
|
||||
"`导出 ${tableName || '数据'}`",
|
||||
"`导出 ${tableName}`",
|
||||
].forEach((rawSnippet) => {
|
||||
expect(dataGridSource).not.toContain(rawSnippet);
|
||||
expect(tableOverviewSource).not.toContain(rawSnippet);
|
||||
expect(sidebarObjectActionsSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
expect(dataGridSource).toContain("translateDataGrid('file.backend.dialog.export_data')");
|
||||
expect(dataGridSource).toContain("translateDataGrid('file.backend.dialog.export_table'");
|
||||
expect(tableOverviewSource).toContain("t('file.backend.dialog.export_table'");
|
||||
expect(sidebarObjectActionsSource).toContain("t('file.backend.dialog.export_table'");
|
||||
});
|
||||
});
|
||||
@@ -1,169 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const modalSource = readFileSync(new URL('./MessagePublishModal.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('MessagePublishModal i18n shell guards', () => {
|
||||
it('localizes the modal shell and send failure wrappers while preserving raw details', () => {
|
||||
[
|
||||
'message_publish_modal.title',
|
||||
'message_publish_modal.title_with_connection',
|
||||
'message_publish_modal.action.send',
|
||||
'message_publish_modal.error.build_command_failed',
|
||||
'message_publish_modal.error.send_failed_detail',
|
||||
'message_publish_modal.error.unknown_error',
|
||||
].forEach((key) => {
|
||||
expect(modalSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
expect(modalSource).toContain('connectionName: connection.name');
|
||||
expect(modalSource).toContain('detail: res?.message');
|
||||
expect(modalSource).toContain('detail: error?.message || String(error)');
|
||||
expect(modalSource).toContain('destroyOnHidden');
|
||||
// Closed modals unmount Form; never call form APIs while open === false.
|
||||
expect(modalSource).not.toContain('form.resetFields()');
|
||||
expect(modalSource).not.toContain('测试发送消息');
|
||||
expect(modalSource).not.toContain('okText="发送"');
|
||||
expect(modalSource).not.toContain('发送失败:');
|
||||
expect(modalSource).not.toContain('未知错误');
|
||||
expect(modalSource).not.toContain('构造发送命令失败');
|
||||
});
|
||||
|
||||
it('localizes the fixed form chrome without translating raw protocol terms', () => {
|
||||
[
|
||||
'message_publish_modal.field.exchange.label',
|
||||
'message_publish_modal.field.exchange.extra',
|
||||
'message_publish_modal.field.exchange.placeholder',
|
||||
'message_publish_modal.field.routing_key.label',
|
||||
'message_publish_modal.field.routing_key.extra',
|
||||
'message_publish_modal.field.routing_key.placeholder',
|
||||
'message_publish_modal.field.qos.extra',
|
||||
'message_publish_modal.field.retain.label',
|
||||
'message_publish_modal.field.tag.label',
|
||||
'message_publish_modal.field.tag.extra',
|
||||
'message_publish_modal.field.delay_level.label',
|
||||
'message_publish_modal.field.delay_level.extra',
|
||||
'message_publish_modal.field.body_mode.label',
|
||||
'message_publish_modal.field.body.label',
|
||||
'message_publish_modal.field.body.required',
|
||||
'message_publish_modal.field.body.extra',
|
||||
'message_publish_modal.field.body.placeholder',
|
||||
'message_publish_modal.field.headers.label',
|
||||
'message_publish_modal.field.headers.extra',
|
||||
'message_publish_modal.field.properties.label',
|
||||
'message_publish_modal.field.properties.extra',
|
||||
'message_publish_modal.option.no_delay',
|
||||
'message_publish_modal.option.text',
|
||||
'message_publish_modal.footer.success_prefix',
|
||||
'message_publish_modal.footer.success_suffix',
|
||||
].forEach((key) => {
|
||||
expect(modalSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'不延时',
|
||||
'Exchange(可选)',
|
||||
'留空使用默认交换机',
|
||||
'Routing Key(可选)',
|
||||
'留空时默认使用当前 Queue 名',
|
||||
'0 为至多一次',
|
||||
'Retain 消息',
|
||||
'Tag(可选)',
|
||||
'Delay Level(可选)',
|
||||
'RocketMQ 使用固定延时级别',
|
||||
'文本',
|
||||
'消息体类型',
|
||||
'消息体',
|
||||
'请输入消息体',
|
||||
'JSON 模式下需输入合法 JSON',
|
||||
'Headers(可选)',
|
||||
'需为 JSON 对象',
|
||||
'Properties(可选)',
|
||||
'发送成功后会返回',
|
||||
'用于确认本次测试消息是否已提交',
|
||||
].forEach((legacyText) => {
|
||||
expect(modalSource).not.toContain(legacyText);
|
||||
});
|
||||
|
||||
expect(modalSource).toContain('affectedRows');
|
||||
});
|
||||
|
||||
it('keeps the modal shell keys in every locale catalog with matching placeholders', () => {
|
||||
(['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const).forEach((locale) => {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
|
||||
[
|
||||
'message_publish_modal.title',
|
||||
'message_publish_modal.action.send',
|
||||
'message_publish_modal.error.build_command_failed',
|
||||
'message_publish_modal.error.unknown_error',
|
||||
].forEach((key) => {
|
||||
expect(catalog[key]).toEqual(expect.any(String));
|
||||
expect(catalog[key].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(catalog['message_publish_modal.title_with_connection']).toContain('{{connectionName}}');
|
||||
expect(catalog['message_publish_modal.error.send_failed_detail']).toContain('{{detail}}');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the fixed form chrome keys in every locale catalog', () => {
|
||||
(['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const).forEach((locale) => {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
|
||||
[
|
||||
'message_publish_modal.field.exchange.label',
|
||||
'message_publish_modal.field.exchange.extra',
|
||||
'message_publish_modal.field.exchange.placeholder',
|
||||
'message_publish_modal.field.routing_key.label',
|
||||
'message_publish_modal.field.routing_key.extra',
|
||||
'message_publish_modal.field.routing_key.placeholder',
|
||||
'message_publish_modal.field.qos.extra',
|
||||
'message_publish_modal.field.retain.label',
|
||||
'message_publish_modal.field.tag.label',
|
||||
'message_publish_modal.field.tag.extra',
|
||||
'message_publish_modal.field.delay_level.label',
|
||||
'message_publish_modal.field.delay_level.extra',
|
||||
'message_publish_modal.field.body_mode.label',
|
||||
'message_publish_modal.field.body.label',
|
||||
'message_publish_modal.field.body.required',
|
||||
'message_publish_modal.field.body.extra',
|
||||
'message_publish_modal.field.body.placeholder',
|
||||
'message_publish_modal.field.headers.label',
|
||||
'message_publish_modal.field.headers.extra',
|
||||
'message_publish_modal.field.properties.label',
|
||||
'message_publish_modal.field.properties.extra',
|
||||
'message_publish_modal.option.no_delay',
|
||||
'message_publish_modal.option.text',
|
||||
'message_publish_modal.footer.success_prefix',
|
||||
'message_publish_modal.footer.success_suffix',
|
||||
].forEach((key) => {
|
||||
expect(catalog[key]).toEqual(expect.any(String));
|
||||
expect(catalog[key].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
[
|
||||
['message_publish_modal.field.exchange.label', 'Exchange'],
|
||||
['message_publish_modal.field.routing_key.label', 'Routing Key'],
|
||||
['message_publish_modal.field.qos.extra', 'at most once'],
|
||||
['message_publish_modal.field.qos.extra', 'at least once'],
|
||||
['message_publish_modal.field.qos.extra', 'exactly once'],
|
||||
['message_publish_modal.field.retain.label', 'Retain'],
|
||||
['message_publish_modal.field.tag.label', 'Tag'],
|
||||
['message_publish_modal.field.delay_level.label', 'Delay Level'],
|
||||
['message_publish_modal.field.delay_level.extra', 'RocketMQ'],
|
||||
['message_publish_modal.field.body.extra', 'JSON'],
|
||||
['message_publish_modal.field.headers.label', 'Headers'],
|
||||
['message_publish_modal.field.properties.label', 'Properties'],
|
||||
['message_publish_modal.field.headers.extra', '{{example}}'],
|
||||
['message_publish_modal.field.properties.extra', '{{example}}'],
|
||||
].forEach(([key, rawTerm]) => {
|
||||
expect(catalog[key]).toContain(rawTerm);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
|
||||
const queryEditorHelpersSource = readFileSync(new URL('./queryEditor/QueryEditorHelpers.ts', import.meta.url), 'utf8');
|
||||
|
||||
describe('QueryEditor i18n source guards', () => {
|
||||
it('does not keep legacy builtin SQL function completion details in component source', () => {
|
||||
expect(queryEditorSource).not.toContain('const SQL_FUNCTIONS');
|
||||
expect(queryEditorSource).not.toContain("detail: '聚合 - 计数'");
|
||||
expect(queryEditorSource).not.toContain("detail: '字符串 - 拼接'");
|
||||
expect(queryEditorSource).not.toContain("detail: '日期 - 当前日期时间'");
|
||||
expect(queryEditorSource).not.toContain("detail: 'JSON - 提取值'");
|
||||
expect(queryEditorSource).not.toContain("detail: '窗口 - 行号'");
|
||||
});
|
||||
|
||||
it('uses a localized warning for pending managed transactions', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.transaction.message.pending_managed_transaction');
|
||||
expect(queryEditorSource).not.toContain('当前 SQL 编辑器已有未提交事务,请先提交或回滚后再执行新的增删改语句。');
|
||||
});
|
||||
|
||||
it('uses localized labels for SQL format restore chrome', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.format.restore_last_format');
|
||||
expect(queryEditorSource).toContain('query_editor.message.no_format_restore_snapshot');
|
||||
expect(queryEditorSource).toContain('query_editor.message.format_restore_success');
|
||||
expect(queryEditorSource).not.toContain('没有可还原的美化前 SQL');
|
||||
expect(queryEditorSource).not.toContain('已还原到美化前 SQL');
|
||||
expect(queryEditorSource).not.toContain('还原上次美化');
|
||||
});
|
||||
|
||||
it('uses localized wrappers for result pagination feedback', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.message.page_query_failed');
|
||||
expect(queryEditorSource).toContain('query_editor.message.page_query_empty');
|
||||
expect(queryEditorSource).not.toContain('翻页失败: ');
|
||||
expect(queryEditorSource).not.toContain('翻页未返回结果集');
|
||||
});
|
||||
|
||||
it('routes editor search through a localized Monaco action', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.action.find_in_editor');
|
||||
expect(queryEditorSource).toContain('gonavi:find-active-query');
|
||||
expect(queryEditorSource).toContain("editor.getAction?.('actions.find')");
|
||||
expect(queryEditorSource).toContain('addExtraSpaceOnTop: false');
|
||||
});
|
||||
|
||||
it('uses a localized wrapper for save query failures', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.message.save_query_failed');
|
||||
expect(queryEditorSource).not.toContain('保存查询失败: ');
|
||||
});
|
||||
|
||||
it('uses a localized AI diagnosis prompt wrapper', () => {
|
||||
expect(queryEditorSource).toContain('query_editor.ai_prompt.diagnose');
|
||||
expect(queryEditorSource).not.toContain('我在执行以下 SQL 时遇到了错误');
|
||||
expect(queryEditorSource).not.toContain('数据库报错信息如下');
|
||||
expect(queryEditorSource).not.toContain('请帮我分析错误原因,并给出修改建议。');
|
||||
});
|
||||
|
||||
it('uses a localized read-only reason for system metadata query results', () => {
|
||||
expect(queryEditorHelpersSource).toContain('query_editor.message.read_only_system_metadata');
|
||||
expect(queryEditorHelpersSource).not.toContain('系统元数据查询结果保持只读。');
|
||||
});
|
||||
|
||||
it('does not keep the index metadata internal fallback in Chinese', () => {
|
||||
expect(queryEditorHelpersSource).toContain('Failed to load indexes');
|
||||
expect(queryEditorHelpersSource).not.toContain('加载索引失败');
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
|
||||
const appCss = readFileSync(new URL('../App.css', import.meta.url), 'utf8');
|
||||
|
||||
describe('QueryEditor legacy layout', () => {
|
||||
it('keeps the legacy Monaco editor inside a flexing stage and shell', () => {
|
||||
expect(queryEditorSource).toContain("className={isV2Ui ? 'gn-v2-query-monaco-shell gn-query-monaco-shell' : 'gn-query-monaco-shell'}");
|
||||
expect(appCss).toMatch(/\.gn-query-monaco-stage\s*\{[\s\S]*?position:\s*relative;[\s\S]*?display:\s*flex;[\s\S]*?flex-direction:\s*column;[\s\S]*?min-height:\s*0;[\s\S]*?overflow:\s*hidden;/);
|
||||
expect(appCss).toMatch(/\.gn-query-monaco-shell\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?min-height:\s*0;[\s\S]*?min-width:\s*0;/);
|
||||
});
|
||||
});
|
||||
@@ -1,410 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const
|
||||
const catalogs = Object.fromEntries(locales.map((locale) => [
|
||||
locale,
|
||||
JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
|
||||
])) as Record<typeof locales[number], Record<string, string>>
|
||||
|
||||
const sqlAnalysisWorkbenchSource = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
|
||||
const explainWorkbenchSource = readFileSync(new URL('./explain/ExplainWorkbench.tsx', import.meta.url), 'utf8')
|
||||
const slowQueryPanelSource = readFileSync(new URL('./explain/SlowQueryPanel.tsx', import.meta.url), 'utf8')
|
||||
const explainGraphSource = readFileSync(new URL('./explain/ExplainGraph.tsx', import.meta.url), 'utf8')
|
||||
const explainSidebarSource = readFileSync(new URL('./explain/ExplainSidebar.tsx', import.meta.url), 'utf8')
|
||||
const slowQueryRailButtonSource = readFileSync(new URL('./sidebar/SlowQueryRailButton.tsx', import.meta.url), 'utf8')
|
||||
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8')
|
||||
|
||||
const stripLineComments = (source: string): string => (
|
||||
source.replace(/^\s*\/\/.*$/gm, '')
|
||||
)
|
||||
|
||||
const sqlAnalysisWorkbenchRuntimeSource = stripLineComments(sqlAnalysisWorkbenchSource)
|
||||
const explainWorkbenchRuntimeSource = stripLineComments(explainWorkbenchSource)
|
||||
const slowQueryPanelRuntimeSource = stripLineComments(slowQueryPanelSource)
|
||||
const explainGraphRuntimeSource = stripLineComments(explainGraphSource)
|
||||
const explainSidebarRuntimeSource = stripLineComments(explainSidebarSource)
|
||||
const slowQueryRailButtonRuntimeSource = stripLineComments(slowQueryRailButtonSource)
|
||||
|
||||
const placeholdersOf = (value: string): string[] => (
|
||||
Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), (match) => match[1]).sort()
|
||||
)
|
||||
|
||||
const requiredKeys = [
|
||||
'sql_analysis.workbench.validation.sql_required',
|
||||
'sql_analysis.workbench.alert.connection_missing_title',
|
||||
'sql_analysis.workbench.alert.connection_missing_description',
|
||||
'sql_analysis.workbench.title',
|
||||
'sql_analysis.workbench.view.slow_query',
|
||||
'sql_analysis.workbench.view.diagnose',
|
||||
'sql_analysis.workbench.editor.placeholder',
|
||||
'sql_analysis.workbench.editor.aria_label',
|
||||
'sql_analysis.workbench.editor.hint',
|
||||
'sql_analysis.workbench.action.run',
|
||||
'sql_analysis.explain.error.query_required',
|
||||
'sql_analysis.explain.error.run_failed',
|
||||
'sql_analysis.explain.loading',
|
||||
'sql_analysis.explain.error.title',
|
||||
'sql_analysis.explain.action.retry',
|
||||
'sql_analysis.explain.empty',
|
||||
'sql_analysis.explain.view.plan',
|
||||
'sql_analysis.explain.view.raw',
|
||||
'sql_analysis.explain.meta.node_count',
|
||||
'sql_analysis.explain.raw.empty',
|
||||
'sql_analysis.explain_graph.label.table',
|
||||
'sql_analysis.explain_graph.label.index',
|
||||
'sql_analysis.explain_graph.metric.est_rows',
|
||||
'sql_analysis.explain_graph.metric.actual_rows',
|
||||
'sql_analysis.explain_graph.metric.cost',
|
||||
'sql_analysis.explain_graph.flag.full_scan',
|
||||
'sql_analysis.explain_graph.flag.filesort',
|
||||
'sql_analysis.explain_graph.flag.temp_table',
|
||||
'sql_analysis.sidebar.stats.title',
|
||||
'sql_analysis.sidebar.stats.total_cost',
|
||||
'sql_analysis.sidebar.stats.total_duration',
|
||||
'sql_analysis.sidebar.stats.rows_read',
|
||||
'sql_analysis.sidebar.stats.buffer_hit',
|
||||
'sql_analysis.sidebar.stats.max_est_rows',
|
||||
'sql_analysis.sidebar.warning.full_scan',
|
||||
'sql_analysis.sidebar.warning.filesort',
|
||||
'sql_analysis.sidebar.warning.temp_table',
|
||||
'sql_analysis.sidebar.node.title',
|
||||
'sql_analysis.sidebar.node.op_type',
|
||||
'sql_analysis.sidebar.node.op_detail',
|
||||
'sql_analysis.sidebar.node.table',
|
||||
'sql_analysis.sidebar.node.index',
|
||||
'sql_analysis.sidebar.node.est_rows',
|
||||
'sql_analysis.sidebar.node.actual_rows',
|
||||
'sql_analysis.sidebar.node.loops',
|
||||
'sql_analysis.sidebar.node.cost',
|
||||
'sql_analysis.sidebar.node.duration',
|
||||
'sql_analysis.sidebar.node.buffer_hit',
|
||||
'sql_analysis.sidebar.node.flags',
|
||||
'sql_analysis.sidebar.node.extra',
|
||||
'sql_analysis.sidebar.suggestions.title',
|
||||
'sql_analysis.sidebar.suggestions.empty',
|
||||
'sql_analysis.sidebar.suggestions.rows',
|
||||
'sql_analysis.sidebar.suggestions.table',
|
||||
'sql_analysis.slow_query.error.load_failed',
|
||||
'sql_analysis.slow_query.error.copy_failed',
|
||||
'sql_analysis.slow_query.message.cleared',
|
||||
'sql_analysis.slow_query.message.copied',
|
||||
'sql_analysis.slow_query.not_diagnosable',
|
||||
'sql_analysis.slow_query.error.clear_failed',
|
||||
'sql_analysis.slow_query.action.copy',
|
||||
'sql_analysis.slow_query.action.load',
|
||||
'sql_analysis.slow_query.action.load_more',
|
||||
'sql_analysis.slow_query.action.retry',
|
||||
'sql_analysis.slow_query.action.view_full',
|
||||
'sql_analysis.slow_query.clear_confirm.title',
|
||||
'sql_analysis.slow_query.clear_confirm.description',
|
||||
'sql_analysis.slow_query.details.title',
|
||||
'sql_analysis.slow_query.search.placeholder',
|
||||
'sql_analysis.slow_query.search.aria_label',
|
||||
'sql_analysis.slow_query.search.empty',
|
||||
'sql_analysis.slow_query.scope_note',
|
||||
'sql_analysis.slow_query.sort.aria_label',
|
||||
'sql_analysis.slow_query.sort.duration',
|
||||
'sql_analysis.slow_query.sort.frequency',
|
||||
'sql_analysis.slow_query.sort.rows_returned',
|
||||
'sql_analysis.slow_query.sort.recent',
|
||||
'sql_analysis.slow_query.tooltip.clear_current',
|
||||
'sql_analysis.slow_query.loading',
|
||||
'sql_analysis.slow_query.error.title',
|
||||
'sql_analysis.slow_query.empty',
|
||||
'sql_analysis.slow_query.title',
|
||||
'sql_analysis.slow_query.current_connection',
|
||||
'sql_analysis.slow_query.metric.rows_read',
|
||||
'sql_analysis.slow_query.metric.rows_returned',
|
||||
'sql_analysis.slow_query.metric.average_duration',
|
||||
'sql_analysis.slow_query.metric.executions',
|
||||
'sql_analysis.slow_query.summary.statements',
|
||||
'sql_analysis.slow_query.summary.executions',
|
||||
'sql_analysis.slow_query.summary.max_duration',
|
||||
'sql_analysis.slow_query.summary.rows_returned',
|
||||
'sql_analysis.slow_query.truncated',
|
||||
'sql_analysis.slow_query.unsupported_diagnosis',
|
||||
'sql_analysis.slow_query.preview.empty',
|
||||
'sql_analysis.slow_query.relative.just_now',
|
||||
'sql_analysis.slow_query.relative.minutes_ago',
|
||||
'sql_analysis.slow_query.relative.hours_ago',
|
||||
'sql_analysis.slow_query.relative.days_ago',
|
||||
'sql_analysis.slow_query.rail.tooltip.no_connection',
|
||||
'sql_analysis.slow_query.rail.tooltip.open',
|
||||
'sql_analysis.slow_query.rail.aria_label',
|
||||
] as const
|
||||
|
||||
describe('SQL analysis workbench i18n', () => {
|
||||
it('localizes the sql analysis workbench shell copy', () => {
|
||||
;[
|
||||
'请输入要诊断的 SQL',
|
||||
'当前工作台对应的连接已不可用',
|
||||
'请重新选择一个有效连接后再打开 SQL 分析工作台。',
|
||||
'SQL 分析工作台',
|
||||
'慢 SQL',
|
||||
'SQL 诊断',
|
||||
'输入要诊断的 SQL,或从慢 SQL 列表点击条目带入',
|
||||
'支持从慢 SQL 列表点击条目直接带入',
|
||||
'运行诊断',
|
||||
].forEach((text) => {
|
||||
expect(sqlAnalysisWorkbenchRuntimeSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'useI18n(',
|
||||
"t('sql_analysis.workbench.validation.sql_required')",
|
||||
"t('sql_analysis.workbench.alert.connection_missing_title')",
|
||||
"t('sql_analysis.workbench.alert.connection_missing_description')",
|
||||
"t('sql_analysis.workbench.title')",
|
||||
"t('sql_analysis.workbench.view.slow_query')",
|
||||
"t('sql_analysis.workbench.view.diagnose')",
|
||||
"t('sql_analysis.workbench.editor.placeholder')",
|
||||
"t('sql_analysis.workbench.editor.aria_label')",
|
||||
"t('sql_analysis.workbench.editor.hint')",
|
||||
"t('sql_analysis.workbench.action.run')",
|
||||
].forEach((text) => {
|
||||
expect(sqlAnalysisWorkbenchSource).toContain(text)
|
||||
})
|
||||
})
|
||||
|
||||
it('localizes explain report copy while keeping raw payload output untouched', () => {
|
||||
;[
|
||||
'查询语句为空',
|
||||
'诊断失败',
|
||||
'正在执行 EXPLAIN 并解析计划...',
|
||||
'输入 SQL 后运行诊断',
|
||||
'执行计划',
|
||||
'原文',
|
||||
'节点',
|
||||
'(无原文)',
|
||||
'SQL 诊断工作台',
|
||||
].forEach((text) => {
|
||||
expect(explainWorkbenchRuntimeSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'useI18n(',
|
||||
"t('sql_analysis.explain.error.query_required')",
|
||||
"t('sql_analysis.explain.error.run_failed')",
|
||||
"t('sql_analysis.explain.loading')",
|
||||
"t('sql_analysis.explain.error.title')",
|
||||
"t('sql_analysis.explain.action.retry')",
|
||||
"t('sql_analysis.explain.empty')",
|
||||
"t('sql_analysis.explain.view.plan')",
|
||||
"t('sql_analysis.explain.view.raw')",
|
||||
"t('sql_analysis.explain.meta.node_count'",
|
||||
"t('sql_analysis.explain.raw.empty')",
|
||||
"t('sql_analysis.workbench.title')",
|
||||
].forEach((text) => {
|
||||
expect(explainWorkbenchSource).toContain(text)
|
||||
})
|
||||
|
||||
expect(explainWorkbenchSource).toContain('report.plan.rawPayload')
|
||||
})
|
||||
|
||||
it('localizes slow query panel copy while keeping sql preview and db type raw', () => {
|
||||
;[
|
||||
'加载失败',
|
||||
'已清空慢查询历史',
|
||||
'清空失败',
|
||||
'按耗时',
|
||||
'按扫描行数',
|
||||
'按时间',
|
||||
'刷新',
|
||||
'清空当前连接的历史',
|
||||
'加载慢查询历史...',
|
||||
'暂无慢查询记录(阈值 500ms)',
|
||||
'慢 SQL 历史',
|
||||
'(当前连接)',
|
||||
'扫描',
|
||||
'返回',
|
||||
'(无 SQL 预览)',
|
||||
'刚刚',
|
||||
'分钟前',
|
||||
'小时前',
|
||||
'天前',
|
||||
].forEach((text) => {
|
||||
expect(slowQueryPanelRuntimeSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'useI18n(',
|
||||
"t('common.refresh')",
|
||||
"t('sql_analysis.slow_query.error.load_failed')",
|
||||
"t('sql_analysis.slow_query.message.cleared')",
|
||||
"t('sql_analysis.slow_query.error.clear_failed')",
|
||||
"t('sql_analysis.slow_query.sort.duration')",
|
||||
"t('sql_analysis.slow_query.sort.frequency')",
|
||||
"t('sql_analysis.slow_query.sort.rows_returned')",
|
||||
"t('sql_analysis.slow_query.sort.recent')",
|
||||
"t('sql_analysis.slow_query.search.placeholder')",
|
||||
"t('sql_analysis.slow_query.search.aria_label')",
|
||||
"t('sql_analysis.slow_query.clear_confirm.title')",
|
||||
"t('sql_analysis.slow_query.clear_confirm.description')",
|
||||
"t('sql_analysis.slow_query.tooltip.clear_current')",
|
||||
"t('sql_analysis.slow_query.loading')",
|
||||
"t('sql_analysis.slow_query.error.title')",
|
||||
"t('sql_analysis.slow_query.empty'",
|
||||
"t('sql_analysis.slow_query.title')",
|
||||
"t('sql_analysis.slow_query.current_connection')",
|
||||
"t('sql_analysis.slow_query.metric.rows_read')",
|
||||
"t('sql_analysis.slow_query.metric.rows_returned')",
|
||||
"t('sql_analysis.slow_query.metric.average_duration'",
|
||||
"t('sql_analysis.slow_query.metric.executions'",
|
||||
"t('sql_analysis.slow_query.action.copy')",
|
||||
"t('sql_analysis.slow_query.action.load')",
|
||||
"t('sql_analysis.slow_query.action.view_full')",
|
||||
"t('sql_analysis.slow_query.details.title')",
|
||||
"t('sql_analysis.slow_query.scope_note'",
|
||||
"t('sql_analysis.slow_query.truncated')",
|
||||
"t('sql_analysis.slow_query.not_diagnosable')",
|
||||
"t('sql_analysis.slow_query.unsupported_diagnosis')",
|
||||
"t('sql_analysis.slow_query.preview.empty')",
|
||||
"t('sql_analysis.slow_query.relative.just_now')",
|
||||
"t('sql_analysis.slow_query.relative.minutes_ago'",
|
||||
"t('sql_analysis.slow_query.relative.hours_ago'",
|
||||
"t('sql_analysis.slow_query.relative.days_ago'",
|
||||
].forEach((text) => {
|
||||
expect(slowQueryPanelSource).toContain(text)
|
||||
})
|
||||
|
||||
expect(slowQueryPanelSource).toContain('getSlowQuerySql(record)')
|
||||
expect(slowQueryPanelSource).toContain('record.dbType')
|
||||
})
|
||||
|
||||
it('localizes explain graph, explain sidebar and slow-query rail labels', () => {
|
||||
;[
|
||||
'表:',
|
||||
'索引:',
|
||||
'估算',
|
||||
'实际',
|
||||
'成本',
|
||||
'全表扫描',
|
||||
'额外排序',
|
||||
'临时表',
|
||||
].forEach((text) => {
|
||||
expect(explainGraphRuntimeSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'useI18n(',
|
||||
"t('sql_analysis.explain_graph.label.table')",
|
||||
"t('sql_analysis.explain_graph.label.index')",
|
||||
"t('sql_analysis.explain_graph.metric.est_rows')",
|
||||
"t('sql_analysis.explain_graph.metric.actual_rows')",
|
||||
"t('sql_analysis.explain_graph.metric.cost')",
|
||||
"t('sql_analysis.explain_graph.flag.full_scan')",
|
||||
"t('sql_analysis.explain_graph.flag.filesort')",
|
||||
"t('sql_analysis.explain_graph.flag.temp_table')",
|
||||
].forEach((text) => {
|
||||
expect(explainGraphSource).toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'执行统计',
|
||||
'总成本',
|
||||
'总耗时',
|
||||
'扫描行数',
|
||||
'缓冲命中',
|
||||
'最大单节点行数',
|
||||
'存在全表扫描',
|
||||
'存在额外排序',
|
||||
'使用临时表',
|
||||
'操作类型',
|
||||
'操作详情',
|
||||
'表',
|
||||
'索引',
|
||||
'估算行数',
|
||||
'实际行数',
|
||||
'循环次数',
|
||||
'标志',
|
||||
'节点详情',
|
||||
'Extra 字段',
|
||||
'索引建议',
|
||||
'未发现明显性能问题',
|
||||
'行',
|
||||
'表:',
|
||||
].forEach((text) => {
|
||||
expect(explainSidebarRuntimeSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'useI18n(',
|
||||
"t('sql_analysis.sidebar.stats.title')",
|
||||
"t('sql_analysis.sidebar.stats.total_cost')",
|
||||
"t('sql_analysis.sidebar.stats.total_duration')",
|
||||
"t('sql_analysis.sidebar.stats.rows_read')",
|
||||
"t('sql_analysis.sidebar.stats.buffer_hit')",
|
||||
"t('sql_analysis.sidebar.stats.max_est_rows')",
|
||||
"t('sql_analysis.sidebar.warning.full_scan')",
|
||||
"t('sql_analysis.sidebar.warning.filesort')",
|
||||
"t('sql_analysis.sidebar.warning.temp_table')",
|
||||
"t('sql_analysis.sidebar.node.title')",
|
||||
"t('sql_analysis.sidebar.node.op_type')",
|
||||
"t('sql_analysis.sidebar.node.op_detail')",
|
||||
"t('sql_analysis.sidebar.node.table')",
|
||||
"t('sql_analysis.sidebar.node.index')",
|
||||
"t('sql_analysis.sidebar.node.est_rows')",
|
||||
"t('sql_analysis.sidebar.node.actual_rows')",
|
||||
"t('sql_analysis.sidebar.node.loops')",
|
||||
"t('sql_analysis.sidebar.node.cost')",
|
||||
"t('sql_analysis.sidebar.node.duration')",
|
||||
"t('sql_analysis.sidebar.node.buffer_hit')",
|
||||
"t('sql_analysis.sidebar.node.flags')",
|
||||
"t('sql_analysis.sidebar.node.extra'",
|
||||
"t('sql_analysis.sidebar.suggestions.title'",
|
||||
"t('sql_analysis.sidebar.suggestions.empty')",
|
||||
"t('sql_analysis.sidebar.suggestions.rows'",
|
||||
"t('sql_analysis.sidebar.suggestions.table'",
|
||||
].forEach((text) => {
|
||||
expect(explainSidebarSource).toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'请先打开一个数据库连接的标签页',
|
||||
'打开当前连接的 SQL 分析工作台',
|
||||
'慢 SQL 工作台',
|
||||
].forEach((text) => {
|
||||
expect(slowQueryRailButtonRuntimeSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
'useI18n(',
|
||||
"t('sql_analysis.slow_query.rail.tooltip.no_connection')",
|
||||
"t('sql_analysis.slow_query.rail.tooltip.open')",
|
||||
"t('sql_analysis.slow_query.rail.aria_label')",
|
||||
'buildSqlAnalysisWorkbenchTab',
|
||||
"view: 'slow-query'",
|
||||
].forEach((text) => {
|
||||
expect(slowQueryRailButtonSource).toContain(text)
|
||||
})
|
||||
})
|
||||
|
||||
it('uses shortcut translation keys without Chinese fallback labels in query editor menus', () => {
|
||||
;[
|
||||
"{translate('app.shortcuts.action.diagnoseQuery.label' as any) || 'SQL 诊断'}",
|
||||
"{translate('app.shortcuts.action.showSlowQueries.label' as any) || '慢 SQL 历史'}",
|
||||
].forEach((text) => {
|
||||
expect(queryEditorSource).not.toContain(text)
|
||||
})
|
||||
|
||||
;[
|
||||
"translate('app.shortcuts.action.diagnoseQuery.label' as any)",
|
||||
"translate('app.shortcuts.action.showSlowQueries.label' as any)",
|
||||
].forEach((text) => {
|
||||
expect(queryEditorSource).toContain(text)
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps sql analysis catalog keys in all supported languages with matching placeholders', () => {
|
||||
const zhCnCatalog = catalogs['zh-CN']
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(zhCnCatalog, `zh-CN:${key}`).toHaveProperty(key)
|
||||
const expectedPlaceholders = placeholdersOf(zhCnCatalog[key])
|
||||
locales.forEach((locale) => {
|
||||
expect(catalogs[locale], `${locale}:${key}`).toHaveProperty(key)
|
||||
expect(placeholdersOf(catalogs[locale][key]), `${locale}:${key}`).toEqual(expectedPlaceholders)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('SQL analysis workbench wiring', () => {
|
||||
it('routes QueryEditor diagnose and slow-query actions to the sql-analysis workbench tab', () => {
|
||||
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(source).toContain("buildSqlAnalysisWorkbenchTab")
|
||||
expect(source).toContain("openSqlAnalysisWorkbench('diagnose', getCurrentQuery())")
|
||||
expect(source).toContain("openSqlAnalysisWorkbench('slow-query')")
|
||||
expect(source).not.toContain('const [explainOpen, setExplainOpen]')
|
||||
expect(source).not.toContain('const [slowQueryOpen, setSlowQueryOpen]')
|
||||
expect(source).not.toContain('<ExplainWorkbench')
|
||||
expect(source).not.toContain('<SlowQueryPanel')
|
||||
})
|
||||
|
||||
it('opens the same sql-analysis workbench from the sidebar slow-query button', () => {
|
||||
const source = readFileSync(new URL('./sidebar/SlowQueryRailButton.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(source).toContain('buildSqlAnalysisWorkbenchTab')
|
||||
expect(source).toContain("view: 'slow-query'")
|
||||
expect(source).not.toContain('SlowQueryPanel')
|
||||
})
|
||||
|
||||
it('uses a compact segmented switcher in the sql-analysis workbench header', () => {
|
||||
const source = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(source).toContain('Segmented')
|
||||
expect(source).toContain('gn-sql-analysis-view-switcher')
|
||||
expect(source).not.toContain('<Tabs')
|
||||
})
|
||||
|
||||
it('uses a compact segmented switcher inside the explain report view', () => {
|
||||
const source = readFileSync(new URL('./explain/ExplainWorkbench.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(source).toContain('Segmented')
|
||||
expect(source).toContain('gn-explain-report-switcher')
|
||||
expect(source).not.toContain('<Tabs')
|
||||
})
|
||||
|
||||
it('fills the SQL analysis report viewport through the Ant Spin wrapper', () => {
|
||||
const source = readFileSync(new URL('./explain/ExplainWorkbench.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(source).toContain('wrapperClassName="gn-explain-report-spinner"')
|
||||
expect(source).toMatch(
|
||||
/\.gn-explain-report-spinner > \.ant-spin-container \{[^}]*height: 100%;[^}]*min-height: 0;[^}]*display: flex;[^}]*flex-direction: column;/s,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/\.gn-explain-report-shell \{[^}]*flex: 1 1 auto;[^}]*min-height: 0;[^}]*display: flex;[^}]*flex-direction: column;/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the editor draft separate from the SQL submitted for diagnosis', () => {
|
||||
const source = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(source).toContain('submittedSql')
|
||||
expect(source).toContain('setSubmittedSql')
|
||||
expect(source).toContain('sql={submittedSql}')
|
||||
expect(source).toContain("event.key === 'Enter'")
|
||||
expect(source).toContain('event.ctrlKey || event.metaKey')
|
||||
})
|
||||
|
||||
it('loads a slow query into the editor without immediately diagnosing it', () => {
|
||||
const source = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
|
||||
const handler = source.slice(source.indexOf('const handlePickSlowQuery'), source.indexOf('const slowQueryLoadKey'))
|
||||
|
||||
expect(handler).toContain('setSqlDraft(nextSql)')
|
||||
expect(handler).toContain("setActiveView('diagnose')")
|
||||
expect(handler).toContain("setSubmittedSql('')")
|
||||
expect(handler).toContain('setDiagnoseRunKey(0)')
|
||||
expect(handler).not.toContain('setDiagnoseRunKey((previous)')
|
||||
})
|
||||
|
||||
it('guards diagnose and slow-query responses against stale requests', () => {
|
||||
const explainSource = readFileSync(new URL('./explain/ExplainWorkbench.tsx', import.meta.url), 'utf8')
|
||||
const slowQuerySource = readFileSync(new URL('./explain/SlowQueryPanel.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(explainSource).toContain('requestSequenceRef')
|
||||
expect(slowQuerySource).toContain('requestSequenceRef')
|
||||
})
|
||||
|
||||
it('disables execution-plan diagnosis for unsupported datasource types', () => {
|
||||
const capabilitiesSource = readFileSync(new URL('../utils/dataSourceCapabilities.ts', import.meta.url), 'utf8')
|
||||
const workbenchSource = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
|
||||
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8')
|
||||
|
||||
expect(capabilitiesSource).toContain('supportsExplainDiagnosis')
|
||||
expect(workbenchSource).toContain('disabled: !supportsDiagnosis')
|
||||
expect(workbenchSource).toContain("t('sql_analysis.slow_query.unsupported_diagnosis')")
|
||||
expect(queryEditorSource).toContain('!currentConnectionCapabilities.supportsExplainDiagnosis')
|
||||
})
|
||||
})
|
||||
@@ -1,20 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('QueryEditorToolbar AI trigger affordance', () => {
|
||||
it('keeps a direct toolbar trigger for inline AI completion', () => {
|
||||
expect(source).toContain('onMouseDown={onCaptureEditorCursorPosition}');
|
||||
expect(source).toContain('onClick={onTriggerSqlAiCompletion}');
|
||||
expect(source).toContain('triggerSqlAiCompletionLabel');
|
||||
expect(source).toContain('const aiMoreTitle');
|
||||
expect(source).toMatch(/aria-label=\{aiMoreTitle\}[\s\S]*?onMouseDown=\{onCaptureEditorCursorPosition\}/);
|
||||
});
|
||||
|
||||
it('keeps the secondary AI dropdown for other actions', () => {
|
||||
expect(source).toContain('icon={<DownOutlined />}');
|
||||
expect(source).toContain('menu={{ items: aiMenuItems }}');
|
||||
expect(source).toContain('title={isV2Ui ? aiMoreTitle : undefined}');
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./RedisCommandEditor.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('RedisCommandEditor i18n', () => {
|
||||
it('localizes console chrome while preserving Redis command and result raw content', () => {
|
||||
[
|
||||
'请输入要执行的命令',
|
||||
'连接不存在',
|
||||
'Redis Console',
|
||||
'执行 (Cmd+Enter)',
|
||||
'Execution Output',
|
||||
'清空控制台',
|
||||
'在此终端执行命令,结果会以原样输出',
|
||||
'选中任意行',
|
||||
'仅执行选中段落',
|
||||
'Redis Command',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain('useOptionalI18n()');
|
||||
expect(source).toContain("tr('redis_command.message.command_required'");
|
||||
expect(source).toContain("tr('redis_command.state.connection_not_found'");
|
||||
expect(source).toContain("tr('redis_command.title.console'");
|
||||
expect(source).toContain("tr('redis_command.action.execute'");
|
||||
expect(source).toContain("tr('redis_command.output.title'");
|
||||
expect(source).toContain("tr('redis_command.action.clear_console'");
|
||||
expect(source).toContain("tr('redis_command.output.empty_hint'");
|
||||
expect(source).toContain("tr('redis_command.output.selection_tip'");
|
||||
expect(source).toContain("tr('redis_command.completion.detail'");
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./RedisMonitor.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('RedisMonitor i18n', () => {
|
||||
it('localizes monitor chrome while preserving Redis metrics and raw server info', () => {
|
||||
[
|
||||
'Redis 实例监控',
|
||||
'暂停刷新',
|
||||
'恢复刷新',
|
||||
'立即刷新',
|
||||
'已用内存 (Used)',
|
||||
'客户端数量 (Clients)',
|
||||
'吞吐量 (OPS)',
|
||||
'启动时长 (Uptime)',
|
||||
'请求吞吐量 (QPS)',
|
||||
'内存开销 (Memory)',
|
||||
'CPU 使用率 (CPU Usage)',
|
||||
'连接信息 (Clients & Keys)',
|
||||
'详细服务器参数',
|
||||
'Connection not found.',
|
||||
'Failed to fetch Redis info',
|
||||
'Unknown error',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain('useOptionalI18n()');
|
||||
expect(source).toContain("tr('redis_monitor.title.instance'");
|
||||
expect(source).toContain("tr('redis_monitor.action.pause_refresh'");
|
||||
expect(source).toContain("tr('redis_monitor.action.resume_refresh'");
|
||||
expect(source).toContain("tr('redis_monitor.action.refresh_now'");
|
||||
expect(source).toContain("tr('redis_monitor.metric.memory_used'");
|
||||
expect(source).toContain("tr('redis_monitor.metric.clients'");
|
||||
expect(source).toContain("tr('redis_monitor.metric.ops'");
|
||||
expect(source).toContain("tr('redis_monitor.metric.uptime'");
|
||||
expect(source).toContain("tr('redis_monitor.chart.qps'");
|
||||
expect(source).toContain("tr('redis_monitor.chart.memory'");
|
||||
expect(source).toContain("tr('redis_monitor.chart.cpu_usage'");
|
||||
expect(source).toContain("tr('redis_monitor.chart.clients_keys'");
|
||||
expect(source).toContain("tr('redis_monitor.server_details.title'");
|
||||
expect(source).toContain("tr('redis_monitor.state.connection_not_found'");
|
||||
expect(source).toContain("tr('redis_monitor.message.fetch_failed'");
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./RedisViewer.tsx', import.meta.url), 'utf8');
|
||||
const keyToolbarSource = readFileSync(new URL('./RedisViewerKeyToolbar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('RedisViewer i18n', () => {
|
||||
it('localizes fixed key browser chrome and feedback while preserving raw Key names', () => {
|
||||
[
|
||||
'加载 Key 失败',
|
||||
'获取值失败',
|
||||
'设置失败',
|
||||
'Key 重命名成功',
|
||||
'选择一个 Key 查看详情',
|
||||
'复制 Key 名称',
|
||||
'查看模式',
|
||||
'模糊',
|
||||
'精确',
|
||||
'新建 Key',
|
||||
'导出全部',
|
||||
'导出已选',
|
||||
'导入',
|
||||
'重命名 Key',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain('useOptionalI18n()');
|
||||
expect(source).toContain("tr('redis_viewer.message.load_keys_failed'");
|
||||
expect(source).toContain("tr('redis_viewer.message.value_load_failed'");
|
||||
expect(source).toContain("tr('redis_viewer.message.rename_success'");
|
||||
expect(source).toContain("tr('redis_viewer.message.export_success'");
|
||||
expect(source).toContain("tr('redis_viewer.message.import_summary'");
|
||||
expect(source).toContain("tr('redis_viewer.message.import_file_required'");
|
||||
expect(source).toContain("tr('redis_viewer.message.import_selection_required'");
|
||||
expect(source).toContain("tr('redis_viewer.state.empty_selection'");
|
||||
expect(source).toContain("tr('redis_viewer.state.import_preview_empty'");
|
||||
expect(source).toContain("tr('redis_viewer.action.copy_key_name'");
|
||||
expect(source).toContain("tr('redis_viewer.action.export_all'");
|
||||
expect(source).toContain("tr('redis_viewer.action.export_selected'");
|
||||
expect(source).toContain("tr('redis_viewer.action.import'");
|
||||
expect(source).toContain("tr('redis_viewer.action.select_import_file'");
|
||||
expect(source).toContain("tr('redis_viewer.action.change_import_file'");
|
||||
expect(source).toContain("tr('redis_viewer.action.select_all_import_keys'");
|
||||
expect(source).toContain("tr('redis_viewer.confirm.delete_list_item'");
|
||||
});
|
||||
|
||||
it('localizes TTL and table labels with catalog keys', () => {
|
||||
expect(source).not.toContain("return '永久'");
|
||||
expect(source).not.toContain("return '已过期'");
|
||||
expect(source).not.toContain('title: \'操作\'');
|
||||
|
||||
expect(source).toContain("tr('redis_viewer.ttl.forever'");
|
||||
expect(source).toContain("tr('redis_viewer.ttl.expired'");
|
||||
expect(source).toContain("tr('redis_viewer.table.action'");
|
||||
});
|
||||
|
||||
it('localizes the standalone key toolbar chrome while preserving raw Redis identifiers', () => {
|
||||
[
|
||||
"return '单机'",
|
||||
'Key Explorer',
|
||||
'节点',
|
||||
'模糊',
|
||||
'精确',
|
||||
'输入完整 Key / 命名空间精确搜索',
|
||||
'搜索 Key(模糊匹配)',
|
||||
'刷新',
|
||||
'新建',
|
||||
'全选全部',
|
||||
'加载全部',
|
||||
'取消全选',
|
||||
'确定删除选中的',
|
||||
'删除选中',
|
||||
].forEach((snippet) => {
|
||||
expect(keyToolbarSource).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(keyToolbarSource).toContain('useOptionalI18n()');
|
||||
expect(keyToolbarSource).toContain("tr('redis_viewer.title.key_explorer'");
|
||||
expect(keyToolbarSource).toContain("tr('redis_viewer.label.keys_count'");
|
||||
expect(keyToolbarSource).toContain("tr('redis_viewer.label.node_count'");
|
||||
expect(keyToolbarSource).toContain("tr('redis_viewer.action.load_all'");
|
||||
expect(keyToolbarSource).toContain("tr('redis_viewer.confirm.delete_selected'");
|
||||
expect(keyToolbarSource).toContain("tr('redis_viewer.action.delete_selected'");
|
||||
expect(keyToolbarSource).toContain('master: {sentinelMaster}');
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const contextMenuSource = readFileSync(new URL('./V2TableContextMenu.tsx', import.meta.url), 'utf8');
|
||||
const legacyMenuSource = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
const v2ActionSource = readFileSync(new URL('./sidebar/useSidebarV2ActionHandlers.tsx', import.meta.url), 'utf8');
|
||||
const modalSource = readFileSync(new URL('./MessagePublishModal.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('Sidebar Kafka publish entry', () => {
|
||||
it('adds a Kafka topic publish action in both legacy and v2 table menus', () => {
|
||||
expect(legacyMenuSource).toContain("key: 'publish-message'");
|
||||
expect(legacyMenuSource).toContain("label: t('message_publish_modal.title')");
|
||||
expect(legacyMenuSource).toContain('openMessagePublishModal(node)');
|
||||
expect(contextMenuSource).toContain("| 'publish-message'");
|
||||
expect(contextMenuSource).toContain("title: t('message_publish_modal.title')");
|
||||
expect(v2ActionSource).toContain("case 'publish-message'");
|
||||
expect(v2ActionSource).toContain('openMessagePublishModal(node)');
|
||||
expect(contextMenuSource).not.toContain("title: '测试发送消息'");
|
||||
});
|
||||
|
||||
it('renders the dedicated message publish modal and executes an audited user action through the encoder', () => {
|
||||
expect(sidebarSource).toContain('<MessagePublishModal');
|
||||
expect(modalSource).toContain('buildMessagePublishCommand');
|
||||
expect(modalSource).toContain('DBQueryAudited(');
|
||||
expect(modalSource).toContain("'message_publish'");
|
||||
expect(modalSource).toContain("t('message_publish_modal.field.body.label')");
|
||||
});
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const objectActionsSource = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const searchModelSource = readFileSync(new URL('./sidebar/useSidebarSearchModel.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.message.ai_table_context_missing',
|
||||
'sidebar.ai_prompt.explain.intro',
|
||||
'sidebar.ai_prompt.explain.detail',
|
||||
'sidebar.ai_prompt.query.intro',
|
||||
'sidebar.ai_prompt.query.detail',
|
||||
];
|
||||
|
||||
describe('Sidebar AI prompt i18n', () => {
|
||||
it('localizes AI table prompt shells without translating raw table or DDL values', () => {
|
||||
[
|
||||
'当前表缺少连接上下文,无法发送给 AI',
|
||||
'请解释数据表 ${conn.dbName}.${tableName} 的结构和业务含义。',
|
||||
'重点说明字段含义、主键/索引、潜在关联关系、典型查询场景和风险点。',
|
||||
'请基于数据表 ${conn.dbName}.${tableName} 生成 3 条常用查询 SQL。',
|
||||
'要求包含:数据预览查询、按关键字段过滤查询、一个聚合或统计查询。',
|
||||
"title: '让 AI 回答'",
|
||||
].forEach((legacyCopy) => {
|
||||
expect(source).not.toContain(legacyCopy);
|
||||
expect(objectActionsSource).not.toContain(legacyCopy);
|
||||
expect(searchModelSource).not.toContain(legacyCopy);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(objectActionsSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
expect(objectActionsSource).toContain('DBShowCreateTable');
|
||||
expect(objectActionsSource).toContain('conn.dbName');
|
||||
expect(objectActionsSource).toContain('tableName');
|
||||
expect(objectActionsSource).toContain('ddl ? `\\n\\`\\`\\`sql');
|
||||
expect(objectActionsSource).toContain('${ddl}');
|
||||
expect(searchModelSource).toContain("t('sidebar.command_search.action.ask_ai.title')");
|
||||
expect(searchModelSource).toContain('v2CommandSearchQuery.aiPrompt');
|
||||
});
|
||||
|
||||
it('keeps AI prompt keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const batchHookSource = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8');
|
||||
const batchTabSource = readFileSync(new URL('../utils/tableExportTab.ts', import.meta.url), 'utf8');
|
||||
const batchWorkbenchSource = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
const requiredKeys = [
|
||||
'sidebar.action.batch_tables',
|
||||
'sidebar.action.batch_databases',
|
||||
'sidebar.action.clear_tables',
|
||||
'sidebar.action.delete_tables',
|
||||
'sidebar.action.delete_database_count',
|
||||
'sidebar.action.export_schema',
|
||||
'sidebar.action.export_data_only',
|
||||
'sidebar.action.backup_schema_data',
|
||||
'sidebar.action.select_all',
|
||||
'sidebar.action.clear_selection',
|
||||
'sidebar.action.invert_selection',
|
||||
'sidebar.action.export_database_schema_count',
|
||||
'sidebar.action.backup_database_count',
|
||||
'sidebar.field.select_connection',
|
||||
'sidebar.field.select_database',
|
||||
'sidebar.placeholder.select_connection',
|
||||
'sidebar.placeholder.select_connection_first',
|
||||
'sidebar.placeholder.filter_table_view',
|
||||
'sidebar.filter.all_objects',
|
||||
'sidebar.filter.tables_only',
|
||||
'sidebar.filter.views_only',
|
||||
'sidebar.filter.scope_filtered',
|
||||
'sidebar.filter.scope_all',
|
||||
'sidebar.modal.batch_tables.title',
|
||||
'sidebar.modal.batch_tables.description',
|
||||
'sidebar.modal.batch_tables.selection_hint',
|
||||
'sidebar.modal.batch_databases.title',
|
||||
'sidebar.modal.batch_databases.description',
|
||||
'sidebar.modal.batch_databases.selection_hint',
|
||||
'sidebar.batch.filtered_count',
|
||||
'sidebar.batch.selected_objects',
|
||||
'sidebar.batch.selected_databases',
|
||||
'sidebar.batch.group.tables',
|
||||
'sidebar.batch.group.views',
|
||||
'sidebar.batch.no_matching_objects',
|
||||
'sidebar.tab.batch_export_objects',
|
||||
'sidebar.tab.batch_export_objects_database',
|
||||
'sidebar.tab.batch_export_databases',
|
||||
] as const;
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar batch actions i18n', () => {
|
||||
it('localizes batch table and database action copy', () => {
|
||||
[
|
||||
'批量操作表',
|
||||
'批量操作库',
|
||||
'按对象批量导出结构、数据或完整备份。',
|
||||
'按数据库批量导出结构,或生成结构加数据的备份。',
|
||||
].forEach((rawSnippet) => {
|
||||
expect(sidebarSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
[
|
||||
'`批量导出 ${dbName} 对象`',
|
||||
"'批量导出对象'",
|
||||
"title: '批量导出库'",
|
||||
].forEach((rawSnippet) => {
|
||||
expect(batchHookSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
[
|
||||
"title: String(input.title || '批量导出对象').trim() || '批量导出对象'",
|
||||
"title: String(input.title || '批量导出库').trim() || '批量导出库'",
|
||||
].forEach((rawSnippet) => {
|
||||
expect(batchTabSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.action.batch_tables',
|
||||
'sidebar.action.batch_databases',
|
||||
].forEach((key) => {
|
||||
expect(sidebarSource, key).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.action.batch_tables',
|
||||
'sidebar.action.batch_databases',
|
||||
].forEach((key) => {
|
||||
expect(batchHookSource, key).toContain(`t('${key}'`);
|
||||
});
|
||||
expect(batchHookSource).toContain('openBatchTableWorkbench');
|
||||
expect(batchHookSource).toContain('openBatchDatabaseWorkbench');
|
||||
expect(batchHookSource).not.toContain('requestKey: createTableExportRequestKey(\'batch');
|
||||
|
||||
[
|
||||
'sidebar.action.clear_tables',
|
||||
'sidebar.action.delete_tables',
|
||||
'sidebar.action.delete_database_count',
|
||||
'sidebar.modal.confirm_clear_selected_tables.title',
|
||||
'sidebar.modal.confirm_delete_selected_tables.title',
|
||||
'sidebar.modal.confirm_delete_selected_databases.title',
|
||||
].forEach((key) => {
|
||||
expect(batchWorkbenchSource, key).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.tab.batch_export_objects',
|
||||
'sidebar.tab.batch_export_databases',
|
||||
].forEach((key) => {
|
||||
expect(batchTabSource, key).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps batch action catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['sidebar.action.delete_database_count'])).toEqual(['count']);
|
||||
expect(placeholders(catalog['sidebar.action.delete_tables'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.tab.batch_export_objects'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.tab.batch_export_objects_database'])).toEqual(['database']);
|
||||
expect(placeholders(catalog['sidebar.tab.batch_export_databases'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.modal.confirm_clear_selected_tables.title',
|
||||
'sidebar.modal.confirm_clear_selected_tables.content',
|
||||
'sidebar.action.continue',
|
||||
'sidebar.action.cancel',
|
||||
'sidebar.message.clearing_selected_tables',
|
||||
'sidebar.message.clear_success',
|
||||
'sidebar.message.clear_failed',
|
||||
'sidebar.modal.confirm_delete_selected_tables.title',
|
||||
'sidebar.modal.confirm_delete_selected_tables.content',
|
||||
'sidebar.message.deleting_selected_tables',
|
||||
'sidebar.message.delete_tables_success',
|
||||
'sidebar.message.delete_tables_failed',
|
||||
] as const;
|
||||
|
||||
const extractHandleBatchClearBlock = (): string => {
|
||||
const start = source.indexOf('const handleClearSelectedTables = async');
|
||||
const end = source.indexOf('const handleDeleteSelectedTables = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const extractHandleBatchDeleteTablesBlock = (): string => {
|
||||
const start = source.indexOf('const handleDeleteSelectedTables = async');
|
||||
const end = source.indexOf('const handleDeleteSelectedDatabases = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar batch clear feedback i18n', () => {
|
||||
it('localizes clear confirmation, loading, success, and failure wrappers', () => {
|
||||
const block = extractHandleBatchClearBlock();
|
||||
|
||||
expect(block).toContain('ClearTables(');
|
||||
expect(block).toContain("t('sidebar.modal.confirm_clear_selected_tables.title')");
|
||||
expect(block).toContain("t('sidebar.modal.confirm_clear_selected_tables.content'");
|
||||
expect(block).toContain("t('sidebar.action.continue')");
|
||||
expect(block).toContain("t('sidebar.message.clearing_selected_tables'");
|
||||
expect(block).toContain("t('sidebar.message.clear_success')");
|
||||
expect(block).toContain("t('sidebar.message.clear_failed'");
|
||||
expect(source).toContain("cancelText: t('sidebar.action.cancel')");
|
||||
expect(block).toContain('connection: connection?.name || effectiveConnectionId');
|
||||
expect(block).toContain('database: selectedDbName');
|
||||
expect(block).toContain('count: selectedTableNames.length');
|
||||
expect(block).toContain('error: res.message');
|
||||
expect(block).toContain('error: errorMessage');
|
||||
expect(block).toContain("res.message !== '已取消'");
|
||||
});
|
||||
|
||||
it('localizes table deletion confirmation, loading, success, and failure wrappers', () => {
|
||||
const block = extractHandleBatchDeleteTablesBlock();
|
||||
|
||||
expect(block).toContain('DropTable');
|
||||
expect(source).toContain("item.objectType === 'table'");
|
||||
expect(block).toContain("t('sidebar.modal.confirm_delete_selected_tables.title')");
|
||||
expect(block).toContain("t('sidebar.modal.confirm_delete_selected_tables.content'");
|
||||
expect(source).toContain("okText: options.okText || t('sidebar.action.delete')");
|
||||
expect(source).toContain('okButtonProps: { danger: true }');
|
||||
expect(source).toContain("cancelText: t('sidebar.action.cancel')");
|
||||
expect(block).toContain("t('sidebar.message.deleting_selected_tables'");
|
||||
expect(block).toContain("t('sidebar.message.delete_tables_success'");
|
||||
expect(block).toContain("t('sidebar.message.delete_tables_failed'");
|
||||
expect(block).toContain('connection: connection?.name || effectiveConnectionId');
|
||||
expect(block).toContain('database: selectedDbName');
|
||||
expect(block).toContain('count: selectedTableNames.length');
|
||||
expect(block).toContain('count: succeededNames.length');
|
||||
expect(block).toContain('table: failed.table');
|
||||
expect(block).toContain('error: failed.error');
|
||||
});
|
||||
|
||||
it('keeps batch clear feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['sidebar.modal.confirm_clear_selected_tables.title'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.modal.confirm_clear_selected_tables.content'])).toEqual(['connection', 'database']);
|
||||
expect(placeholders(catalog['sidebar.action.continue'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.action.cancel'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.message.clearing_selected_tables'])).toEqual(['count']);
|
||||
expect(placeholders(catalog['sidebar.message.clear_success'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.message.clear_failed'])).toEqual(['error']);
|
||||
expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_tables.title'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_tables.content'])).toEqual(['connection', 'count', 'database']);
|
||||
expect(placeholders(catalog['sidebar.message.deleting_selected_tables'])).toEqual(['count']);
|
||||
expect(placeholders(catalog['sidebar.message.delete_tables_success'])).toEqual(['count']);
|
||||
expect(placeholders(catalog['sidebar.message.delete_tables_failed'])).toEqual(['error', 'table']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const runnerSource = readFileSync(new URL('./useExportProgressRunner.ts', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'data_export.message.load_databases_failed',
|
||||
'data_export.message.already_running',
|
||||
'data_export.message.export_success',
|
||||
'data_export.message.export_failed',
|
||||
'sidebar.modal.confirm_delete_selected_databases.title',
|
||||
'sidebar.modal.confirm_delete_selected_databases.content',
|
||||
'sidebar.message.deleting_selected_databases',
|
||||
'sidebar.message.delete_databases_success',
|
||||
'sidebar.message.delete_databases_failed',
|
||||
] as const;
|
||||
|
||||
const extractBatchDatabaseExportBlock = (): string => {
|
||||
const start = source.indexOf('const handleStartBatchDatabasesExport = async');
|
||||
const end = source.indexOf('const handleStartDirectDatabaseExport = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const extractHandleBatchDbDeleteBlock = (): string => {
|
||||
const start = source.indexOf('const handleDeleteSelectedDatabases = async');
|
||||
const end = source.indexOf('const handleStartSingleExport = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar batch database export feedback i18n', () => {
|
||||
it('loads databases and routes selected targets through the retained progress runner', () => {
|
||||
const block = extractBatchDatabaseExportBlock();
|
||||
|
||||
expect(source).toContain('DBGetDatabases(');
|
||||
expect(source).toContain("res.message || t('data_export.message.load_databases_failed')");
|
||||
expect(source).toContain("error?.message || t('data_export.message.load_databases_failed')");
|
||||
expect(block).toContain('selectedDatabaseNames.length === 0');
|
||||
expect(block).toContain("batchDatabaseMode === 'backup'");
|
||||
expect(block).toContain('await runExportWithProgress({');
|
||||
expect(block).toContain('ExportDatabasesSQLWithOptions(');
|
||||
expect(block).toContain('selectedDatabaseNames,');
|
||||
expect(block).toContain('includeDropIfExists,');
|
||||
expect(runnerSource).toContain("message.warning(t('data_export.message.already_running'))");
|
||||
expect(runnerSource).toContain("message.success(t('data_export.message.export_success'))");
|
||||
expect(runnerSource).toContain("message.error(t('data_export.message.export_failed', { error: result.message }))");
|
||||
});
|
||||
|
||||
it('localizes database deletion confirmation, loading, success, and failure wrappers', () => {
|
||||
const block = extractHandleBatchDbDeleteBlock();
|
||||
|
||||
expect(block).toContain('DropDatabase');
|
||||
expect(block).toContain("t('sidebar.modal.confirm_delete_selected_databases.title')");
|
||||
expect(block).toContain("t('sidebar.modal.confirm_delete_selected_databases.content'");
|
||||
expect(block).toContain("t('sidebar.message.deleting_selected_databases'");
|
||||
expect(block).toContain("t('sidebar.message.delete_databases_success'");
|
||||
expect(block).toContain("t('sidebar.message.delete_databases_failed'");
|
||||
expect(source).toContain("okText: options.okText || t('sidebar.action.delete')");
|
||||
expect(source).toContain('okButtonProps: { danger: true }');
|
||||
expect(source).toContain("cancelText: t('sidebar.action.cancel')");
|
||||
expect(block).toContain('connection: connection?.name || effectiveConnectionId');
|
||||
expect(block).toContain('count: selectedDatabaseNames.length');
|
||||
expect(block).toContain('count: succeededNames.length');
|
||||
expect(block).toContain('database: failed.database');
|
||||
expect(block).toContain('error: failed.error');
|
||||
});
|
||||
|
||||
it('keeps batch database export feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['data_export.message.load_databases_failed'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.already_running'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_success'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_failed'])).toEqual(['error']);
|
||||
expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_databases.title'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_databases.content'])).toEqual(['connection', 'count']);
|
||||
expect(placeholders(catalog['sidebar.message.deleting_selected_databases'])).toEqual(['count']);
|
||||
expect(placeholders(catalog['sidebar.message.delete_databases_success'])).toEqual(['count']);
|
||||
expect(placeholders(catalog['sidebar.message.delete_databases_failed'])).toEqual(['database', 'error']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const workbenchSource = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const runnerSource = readFileSync(new URL('./useExportProgressRunner.ts', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'data_export.message.already_running',
|
||||
'data_export.message.export_success',
|
||||
'data_export.message.export_failed',
|
||||
'data_export.progress.stage.waiting_file_selection',
|
||||
'data_export.progress.title.done',
|
||||
'data_export.progress.title.error',
|
||||
] as const;
|
||||
|
||||
const extractHandleBatchExportBlock = (): string => {
|
||||
const start = workbenchSource.indexOf('const handleStartBatchTablesExport = async');
|
||||
const end = workbenchSource.indexOf('const handleStartBatchDatabasesExport = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return workbenchSource.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar batch object export feedback i18n', () => {
|
||||
it('routes workbench selections and export mode through the retained progress runner', () => {
|
||||
const block = extractHandleBatchExportBlock();
|
||||
|
||||
expect(block).toContain('selectedObjectNames.length === 0');
|
||||
expect(block).toContain("batchTableMode !== 'dataOnly'");
|
||||
expect(block).toContain("batchTableMode !== 'schema'");
|
||||
expect(block).toContain('await runExportWithProgress({');
|
||||
expect(block).toContain('ExportTablesSQLWithOptions(');
|
||||
expect(block).toContain('selectedObjectNames,');
|
||||
expect(block).toContain('includeDropIfExists: includeSchema && includeDropIfExists');
|
||||
expect(runnerSource).toContain("message.warning(t('data_export.message.already_running'))");
|
||||
expect(runnerSource).toContain("message.success(t('data_export.message.export_success'))");
|
||||
expect(runnerSource).toContain("message.error(t('data_export.message.export_failed', { error: result.message }))");
|
||||
expect(runnerSource).toContain("message.error(t('data_export.message.export_failed', { error: errorMessage }))");
|
||||
});
|
||||
|
||||
it('keeps batch object export feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['data_export.message.already_running'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_success'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_failed'])).toEqual(['error']);
|
||||
expect(placeholders(catalog['data_export.progress.stage.waiting_file_selection'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.progress.title.done'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.progress.title.error'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'data_export.message.load_databases_failed',
|
||||
'data_export.message.load_objects_failed',
|
||||
] as const;
|
||||
|
||||
const extractBatchObjectLoadBlock = (): string => {
|
||||
const start = source.indexOf("if ((!isBatchTablesWorkbench && !isBatchDatabasesWorkbench) || !connectionConfig)");
|
||||
const end = source.indexOf('const hostSummary = useMemo', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar batch object load feedback i18n', () => {
|
||||
it('keeps localized fallbacks on batch database and object loading failures', () => {
|
||||
const block = extractBatchObjectLoadBlock();
|
||||
|
||||
expect(block).toContain('DBGetDatabases(');
|
||||
expect(block).toContain('DBGetTables(');
|
||||
expect(block).toContain('loadViews(connection, selectedDbName)');
|
||||
expect(block).toContain("res.message || t('data_export.message.load_databases_failed')");
|
||||
expect(block).toContain("error?.message || t('data_export.message.load_databases_failed')");
|
||||
expect(block).toContain("res.message || t('data_export.message.load_objects_failed')");
|
||||
expect(block).toContain("error?.message || t('data_export.message.load_objects_failed')");
|
||||
});
|
||||
|
||||
it('keeps batch object load feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
expect(placeholders(catalog[key])).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const readSourceFile = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), 'utf8');
|
||||
const source = [
|
||||
readSourceFile('./Sidebar.tsx'),
|
||||
readSourceFile('./sidebar/SidebarSearchPanel.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarSearchModel.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarCommandSearchRunner.ts'),
|
||||
].join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.command_search.recent_sql_fallback',
|
||||
'sidebar.command_search.action.new_query.meta',
|
||||
'sidebar.command_search.action.new_connection.title',
|
||||
'sidebar.command_search.action.new_connection.meta',
|
||||
'sidebar.command_search.action.open_ai.title',
|
||||
'sidebar.command_search.action.open_ai.meta',
|
||||
'sidebar.command_search.action.open_sql_log.title',
|
||||
'sidebar.command_search.action.open_sql_log.meta',
|
||||
'sidebar.command_search.action.clear_recent',
|
||||
'sidebar.command_search.action.remove_recent',
|
||||
'sidebar.command_search.empty.ai',
|
||||
'sidebar.command_search.empty.object',
|
||||
'sidebar.command_search.empty.default',
|
||||
'sidebar.command_search.section.goto',
|
||||
'sidebar.command_search.section.ai',
|
||||
'sidebar.command_search.section.actions',
|
||||
'sidebar.command_search.section.recent',
|
||||
'sidebar.command_search.footer.navigate',
|
||||
'sidebar.command_search.footer.select',
|
||||
'sidebar.command_search.footer.object_only',
|
||||
'sidebar.command_search.footer.ask_ai',
|
||||
'sidebar.tab.recent_query',
|
||||
];
|
||||
|
||||
describe('Sidebar command search i18n', () => {
|
||||
it('localizes v2 command search chrome without translating raw SQL or object values', () => {
|
||||
[
|
||||
"'SQL 记录'",
|
||||
"title: '最近查询'",
|
||||
"meta: '打开一个新的 SQL 编辑页'",
|
||||
"title: '新建数据源'",
|
||||
"meta: '创建数据库、运行时或其他数据源连接'",
|
||||
"title: '打开 AI 数据洞察'",
|
||||
"meta: '让 AI 分析当前数据库上下文'",
|
||||
"title: '查看 SQL 执行日志'",
|
||||
"meta: '打开最近执行记录面板'",
|
||||
'输入「?」后加问题,按 Enter 发送到 AI 面板。',
|
||||
'未找到匹配的表、视图或物化视图。',
|
||||
'未找到匹配项。可输入 @表名 只搜表对象,或输入 ?问题 让 AI 回答。',
|
||||
"'跳转 · GO TO'",
|
||||
"'AI · ASK'",
|
||||
"'动作 · ACTIONS'",
|
||||
"'近期查询 · RECENT'",
|
||||
'导航</span>',
|
||||
'选择</span>',
|
||||
'只搜表对象</span>',
|
||||
'发送给 AI</span>',
|
||||
].forEach((legacyCopy) => {
|
||||
expect(source).not.toContain(legacyCopy);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
expect(source).toContain('log.sql.replace');
|
||||
expect(source).toContain('item.sql');
|
||||
expect(source).toContain('dataRef.tableName || dataRef.viewName');
|
||||
});
|
||||
|
||||
it('keeps command search keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8'),
|
||||
].join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
const requiredKeys = [
|
||||
'sidebar.copy_object_name.label.database',
|
||||
'sidebar.copy_object_name.label.table',
|
||||
'sidebar.copy_object_name.label.view',
|
||||
'sidebar.copy_object_name.label.materialized_view',
|
||||
'sidebar.copy_object_name.label.sequence',
|
||||
'sidebar.copy_object_name.label.package',
|
||||
'sidebar.copy_object_name.label.event',
|
||||
'sidebar.copy_object_name.empty',
|
||||
'sidebar.copy_object_name.copied',
|
||||
'sidebar.copy_object_name.failed',
|
||||
] as const;
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]).sort();
|
||||
|
||||
describe('Sidebar copy object name i18n', () => {
|
||||
it('localizes copy object name labels and feedback messages', () => {
|
||||
expect(source).not.toContain("return '视图名称'");
|
||||
expect(source).not.toContain("return '物化视图名称'");
|
||||
expect(source).not.toContain("return '事件名称'");
|
||||
expect(source).not.toContain("return '表名'");
|
||||
expect(source).not.toContain('`${label}为空,无法复制`');
|
||||
expect(source).not.toContain('`${label}已复制到剪贴板`');
|
||||
expect(source).not.toContain('`复制${label}失败: `');
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.database')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.view')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.materialized_view')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.sequence')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.package')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.event')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.label.table')");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.empty'");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.copied'");
|
||||
expect(source).toContain("t('sidebar.copy_object_name.failed'");
|
||||
});
|
||||
|
||||
it('keeps copy object name catalog entries available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['sidebar.copy_object_name.empty'])).toEqual(['label']);
|
||||
expect(placeholders(catalog['sidebar.copy_object_name.copied'])).toEqual(['label']);
|
||||
expect(placeholders(catalog['sidebar.copy_object_name.failed'])).toEqual(['error', 'label']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar copy structure i18n guard', () => {
|
||||
it('uses the shared localized success message when copying table structure', () => {
|
||||
expect(source).not.toContain("message.success('表结构已复制到剪贴板')");
|
||||
expect(source).toContain("t('table_overview.message.copy_structure_success')");
|
||||
});
|
||||
|
||||
it('keeps the reused success key available in every supported locale', () => {
|
||||
for (const locale of locales) {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
|
||||
expect(catalog['table_overview.message.copy_structure_success']).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./sidebar/useSidebarObjectActions.tsx',
|
||||
'./sidebar/sidebarLegacyNodeMenu.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar create routine i18n', () => {
|
||||
it('localizes create routine tab title and menu labels', () => {
|
||||
expect(source).not.toContain("title: isProc ? '新建存储过程' : '新建函数'");
|
||||
expect(source).not.toContain("label: '新建函数'");
|
||||
expect(source).not.toContain("label: '新建存储过程'");
|
||||
expect(source.match(/sidebar\.tab\.create_function/g) || []).toHaveLength(2);
|
||||
expect(source.match(/sidebar\.tab\.create_procedure/g) || []).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps create routine catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.tab.create_function'], `${locale}:create function`).toBeTruthy();
|
||||
expect(catalog['sidebar.tab.create_procedure'], `${locale}:create procedure`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar create routine DuckDB template i18n', () => {
|
||||
it('localizes DuckDB procedure fallback comments without translating SQL Macro DDL', () => {
|
||||
expect(source).not.toContain('-- DuckDB 暂不支持存储过程');
|
||||
expect(source).not.toContain('-- 请使用 SQL Macro 作为函数能力');
|
||||
expect(source).toContain("t('sidebar.sql_template.duckdb_procedure_unsupported')");
|
||||
expect(source).toContain("t('sidebar.sql_template.duckdb_macro_hint')");
|
||||
expect(source).toContain('CREATE MACRO func_name(param1) AS (param1 * 2);');
|
||||
});
|
||||
|
||||
it('keeps DuckDB fallback catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.sql_template.duckdb_procedure_unsupported'], `${locale}:duckdb procedure unsupported`).toBeTruthy();
|
||||
expect(catalog['sidebar.sql_template.duckdb_macro_hint'], `${locale}:duckdb macro hint`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('Sidebar danger operations menu i18n', () => {
|
||||
it('localizes danger operation group labels', () => {
|
||||
expect(source).not.toContain("label: '危险操作'");
|
||||
expect(source.match(/label: t\('sidebar\.menu\.danger_operations'\)/g) || []).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.message.exporting_database_schema',
|
||||
'sidebar.message.exporting_database_backup',
|
||||
'sidebar.message.export_success',
|
||||
'sidebar.message.export_failed',
|
||||
] as const;
|
||||
|
||||
const extractHandleExportDatabaseBlock = (): string => {
|
||||
const start = source.indexOf('const handleExportDatabaseSQL = async');
|
||||
const end = source.indexOf('const handleExportSchemaSQL = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar database export feedback i18n', () => {
|
||||
it('opens database SQL exports in the workbench for review', () => {
|
||||
const block = extractHandleExportDatabaseBlock();
|
||||
|
||||
expect(block).not.toContain('showSQLExportOptionsDialog()');
|
||||
expect(block).toContain('addTab(buildDatabaseExportWorkbenchTab({');
|
||||
expect(block).toContain("contentMode: includeData ? 'backup' : 'schema'");
|
||||
expect(block).toContain('includeDropIfExists: false');
|
||||
expect(block).toContain("launchKey: createTableExportKey('database')");
|
||||
expect(block).not.toContain('requestKey:');
|
||||
expect(block).not.toContain('ExportDatabaseSQLWithOptions(');
|
||||
expect(block).not.toContain('message.loading(');
|
||||
});
|
||||
|
||||
it('keeps database export feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['sidebar.message.exporting_database_schema'])).toEqual(['database']);
|
||||
expect(placeholders(catalog['sidebar.message.exporting_database_backup'])).toEqual(['database']);
|
||||
expect(placeholders(catalog['sidebar.message.export_success'])).toEqual([]);
|
||||
expect(placeholders(catalog['sidebar.message.export_failed'])).toEqual(['error']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./sidebar/sidebarLegacyNodeMenu.tsx',
|
||||
'./sidebar/useSidebarObjectActions.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar delete routine menu i18n', () => {
|
||||
it('localizes the routine delete menu label and routine type', () => {
|
||||
expect(source).not.toContain('label: `删除${typeLabel}`');
|
||||
expect(source).toContain("label: t('sidebar.menu.delete_routine'");
|
||||
expect(source).toContain("t(routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function')");
|
||||
});
|
||||
|
||||
it('keeps delete routine catalog text usable in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.menu.delete_routine'], `${locale}:delete_routine`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.object.function'], `${locale}:function`).toBeTruthy();
|
||||
expect(catalog['sidebar.object.procedure'], `${locale}:procedure`).toBeTruthy();
|
||||
});
|
||||
|
||||
const zhCN = JSON.parse(readFileSync(new URL('../../../shared/i18n/zh-CN.json', import.meta.url), 'utf8')) as Record<string, string>;
|
||||
const zhTW = JSON.parse(readFileSync(new URL('../../../shared/i18n/zh-TW.json', import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(zhCN['sidebar.menu.delete_routine']).toBe('删除{{type}}');
|
||||
expect(zhTW['sidebar.menu.delete_routine']).toBe('刪除{{type}}');
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const dropRoutineSource = source.slice(
|
||||
source.indexOf('const handleDropRoutine ='),
|
||||
source.indexOf('const resolveMessagePublishTarget ='),
|
||||
);
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar drop routine confirm i18n', () => {
|
||||
it('localizes the drop routine confirm dialog and feedback copy', () => {
|
||||
expect(dropRoutineSource).toContain('const handleDropRoutine =');
|
||||
expect(dropRoutineSource).not.toContain('const typeLabel = routineType === \'PROCEDURE\' ? \'存储过程\' : \'函数\';');
|
||||
expect(dropRoutineSource).not.toContain('title: `确认删除${typeLabel}`');
|
||||
expect(dropRoutineSource).not.toContain('content: `确定删除${typeLabel} "${routineName}" 吗?该操作不可恢复。`');
|
||||
expect(dropRoutineSource).not.toContain('message.success(`${typeLabel}删除成功`)');
|
||||
expect(dropRoutineSource).not.toContain('message.error("删除失败: " + res.message)');
|
||||
expect(dropRoutineSource).toContain("title: t('sidebar.modal.confirm_delete_routine.title'");
|
||||
expect(dropRoutineSource).toContain("content: t('sidebar.modal.confirm_delete_routine.content'");
|
||||
expect(dropRoutineSource).toContain("message.success(t('sidebar.message.routine_deleted'");
|
||||
expect(dropRoutineSource).toContain("message.error(t('sidebar.message.delete_failed'");
|
||||
expect(dropRoutineSource).toContain("t(routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function')");
|
||||
});
|
||||
|
||||
it('keeps drop routine catalog placeholders aligned', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.modal.confirm_delete_routine.title'], `${locale}:title`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.modal.confirm_delete_routine.content'], `${locale}:content type`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.modal.confirm_delete_routine.content'], `${locale}:content name`).toContain('{{name}}');
|
||||
expect(catalog['sidebar.message.routine_deleted'], `${locale}:routine deleted`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.message.delete_failed'], `${locale}:delete failed`).toContain('{{error}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('Sidebar edit definition menu i18n', () => {
|
||||
it('localizes edit definition menu labels', () => {
|
||||
expect(source).not.toContain("label: '编辑定义'");
|
||||
expect(source.match(/label: t\('sidebar\.menu\.edit_definition'\)/g) || []).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const key = 'sidebar.tab.edit_event';
|
||||
|
||||
describe('Sidebar edit event tab title i18n', () => {
|
||||
it('localizes edit event query tab titles', () => {
|
||||
expect(source).not.toContain('title: `编辑事件: ${eventName}`');
|
||||
expect(source).toContain(`title: t('${key}', { name: eventName })`);
|
||||
});
|
||||
|
||||
it('keeps the edit event tab key available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
expect(catalog[key], `${locale}:${key}`).toContain('{{name}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar edit routine SQL template i18n', () => {
|
||||
it('localizes generated edit routine SQL comments without translating DDL', () => {
|
||||
expect(source).not.toContain('-- 编辑${typeLabel} ${routineName}');
|
||||
expect(source).toContain("t('sidebar.sql_template.edit_routine'");
|
||||
expect(source).toContain('CREATE OR REPLACE ${lines}');
|
||||
expect(source).toContain('\\n${ddl}');
|
||||
expect(source).toContain('\\n${def}');
|
||||
});
|
||||
|
||||
it('keeps edit routine SQL template placeholders aligned', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.sql_template.edit_routine'], `${locale}:edit routine sql type`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.sql_template.edit_routine'], `${locale}:edit routine sql name`).toContain('{{name}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar edit routine tab i18n', () => {
|
||||
it('localizes edit routine tab titles without touching routine names', () => {
|
||||
expect(source).not.toContain('title: `编辑${typeLabel}: ${routineName}`');
|
||||
expect(source).toContain("title: t('sidebar.tab.edit_routine'");
|
||||
expect(source).toContain("name: routineName");
|
||||
});
|
||||
|
||||
it('keeps edit routine tab catalog placeholders aligned', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.tab.edit_routine'], `${locale}:edit routine type`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.tab.edit_routine'], `${locale}:edit routine name`).toContain('{{name}}');
|
||||
expect(catalog['sidebar.object.function'], `${locale}:function`).toBeTruthy();
|
||||
expect(catalog['sidebar.object.procedure'], `${locale}:procedure`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar edit view SQL template i18n', () => {
|
||||
it('localizes generated edit view SQL comments without translating DDL', () => {
|
||||
expect(source).not.toContain('-- 编辑视图 ${viewName}');
|
||||
expect(source).not.toContain('-- 请修改后执行');
|
||||
expect(source).toContain("t('sidebar.sql_template.edit_view'");
|
||||
expect(source).toContain("t('sidebar.sql_template.modify_then_execute')");
|
||||
expect(source).toContain('CREATE OR REPLACE VIEW ${viewName} AS');
|
||||
expect(source).toContain('CREATE VIEW ${viewName} AS');
|
||||
});
|
||||
|
||||
it('keeps edit view SQL template catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.sql_template.edit_view'], `${locale}:edit view sql name`).toContain('{{name}}');
|
||||
expect(catalog['sidebar.sql_template.modify_then_execute'], `${locale}:modify then execute`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const key = 'sidebar.tab.event';
|
||||
|
||||
describe('Sidebar event tab title i18n', () => {
|
||||
it('localizes event definition tab titles', () => {
|
||||
expect(source).not.toContain('title: `事件: ${eventName}`');
|
||||
expect(source).toContain(`title: t('${key}', { name: eventName })`);
|
||||
});
|
||||
|
||||
it('keeps the event tab key available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
expect(catalog[key], `${locale}:${key}`).toContain('{{name}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const externalSqlCrudBlock = source.slice(
|
||||
source.indexOf('const openCreateExternalSQLFileModal ='),
|
||||
source.indexOf('const handleDeleteExternalSQLFile ='),
|
||||
);
|
||||
|
||||
describe('Sidebar external SQL create and rename feedback i18n', () => {
|
||||
it('localizes create and rename file or directory feedback while keeping names and backend details raw', () => {
|
||||
[
|
||||
'未找到可新建 SQL 文件的目录',
|
||||
'未找到可重命名的 SQL 文件',
|
||||
'未找到可新建目录的位置',
|
||||
'未找到可重命名的目录',
|
||||
'目录名不能为空',
|
||||
'SQL 文件名不能为空',
|
||||
'新建 SQL 文件失败: ',
|
||||
'SQL 文件已新建',
|
||||
'重命名 SQL 文件失败: ',
|
||||
'SQL 文件已重命名',
|
||||
'新建目录失败: ',
|
||||
'目录已新建',
|
||||
'重命名目录失败: ',
|
||||
'目录已重命名,但无法同步外部 SQL 目录列表,请重新添加目录',
|
||||
'SQL目录',
|
||||
'目录已重命名',
|
||||
].forEach((snippet) => {
|
||||
expect(externalSqlCrudBlock).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.message.external_sql_file_parent_missing',
|
||||
'sidebar.message.external_sql_file_rename_target_missing',
|
||||
'sidebar.message.external_sql_directory_parent_missing',
|
||||
'sidebar.message.external_sql_directory_rename_target_missing',
|
||||
'sidebar.message.sql_file_name_required',
|
||||
'sidebar.message.sql_directory_name_required',
|
||||
'sidebar.message.create_sql_file_failed',
|
||||
'sidebar.message.sql_file_created',
|
||||
'sidebar.message.rename_sql_file_failed',
|
||||
'sidebar.message.sql_file_renamed',
|
||||
'sidebar.message.create_sql_directory_failed',
|
||||
'sidebar.message.sql_directory_created',
|
||||
'sidebar.message.rename_sql_directory_failed',
|
||||
'sidebar.message.external_sql_directory_rename_sync_failed',
|
||||
'sidebar.message.sql_directory_renamed',
|
||||
'sidebar.sql_directory.default_name',
|
||||
].forEach((key) => {
|
||||
expect(externalSqlCrudBlock).toContain(key);
|
||||
});
|
||||
|
||||
expect(externalSqlCrudBlock).toContain('error: res.message');
|
||||
expect(externalSqlCrudBlock).toContain('nextName || nextPath.split');
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const externalSqlDeleteBlock = source.slice(
|
||||
source.indexOf('const handleDeleteExternalSQLFile ='),
|
||||
source.indexOf('const handleAddExternalSQLDirectory ='),
|
||||
);
|
||||
|
||||
describe('Sidebar external SQL delete feedback i18n', () => {
|
||||
it('localizes file and directory delete feedback while keeping names and backend details raw', () => {
|
||||
[
|
||||
'SQL 文件',
|
||||
'未找到可删除的 SQL 文件',
|
||||
'确认删除 SQL 文件',
|
||||
'该操作会删除本地磁盘文件,无法恢复。',
|
||||
'删除 SQL 文件失败: ',
|
||||
'SQL 文件已删除',
|
||||
'目录',
|
||||
'未找到可删除的目录',
|
||||
'确认删除目录',
|
||||
'该操作会删除本地磁盘目录,且仅支持删除空目录。',
|
||||
'删除目录失败: ',
|
||||
'目录已删除',
|
||||
].forEach((snippet) => {
|
||||
expect(externalSqlDeleteBlock).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.sql_file.default_name',
|
||||
'sidebar.message.external_sql_file_delete_target_missing',
|
||||
'sidebar.modal.confirm_delete_sql_file.title',
|
||||
'sidebar.modal.confirm_delete_sql_file.content',
|
||||
'sidebar.message.delete_sql_file_failed',
|
||||
'sidebar.message.sql_file_deleted',
|
||||
'sidebar.sql_directory.default_name',
|
||||
'sidebar.message.external_sql_directory_delete_target_missing',
|
||||
'sidebar.modal.confirm_delete_sql_directory.title',
|
||||
'sidebar.modal.confirm_delete_sql_directory.content',
|
||||
'sidebar.message.delete_sql_directory_failed',
|
||||
'sidebar.message.sql_directory_deleted',
|
||||
].forEach((key) => {
|
||||
expect(externalSqlDeleteBlock).toContain(key);
|
||||
});
|
||||
|
||||
expect(externalSqlDeleteBlock).toContain('name: fileName');
|
||||
expect(externalSqlDeleteBlock).toContain('name: directoryName');
|
||||
expect(externalSqlDeleteBlock).toContain('error: res.message');
|
||||
expect(externalSqlDeleteBlock).toContain('DeleteSQLFile(filePath)');
|
||||
expect(externalSqlDeleteBlock).toContain('DeleteSQLDirectory(directoryPath)');
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
const externalSqlMenuStart = source.indexOf("if (node.type === 'external-sql-root')");
|
||||
const externalSqlMenuEnd = source.indexOf(' return [];', externalSqlMenuStart);
|
||||
|
||||
const externalSqlMenuBlock = source.slice(
|
||||
externalSqlMenuStart,
|
||||
externalSqlMenuEnd,
|
||||
);
|
||||
|
||||
describe('Sidebar external SQL menu labels i18n', () => {
|
||||
it('localizes external SQL tree menu labels without changing node actions', () => {
|
||||
[
|
||||
"label: '新建 SQL 文件'",
|
||||
"label: '新建目录'",
|
||||
"label: '重命名目录'",
|
||||
"label: '刷新目录'",
|
||||
"label: '删除本地目录'",
|
||||
"label: '删除目录'",
|
||||
"label: '重命名 SQL 文件'",
|
||||
"label: '在此目录新建 SQL 文件'",
|
||||
"label: '在此目录新建目录'",
|
||||
"label: '删除 SQL 文件'",
|
||||
].forEach((snippet) => {
|
||||
expect(externalSqlMenuBlock).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.menu.add_sql_directory',
|
||||
'sidebar.menu.new_sql_file',
|
||||
'sidebar.menu.new_sql_directory',
|
||||
'sidebar.menu.rename_sql_directory',
|
||||
'sidebar.menu.refresh_directory',
|
||||
'sidebar.menu.remove_directory',
|
||||
'sidebar.menu.delete_local_directory',
|
||||
'sidebar.menu.delete_sql_directory',
|
||||
'sidebar.menu.open_sql_file',
|
||||
'sidebar.menu.rename_sql_file',
|
||||
'sidebar.menu.new_sql_file_in_directory',
|
||||
'sidebar.menu.new_sql_directory_in_directory',
|
||||
'sidebar.menu.delete_sql_file',
|
||||
].forEach((key) => {
|
||||
expect(externalSqlMenuBlock).toContain(key);
|
||||
});
|
||||
|
||||
[
|
||||
'openCreateExternalSQLFileModal(node)',
|
||||
'openCreateExternalSQLDirectoryModal(node)',
|
||||
'openRenameExternalSQLDirectoryModal(node)',
|
||||
'handleRefreshExternalSQLDirectory(node)',
|
||||
'handleRemoveExternalSQLDirectory(node)',
|
||||
'handleDeleteExternalSQLDirectory(node)',
|
||||
'openRenameExternalSQLFileModal(node)',
|
||||
'openExternalSQLFile(node)',
|
||||
'handleDeleteExternalSQLFile(node)',
|
||||
].forEach((action) => {
|
||||
expect(externalSqlMenuBlock).toContain(action);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
|
||||
const modalStart = source.indexOf('export const ExternalSQLFileModal');
|
||||
const modalEnd = source.indexOf('export const SQLFileExecutionModal');
|
||||
const externalSqlModalBlock = source.slice(modalStart, modalEnd);
|
||||
|
||||
describe('Sidebar external SQL file modal i18n', () => {
|
||||
it('localizes external SQL create and rename modal chrome without translating names or paths', () => {
|
||||
[
|
||||
"'新建 SQL 文件'",
|
||||
"'重命名 SQL 文件'",
|
||||
"'新建目录'",
|
||||
"'重命名目录'",
|
||||
"? '新建' : '重命名'",
|
||||
'cancelText="取消"',
|
||||
"? '目录名' : 'SQL 文件名'",
|
||||
"'请输入目录名'",
|
||||
"'请输入 SQL 文件名'",
|
||||
"'目录名不能包含路径分隔符'",
|
||||
"'文件名不能包含路径分隔符'",
|
||||
"'目录只会显示在外部 SQL 目录树中,非 SQL 文件仍不会显示'",
|
||||
"'不输入 .sql 后缀时会自动补齐'",
|
||||
"'例如:reports'",
|
||||
"'例如:report.sql'",
|
||||
].forEach((snippet) => {
|
||||
expect(externalSqlModalBlock).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.external_sql_modal.title.create_file',
|
||||
'sidebar.external_sql_modal.title.rename_file',
|
||||
'sidebar.external_sql_modal.title.create_directory',
|
||||
'sidebar.external_sql_modal.title.rename_directory',
|
||||
'sidebar.external_sql_modal.action.create',
|
||||
'sidebar.external_sql_modal.action.rename',
|
||||
'common.cancel',
|
||||
'sidebar.external_sql_modal.field.directory_name',
|
||||
'sidebar.external_sql_modal.field.sql_file_name',
|
||||
'sidebar.external_sql_modal.validation.directory_name_required',
|
||||
'sidebar.external_sql_modal.validation.sql_file_name_required',
|
||||
'sidebar.external_sql_modal.validation.directory_name_no_separator',
|
||||
'sidebar.external_sql_modal.validation.sql_file_name_no_separator',
|
||||
'sidebar.external_sql_modal.help.directory',
|
||||
'sidebar.external_sql_modal.help.sql_file',
|
||||
'sidebar.external_sql_modal.placeholder.directory_name',
|
||||
'sidebar.external_sql_modal.placeholder.sql_file_name',
|
||||
].forEach((key) => {
|
||||
expect(externalSqlModalBlock).toContain(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const externalSqlOpenBlock = source.slice(
|
||||
source.indexOf('const normalizeSQLFileDialogData ='),
|
||||
source.indexOf('const openCreateExternalSQLFileModal ='),
|
||||
);
|
||||
|
||||
describe('Sidebar external SQL open feedback i18n', () => {
|
||||
it('localizes SQL file open fallbacks without translating raw file details', () => {
|
||||
[
|
||||
'运行外部SQL文件',
|
||||
'SQL 文件路径不完整,无法打开',
|
||||
'请先选择一个 Host 后再执行大 SQL 文件',
|
||||
].forEach((snippet) => {
|
||||
expect(externalSqlOpenBlock).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(externalSqlOpenBlock).toContain("t('sidebar.sql_file_exec.title')");
|
||||
expect(externalSqlOpenBlock).toContain("t('sidebar.message.sql_file_path_incomplete')");
|
||||
expect(externalSqlOpenBlock).toContain("t('sidebar.message.select_host_before_large_sql_file')");
|
||||
expect(externalSqlOpenBlock).toContain("t('sidebar.message.read_sql_file_failed', { error: res.message })");
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./Sidebar.tsx',
|
||||
'./sidebar/SidebarExternalSqlWorkflow.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
|
||||
describe('Sidebar external SQL refresh i18n', () => {
|
||||
it('localizes global external SQL refresh feedback while preserving raw directory details', () => {
|
||||
[
|
||||
'SQL 目录读取失败:',
|
||||
"message.success('外部 SQL 目录已刷新')",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
const readFailureKeyUses = source.match(/t\('sidebar\.message\.external_sql_directory_read_failed'/g) || [];
|
||||
const refreshedKeyUses = source.match(/t\('sidebar\.message\.external_sql_directory_refreshed'/g) || [];
|
||||
expect(readFailureKeyUses.length).toBeGreaterThanOrEqual(1);
|
||||
expect(refreshedKeyUses.length).toBeGreaterThanOrEqual(2);
|
||||
expect(source).toContain('name: directory.name');
|
||||
expect(source).toContain('error: directoryRes.message');
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('./sidebar/SidebarSearchPanel.tsx', import.meta.url), 'utf8'),
|
||||
].join('\n');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.command_search.sync_to_filter_tooltip',
|
||||
'sidebar.command_search.sync_to_filter_aria',
|
||||
'sidebar.command_search.reset_filter',
|
||||
'sidebar.command_search.no_synced_filter',
|
||||
'sidebar.command_search.no_filter_content',
|
||||
'sidebar.message.sidebar_filter_sync_enabled',
|
||||
'sidebar.message.sidebar_filter_sync_disabled',
|
||||
'sidebar.message.sidebar_filter_reset',
|
||||
];
|
||||
|
||||
describe('Sidebar filter sync i18n', () => {
|
||||
it('localizes v2 sidebar filter sync and reset shell text', () => {
|
||||
[
|
||||
'已开启左侧筛选同步',
|
||||
'已关闭左侧筛选同步',
|
||||
'已重置侧栏筛选',
|
||||
'同步输入内容到左侧筛选',
|
||||
'同步到左侧筛选',
|
||||
'重置侧栏筛选',
|
||||
'没有已同步的侧栏筛选',
|
||||
'没有筛选内容',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps v2 sidebar filter sync keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./Sidebar.tsx',
|
||||
'./sidebar/useSidebarTreeLoaders.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.message.load_table_list_failed',
|
||||
'sidebar.message.locate_external_sql_file_not_found',
|
||||
'sidebar.message.locate_connection_not_found_for_object',
|
||||
'sidebar.message.locate_connection_not_in_tree',
|
||||
'sidebar.message.locate_database_loading',
|
||||
'sidebar.message.locate_database_not_found',
|
||||
'sidebar.message.locate_object_loading',
|
||||
'sidebar.message.locate_object_not_found',
|
||||
'sidebar.locate.object.table',
|
||||
'sidebar.locate.object.view',
|
||||
'sidebar.locate.object.materialized_view',
|
||||
'sidebar.locate.object.routine',
|
||||
'sidebar.locate.object.trigger',
|
||||
];
|
||||
|
||||
describe('Sidebar locate messages i18n', () => {
|
||||
it('localizes locate and load-table user messages', () => {
|
||||
[
|
||||
"'加载表失败: '",
|
||||
'SQL 文件未在外部 SQL 目录中找到',
|
||||
'未找到当前表对应的连接',
|
||||
"'未在左侧树找到当前连接'",
|
||||
'数据库节点仍在加载中',
|
||||
'未在左侧树找到数据库',
|
||||
'所在数据库对象仍在加载中',
|
||||
'未在左侧树中找到',
|
||||
"request.objectGroup === 'materializedViews'\r\n ? '物化视图'",
|
||||
": '表';",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps locate message keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const sidebarSource = [
|
||||
'./Sidebar.tsx',
|
||||
'./sidebar/SidebarEntityModals.tsx',
|
||||
'../App.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
const requiredKeys = [
|
||||
'sidebar.action.new_group',
|
||||
'sidebar.action.locate_current_tab',
|
||||
'sidebar.message.locate_current_tab_unavailable',
|
||||
'app.sidebar.sql_execution_log',
|
||||
'sidebar.modal.create_database.title',
|
||||
'sidebar.field.database_name',
|
||||
'sidebar.validation.name_required',
|
||||
'sidebar.modal.rename_schema.title',
|
||||
'sidebar.field.schema_name',
|
||||
'sidebar.validation.schema_name_required',
|
||||
'sidebar.modal.rename_table.title',
|
||||
'sidebar.field.new_table_name',
|
||||
'sidebar.validation.new_table_name_required',
|
||||
'sidebar.modal.rename_view.title',
|
||||
'sidebar.field.new_view_name',
|
||||
'sidebar.validation.new_view_name_required',
|
||||
] as const;
|
||||
|
||||
describe('Sidebar management modals i18n', () => {
|
||||
it('localizes legacy toolbar and management modal copy', () => {
|
||||
[
|
||||
'title="新建数据库"',
|
||||
'label="数据库名称"',
|
||||
"message: '请输入名称'",
|
||||
'title="新建组"',
|
||||
'aria-label="新建组"',
|
||||
'定位当前标签页',
|
||||
'当前标签页没有可定位的内容',
|
||||
'SQL 执行日志',
|
||||
'编辑模式${',
|
||||
'label="模式名称"',
|
||||
"message: '请输入模式名称'",
|
||||
'重命名表${',
|
||||
'label="新表名"',
|
||||
"message: '请输入新表名'",
|
||||
'重命名视图${',
|
||||
'label="新视图名"',
|
||||
"message: '请输入新视图名'",
|
||||
].forEach((rawSnippet) => {
|
||||
expect(sidebarSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(sidebarSource, key).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps management modal catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./sidebar/sidebarLegacyNodeMenu.tsx',
|
||||
'./sidebar/useSidebarObjectActions.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar materialized view create menu i18n', () => {
|
||||
it('localizes the materialized view group create action label', () => {
|
||||
expect(source).not.toContain("label: '新建物化视图'");
|
||||
expect(source).toContain("label: t('sidebar.v2_database_menu.new_materialized_view')");
|
||||
});
|
||||
|
||||
it('keeps the materialized view create action catalog entry available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.v2_database_menu.new_materialized_view'], `${locale}:new materialized view`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar materialized view menu labels i18n', () => {
|
||||
it('localizes materialized view context menu labels', () => {
|
||||
expect(source).not.toContain("label: '浏览物化视图数据'");
|
||||
expect(source).not.toContain("label: '查看物化视图定义'");
|
||||
expect(source).toContain("label: t('sidebar.menu.browse_materialized_view_data')");
|
||||
expect(source).toContain("label: t('sidebar.menu.materialized_view_definition')");
|
||||
});
|
||||
|
||||
it('keeps materialized view context menu catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.menu.browse_materialized_view_data'], `${locale}:browse materialized view data`).toBeTruthy();
|
||||
expect(catalog['sidebar.menu.materialized_view_definition'], `${locale}:materialized view definition`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const key = 'sidebar.message.visual_new_table_unsupported';
|
||||
|
||||
describe('Sidebar visual new table unsupported warning i18n', () => {
|
||||
it('localizes the visual new table unsupported warning', () => {
|
||||
expect(source).not.toContain("message.warning('当前数据源暂不支持可视化新建表')");
|
||||
expect(source).toContain(`message.warning(t('${key}'))`);
|
||||
});
|
||||
|
||||
it('keeps the warning key available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const objectActionsSource = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const legacyMenuSource = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
const tableDataDangerActionsSource = readFileSync(new URL('./tableDataDangerActions.ts', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
const requiredKeys = [
|
||||
'sidebar.message.schema_edit_unsupported',
|
||||
'sidebar.message.schema_target_edit_missing',
|
||||
'sidebar.message.schema_name_unchanged',
|
||||
'sidebar.message.schema_renamed',
|
||||
'sidebar.message.schema_target_delete_missing',
|
||||
'sidebar.message.schema_deleted',
|
||||
'sidebar.message.table_export_target_missing',
|
||||
'sidebar.modal.confirm_delete_schema.title',
|
||||
'sidebar.modal.confirm_delete_schema.content',
|
||||
'sidebar.menu.edit_schema',
|
||||
'sidebar.menu.export_current_schema_sql',
|
||||
'sidebar.menu.backup_current_schema_sql',
|
||||
'sidebar.menu.delete_schema',
|
||||
'sidebar.menu.copy_object_name',
|
||||
'sidebar.menu.table_structure',
|
||||
'sidebar.menu.design_table',
|
||||
'sidebar.menu.copy_table_name',
|
||||
'sidebar.menu.copy_database_name',
|
||||
'sidebar.copy_object_name.label.database',
|
||||
'sidebar.menu.copy_table_structure',
|
||||
'sidebar.menu.backup_table_sql',
|
||||
'sidebar.menu.rename_table',
|
||||
'sidebar.menu.truncate_table',
|
||||
'sidebar.menu.clear_table',
|
||||
'sidebar.menu.delete_table',
|
||||
'sidebar.menu.export_table_data',
|
||||
'sidebar.v2_table_menu.new_rollup',
|
||||
'sidebar.message.table_name_required',
|
||||
'sidebar.message.table_name_unchanged',
|
||||
'sidebar.message.table_renamed',
|
||||
'sidebar.message.table_deleted',
|
||||
'sidebar.modal.confirm_delete_table.title',
|
||||
'sidebar.modal.confirm_delete_table.content',
|
||||
'sidebar.message.view_name_required',
|
||||
'sidebar.message.view_name_unchanged',
|
||||
'sidebar.message.view_renamed',
|
||||
'sidebar.message.view_deleted',
|
||||
'sidebar.modal.confirm_delete_view.title',
|
||||
'sidebar.modal.confirm_delete_view.content',
|
||||
'sidebar.message.rename_failed',
|
||||
'sidebar.message.delete_failed',
|
||||
'sidebar.message.table_data_action_loading',
|
||||
'sidebar.message.table_data_action_success',
|
||||
'sidebar.message.table_data_action_failed',
|
||||
'sidebar.modal.confirm_table_data_action.title',
|
||||
'sidebar.modal.confirm_table_data_action.content',
|
||||
'sidebar.table_action.truncate.label',
|
||||
'sidebar.table_action.truncate.progress',
|
||||
'sidebar.table_action.clear.label',
|
||||
'sidebar.table_action.clear.progress',
|
||||
] as const;
|
||||
|
||||
describe('Sidebar object actions i18n', () => {
|
||||
it('localizes schema, table, and view object action copy', () => {
|
||||
[
|
||||
'当前节点不支持通过此入口编辑模式',
|
||||
'未找到目标模式,无法编辑',
|
||||
'新旧模式名称相同,无需修改',
|
||||
'模式重命名成功',
|
||||
'编辑失败: ',
|
||||
'未找到目标模式,无法删除',
|
||||
'确认删除模式',
|
||||
'确定删除模式',
|
||||
'模式删除成功',
|
||||
'删除失败: ',
|
||||
'未识别到表名,无法导出',
|
||||
'新增 Rollup',
|
||||
'表名不能为空',
|
||||
'新旧表名相同,无需修改',
|
||||
'表重命名成功',
|
||||
'确认删除表',
|
||||
'确定删除表',
|
||||
'表删除成功',
|
||||
'确认${label}',
|
||||
'${label}会永久删除表',
|
||||
'正在${progressLabel}',
|
||||
'${progressLabel}成功',
|
||||
'${progressLabel}失败',
|
||||
'视图名称不能为空',
|
||||
'新旧视图名相同,无需修改',
|
||||
'视图重命名成功',
|
||||
'确认删除视图',
|
||||
'确定删除视图',
|
||||
'视图删除成功',
|
||||
].forEach((rawSnippet) => {
|
||||
expect(objectActionsSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
[
|
||||
"label: '编辑模式'",
|
||||
"label: '导出当前模式表结构 (SQL)'",
|
||||
"label: '备份当前模式全部表 (结构+数据 SQL)'",
|
||||
"label: '删除模式'",
|
||||
"label: '复制名称'",
|
||||
"label: '测试发送消息'",
|
||||
"label: '表结构'",
|
||||
"label: '设计表'",
|
||||
"label: '复制表名'",
|
||||
"label: '复制表结构'",
|
||||
"label: '备份表 (SQL)'",
|
||||
"label: '重命名表'",
|
||||
"label: '截断表'",
|
||||
"label: '清空表'",
|
||||
"label: '删除表'",
|
||||
"label: '导出表数据'",
|
||||
].forEach((rawSnippet) => {
|
||||
expect(legacyMenuSource).not.toContain(rawSnippet);
|
||||
});
|
||||
|
||||
[
|
||||
"t('sidebar.message.schema_edit_unsupported')",
|
||||
"t('sidebar.message.schema_target_edit_missing')",
|
||||
"t('sidebar.message.schema_name_unchanged')",
|
||||
"t('sidebar.message.schema_renamed')",
|
||||
"t('sidebar.message.schema_target_delete_missing')",
|
||||
"t('sidebar.message.table_export_target_missing')",
|
||||
"t('sidebar.message.schema_deleted')",
|
||||
"t('sidebar.v2_table_menu.new_rollup'",
|
||||
"t('sidebar.modal.confirm_delete_schema.title')",
|
||||
"t('sidebar.modal.confirm_delete_schema.content'",
|
||||
"t('sidebar.message.table_name_required')",
|
||||
"t('sidebar.message.table_name_unchanged')",
|
||||
"t('sidebar.message.table_renamed')",
|
||||
"t('sidebar.message.table_deleted')",
|
||||
"t('sidebar.modal.confirm_delete_table.title')",
|
||||
"t('sidebar.modal.confirm_delete_table.content'",
|
||||
"t('sidebar.message.view_name_required')",
|
||||
"t('sidebar.message.view_name_unchanged')",
|
||||
"t('sidebar.message.view_renamed')",
|
||||
"t('sidebar.message.view_deleted')",
|
||||
"t('sidebar.modal.confirm_delete_view.title')",
|
||||
"t('sidebar.modal.confirm_delete_view.content'",
|
||||
"t('sidebar.message.rename_failed'",
|
||||
"t('sidebar.message.delete_failed'",
|
||||
"t('sidebar.message.table_data_action_loading'",
|
||||
"t('sidebar.message.table_data_action_success'",
|
||||
"t('sidebar.message.table_data_action_failed'",
|
||||
"t('sidebar.modal.confirm_table_data_action.title'",
|
||||
"t('sidebar.modal.confirm_table_data_action.content'",
|
||||
].forEach((lookup) => {
|
||||
expect(objectActionsSource).toContain(lookup);
|
||||
});
|
||||
|
||||
[
|
||||
"t('sidebar.menu.edit_schema')",
|
||||
"t('sidebar.menu.export_current_schema_sql')",
|
||||
"t('sidebar.menu.backup_current_schema_sql')",
|
||||
"t('sidebar.menu.delete_schema')",
|
||||
"t('sidebar.menu.copy_object_name')",
|
||||
"t('message_publish_modal.title')",
|
||||
"t('sidebar.menu.table_structure')",
|
||||
"t('sidebar.menu.design_table')",
|
||||
"t('sidebar.menu.copy_table_name')",
|
||||
"t('sidebar.menu.copy_database_name')",
|
||||
"t('sidebar.menu.copy_table_structure')",
|
||||
"t('sidebar.menu.backup_table_sql')",
|
||||
"t('sidebar.menu.rename_table')",
|
||||
"t('sidebar.menu.truncate_table')",
|
||||
"t('sidebar.menu.clear_table')",
|
||||
"t('sidebar.menu.delete_table')",
|
||||
"t('sidebar.menu.export_table_data')",
|
||||
].forEach((lookup) => {
|
||||
expect(legacyMenuSource).toContain(lookup);
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes table data danger action metadata', () => {
|
||||
expect(tableDataDangerActionsSource).not.toContain("return { label: '截断表', progressLabel: '截断' };");
|
||||
expect(tableDataDangerActionsSource).not.toContain("return { label: '清空表', progressLabel: '清空' };");
|
||||
expect(tableDataDangerActionsSource).toContain("'sidebar.table_action.truncate.label'");
|
||||
expect(tableDataDangerActionsSource).toContain("'sidebar.table_action.truncate.progress'");
|
||||
expect(tableDataDangerActionsSource).toContain("'sidebar.table_action.clear.label'");
|
||||
expect(tableDataDangerActionsSource).toContain("'sidebar.table_action.clear.progress'");
|
||||
});
|
||||
|
||||
it('keeps object action catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('./sidebar/useSidebarTreeLoaders.tsx', import.meta.url), 'utf8'),
|
||||
].join('\n');
|
||||
|
||||
const appSource = readFileSync(new URL('../App.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.tree.default_schema',
|
||||
'sidebar.object_group.tables',
|
||||
'sidebar.object_group.views',
|
||||
'sidebar.object_group.materialized_views',
|
||||
'sidebar.object_group.sequences',
|
||||
'sidebar.object_group.routines',
|
||||
'sidebar.object_group.packages',
|
||||
'sidebar.object_group.triggers',
|
||||
'sidebar.object_group.events',
|
||||
];
|
||||
|
||||
const visibilityKeys = [
|
||||
'app.settings.sidebar_objects.title',
|
||||
'app.settings.sidebar_objects.description',
|
||||
'app.settings.sidebar_objects.action.show_all',
|
||||
'app.settings.sidebar_objects.action.tables_only',
|
||||
];
|
||||
|
||||
describe('Sidebar object group i18n', () => {
|
||||
it('localizes database object group titles and default schema fallback', () => {
|
||||
[
|
||||
"const schemaTitle = bucket.schemaName || '默认模式'",
|
||||
"buildObjectGroup(schemaNodeKey, 'tables', '表'",
|
||||
"buildObjectGroup(schemaNodeKey, 'views', '视图'",
|
||||
"buildObjectGroup(schemaNodeKey, 'materializedViews', '物化视图'",
|
||||
"buildObjectGroup(schemaNodeKey, 'sequences', '序列'",
|
||||
"buildObjectGroup(schemaNodeKey, 'routines', '函数'",
|
||||
"buildObjectGroup(schemaNodeKey, 'packages', '存储包'",
|
||||
"buildObjectGroup(schemaNodeKey, 'triggers', '触发器'",
|
||||
"buildObjectGroup(schemaNodeKey, 'events', '事件'",
|
||||
"buildObjectGroup(key as string, 'tables', '表'",
|
||||
"buildObjectGroup(key as string, 'views', '视图'",
|
||||
"buildObjectGroup(key as string, 'materializedViews', '物化视图'",
|
||||
"buildObjectGroup(key as string, 'sequences', '序列'",
|
||||
"buildObjectGroup(key as string, 'routines', '函数'",
|
||||
"buildObjectGroup(key as string, 'packages', '存储包'",
|
||||
"buildObjectGroup(key as string, 'triggers', '触发器'",
|
||||
"buildObjectGroup(key as string, 'events', '事件'",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps database object group keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes the persistent object visibility settings', () => {
|
||||
visibilityKeys.forEach((key) => {
|
||||
expect(appSource).toContain(`t('${key}'`);
|
||||
});
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
visibilityKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8'),
|
||||
].join('\n');
|
||||
|
||||
describe('Sidebar Redis DB menu i18n', () => {
|
||||
it('localizes Redis database context menu labels and tab titles', () => {
|
||||
[
|
||||
"label: '浏览 Key'",
|
||||
"label: '新建命令窗口'",
|
||||
"title: `命令 - db${redisDB}`",
|
||||
"label: 'Redis 实例监控'",
|
||||
"title: `监控 - db${redisDB}`",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain("label: t('redis_viewer.title.key_explorer')");
|
||||
expect(source).toContain("label: t('sidebar.menu.new_command_window')");
|
||||
expect(source).toContain("title: buildConnectionRootRedisCommandTabTitle(`db${redisDB}`)");
|
||||
expect(source).toContain("label: t('redis_monitor.title.instance')");
|
||||
expect(source).toContain("title: buildConnectionRootRedisMonitorTabTitle(`db${redisDB}`)");
|
||||
});
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const objectActionsSource = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const legacyMenuSource = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
const v2ActionHandlersSource = readFileSync(new URL('./sidebar/useSidebarV2ActionHandlers.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
const requiredKeys = [
|
||||
'sidebar.message.saved_query_rename_failed',
|
||||
'sidebar.message.saved_query_rebind_success',
|
||||
'sidebar.message.saved_query_rebind_failed',
|
||||
'sidebar.message.message_publish_unsupported',
|
||||
'sidebar.message.message_publish_success',
|
||||
'sidebar.message.message_publish_success_with_count',
|
||||
'sidebar.message.message_publish_target_fallback',
|
||||
'sidebar.message.connection_release_failed',
|
||||
'sidebar.message.connection_release_failed_from_sidebar',
|
||||
'sidebar.menu.new_table',
|
||||
'sidebar.menu.create_event',
|
||||
'sidebar.tab.new_event',
|
||||
'sidebar.modal.confirm_delete_tag.content',
|
||||
'sidebar.menu.bind_to_connection',
|
||||
'sidebar.message.saved_query_delete_failed',
|
||||
'sidebar.message.database_created',
|
||||
'sidebar.message.operation_create_failed',
|
||||
'sidebar.aria.switch_connection',
|
||||
];
|
||||
|
||||
describe('Sidebar residual actions i18n', () => {
|
||||
it('localizes saved-query, message publish, tag, and residual group-menu copy', () => {
|
||||
[
|
||||
'重命名查询失败: ',
|
||||
'查询已绑定到 ',
|
||||
'绑定查询失败: ',
|
||||
'数据库创建成功',
|
||||
'创建失败: ',
|
||||
'当前对象不支持测试发送消息',
|
||||
'(已提交 ',
|
||||
'测试消息已发送到 ',
|
||||
"destination || '目标'",
|
||||
].forEach((legacyCopy) => {
|
||||
expect(objectActionsSource).not.toContain(legacyCopy);
|
||||
});
|
||||
|
||||
[
|
||||
"'释放连接失败'",
|
||||
'连接已从侧边栏断开,但后端连接释放失败',
|
||||
].forEach((legacyCopy) => {
|
||||
expect(v2ActionHandlersSource).not.toContain(legacyCopy);
|
||||
});
|
||||
|
||||
[
|
||||
"label: '刷新'",
|
||||
"label: '新建表'",
|
||||
"label: '按名称排序'",
|
||||
"label: '按使用频率排序'",
|
||||
"label: '新建事件'",
|
||||
"title: '新建事件'",
|
||||
"label: '编辑标签'",
|
||||
"label: '删除标签'",
|
||||
"title: '确认删除'",
|
||||
'确定要删除标签',
|
||||
"label: '绑定到连接'",
|
||||
'删除查询失败: ',
|
||||
].forEach((legacyCopy) => {
|
||||
expect(legacyMenuSource).not.toContain(legacyCopy);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.message.saved_query_rename_failed',
|
||||
'sidebar.message.saved_query_rebind_success',
|
||||
'sidebar.message.saved_query_rebind_failed',
|
||||
'sidebar.message.database_created',
|
||||
'sidebar.message.operation_create_failed',
|
||||
'sidebar.message.message_publish_unsupported',
|
||||
'sidebar.message.message_publish_success',
|
||||
'sidebar.message.message_publish_success_with_count',
|
||||
'sidebar.message.message_publish_target_fallback',
|
||||
].forEach((key) => {
|
||||
expect(objectActionsSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.message.connection_release_failed_from_sidebar',
|
||||
'sidebar.menu.new_table',
|
||||
'sidebar.menu.create_event',
|
||||
'sidebar.tab.new_event',
|
||||
'sidebar.modal.confirm_delete_tag.content',
|
||||
'sidebar.menu.bind_to_connection',
|
||||
'sidebar.message.saved_query_delete_failed',
|
||||
].forEach((key) => {
|
||||
expect(`${legacyMenuSource}\n${v2ActionHandlersSource}`).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
[
|
||||
'sidebar.message.connection_release_failed_from_sidebar',
|
||||
].forEach((key) => {
|
||||
expect(v2ActionHandlersSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
expect(legacyMenuSource).toContain("label: conn.name || conn.id");
|
||||
expect(legacyMenuSource).toContain('node.title');
|
||||
});
|
||||
|
||||
it('keeps residual Sidebar keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const objectActionsSource = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const source = `${sidebarSource}\n${objectActionsSource}`;
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar routine definition tab i18n', () => {
|
||||
it('localizes routine definition tab titles', () => {
|
||||
expect(source).not.toContain('title: `${typeLabel}: ${routineName}`');
|
||||
expect(sidebarSource.match(/title: t\('sidebar\.tab\.routine_definition'/g) || []).toHaveLength(2);
|
||||
expect(source.match(/t\(routineType === 'PROCEDURE' \? 'sidebar\.object\.procedure' : 'sidebar\.object\.function'\)/g) || []).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('keeps routine definition tab catalog placeholders aligned', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.tab.routine_definition'], `${locale}:routine definition type`).toContain('{{type}}');
|
||||
expect(catalog['sidebar.tab.routine_definition'], `${locale}:routine definition name`).toContain('{{name}}');
|
||||
expect(catalog['sidebar.object.function'], `${locale}:function`).toBeTruthy();
|
||||
expect(catalog['sidebar.object.procedure'], `${locale}:procedure`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const hookSource = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8');
|
||||
const objectActionsSource = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const tableOverviewSource = readFileSync(new URL('./TableOverview.tsx', import.meta.url), 'utf8');
|
||||
const workbenchSource = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const bindingSource = readFileSync(new URL('../../wailsjs/go/app/App.d.ts', import.meta.url), 'utf8');
|
||||
const modelSource = readFileSync(new URL('../../wailsjs/go/models.ts', import.meta.url), 'utf8');
|
||||
|
||||
describe('Sidebar SQL export options', () => {
|
||||
it('opens database and table backups for review while retaining schema confirmation', () => {
|
||||
expect(hookSource).toContain('buildDatabaseExportWorkbenchTab({');
|
||||
expect(hookSource).toContain('buildSchemaExportWorkbenchTab({');
|
||||
expect(hookSource).toContain('buildBatchTableExportWorkbenchTab({');
|
||||
expect(hookSource).toContain('buildBatchDatabaseExportWorkbenchTab({');
|
||||
expect(hookSource.match(/showSQLExportOptionsDialog\(\)/g)).toHaveLength(1);
|
||||
expect(hookSource).toContain("launchKey: createTableExportKey('database')");
|
||||
expect(hookSource).toContain('const openBatchTableWorkbench = () =>');
|
||||
expect(hookSource).toContain('const openBatchDatabaseWorkbench = () =>');
|
||||
expect(objectActionsSource).toContain("if (options.format === 'sql')");
|
||||
expect(objectActionsSource).toContain("await openTableSQLExportWorkbench(node, 'backup')");
|
||||
expect(objectActionsSource).toContain("await openTableSQLExportWorkbench(node, 'dataOnly')");
|
||||
expect(objectActionsSource).not.toContain('showSQLExportOptionsDialog');
|
||||
expect(objectActionsSource).toContain("...(mode === 'backup' ? { launchKey } : { requestKey: launchKey })");
|
||||
expect(objectActionsSource).toContain('includeDropIfExists: false');
|
||||
expect(tableOverviewSource).not.toContain('showSQLExportOptionsDialog');
|
||||
expect(tableOverviewSource).toContain("...(mode === 'backup' ? { launchKey } : { requestKey: launchKey })");
|
||||
expect(workbenchSource).toContain('const [includeDropIfExists, setIncludeDropIfExists] = useState(');
|
||||
expect(workbenchSource).toContain('includeDropIfExists: includeSchema && includeDropIfExists');
|
||||
expect(workbenchSource).toContain('includeDropIfExists,');
|
||||
expect(workbenchSource).toContain('includeDatabaseContext,');
|
||||
expect(workbenchSource).toContain('onChange={(event) => setIncludeDropIfExists(event.target.checked)}');
|
||||
});
|
||||
|
||||
it('keeps the Wails option and typed single-database/schema methods in sync', () => {
|
||||
expect(bindingSource).toContain('ExportDatabaseSQLWithOptions(');
|
||||
expect(bindingSource).toContain('ExportSchemaSQLWithOptions(');
|
||||
expect(modelSource).toContain('includeDropIfExists?: boolean;');
|
||||
expect(modelSource).toContain('this.includeDropIfExists = source["includeDropIfExists"]');
|
||||
expect(modelSource).toContain('includeDatabaseContext?: boolean;');
|
||||
expect(modelSource).toContain('this.includeDatabaseContext = source["includeDatabaseContext"]');
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.tree.untitled_query',
|
||||
'sidebar.tree.default_database',
|
||||
'sidebar.tree.unknown_connection',
|
||||
'sidebar.tree.unmatched_saved_queries',
|
||||
'sidebar.tree.all_saved_queries',
|
||||
'sidebar.tree.ungrouped_saved_queries',
|
||||
'sidebar.saved_query_group.create_title',
|
||||
'sidebar.saved_query_group.edit_title',
|
||||
'sidebar.saved_query_group.new_group',
|
||||
'sidebar.saved_query_group.new_subgroup',
|
||||
'sidebar.saved_query_group.move_to_group',
|
||||
'sidebar.saved_query_group.move_to_ungrouped',
|
||||
'sidebar.saved_query_group.empty_queries',
|
||||
'sidebar.message.saved_query_group_created',
|
||||
'sidebar.message.saved_query_group_delete_failed',
|
||||
'sidebar.message.saved_query_group_move_failed',
|
||||
'sidebar.message.saved_query_group_save_failed',
|
||||
'sidebar.saved_query_group.error.backend_unavailable',
|
||||
'sidebar.saved_query_group.error.invalid_input',
|
||||
];
|
||||
|
||||
describe('Sidebar saved queries tree i18n', () => {
|
||||
it('localizes saved query fallback tree titles', () => {
|
||||
[
|
||||
"title: query.name || '未命名查询'",
|
||||
"|| '默认数据库'",
|
||||
"|| '未知连接'",
|
||||
"title: '未匹配'",
|
||||
"title: '全部已存查询'",
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain("query.name || t('sidebar.tree.untitled_query')");
|
||||
expect(source).toContain("t('sidebar.tree.default_database')");
|
||||
expect(source).toContain("t('sidebar.tree.unknown_connection')");
|
||||
expect(source).toContain("title: t('sidebar.tree.unmatched_saved_queries')");
|
||||
expect(source).toContain("title: t('sidebar.tree.all_saved_queries')");
|
||||
expect(source).toContain("title: t('sidebar.tree.ungrouped_saved_queries')");
|
||||
});
|
||||
|
||||
it('keeps saved query fallback keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8');
|
||||
const workbenchSource = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const runnerSource = readFileSync(new URL('./useExportProgressRunner.ts', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.message.schema_export_target_missing',
|
||||
'data_export.message.already_running',
|
||||
'data_export.message.export_success',
|
||||
'data_export.message.export_failed',
|
||||
] as const;
|
||||
|
||||
const extractHandleExportSchemaBlock = (): string => {
|
||||
const start = source.indexOf('const handleExportSchemaSQL = async');
|
||||
const end = source.indexOf('const openBatchTableWorkbench = () =>', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const extractDirectSchemaExportBlock = (): string => {
|
||||
const start = workbenchSource.indexOf('const handleStartDirectSchemaExport = async');
|
||||
const end = workbenchSource.indexOf('const handleStartExport = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return workbenchSource.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar schema export feedback i18n', () => {
|
||||
it('validates the target and routes schema SQL export into the background workbench', () => {
|
||||
const block = extractHandleExportSchemaBlock();
|
||||
const executionBlock = extractDirectSchemaExportBlock();
|
||||
|
||||
expect(block).toContain("t('sidebar.message.schema_export_target_missing')");
|
||||
expect(block).toContain('showSQLExportOptionsDialog()');
|
||||
expect(block).toContain('addTab(buildSchemaExportWorkbenchTab({');
|
||||
expect(block).toContain('schemaName,');
|
||||
expect(block).toContain("contentMode: includeData ? 'backup' : 'schema'");
|
||||
expect(block).toContain('includeDropIfExists: exportOptions.includeDropIfExists');
|
||||
expect(block).toContain("requestKey: createTableExportKey('schema')");
|
||||
expect(block).not.toContain('ExportSchemaSQLWithOptions(');
|
||||
expect(block).not.toContain('message.loading(');
|
||||
expect(executionBlock).toContain('await runExportWithProgress({');
|
||||
expect(executionBlock).toContain('ExportSchemaSQLWithOptions(');
|
||||
expect(executionBlock).toContain('buildRpcConnectionConfig(connectionConfig, { database: effectiveDbName })');
|
||||
expect(executionBlock).toContain('includeData,');
|
||||
expect(executionBlock).toContain('includeDropIfExists,');
|
||||
expect(runnerSource).toContain("message.warning(t('data_export.message.already_running'))");
|
||||
expect(runnerSource).toContain("message.success(t('data_export.message.export_success'))");
|
||||
expect(runnerSource).toContain("message.error(t('data_export.message.export_failed', { error: result.message }))");
|
||||
});
|
||||
|
||||
it('keeps schema export feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['sidebar.message.schema_export_target_missing'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.already_running'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_success'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_failed'])).toEqual(['error']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarTreeLoaders.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.object_group.views',
|
||||
'sidebar.object_group.routines',
|
||||
'sidebar.object_group.triggers',
|
||||
'sidebar.message.sphinx_unsupported_objects',
|
||||
'sidebar.punctuation.list_separator',
|
||||
];
|
||||
|
||||
describe('Sidebar Sphinx capability i18n', () => {
|
||||
it('localizes Sphinx unsupported object capability warning text', () => {
|
||||
[
|
||||
"unsupportedObjects.push('视图')",
|
||||
"unsupportedObjects.push('函数/存储过程')",
|
||||
"unsupportedObjects.push('触发器')",
|
||||
"unsupportedObjects.join('、')",
|
||||
'当前 Sphinx 实例未开放以下对象能力',
|
||||
'已自动降级兼容',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Sphinx capability warning keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./sidebar/sidebarLegacyNodeMenu.tsx',
|
||||
'./sidebar/useSidebarObjectActions.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.v2_table_menu.new_rollup',
|
||||
] as const;
|
||||
|
||||
const catalogs = Object.fromEntries(locales.map(locale => [
|
||||
locale,
|
||||
JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
|
||||
])) as Record<typeof locales[number], Record<string, string>>;
|
||||
|
||||
const placeholdersOf = (value: string): string[] => (
|
||||
Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), match => match[1]).sort()
|
||||
);
|
||||
|
||||
describe('Sidebar StarRocks Rollup i18n', () => {
|
||||
it('localizes Rollup tab title and menu label while keeping SQL raw', () => {
|
||||
expect(source).not.toContain("title: '新增 Rollup'");
|
||||
expect(source).not.toContain("label: '新增 Rollup'");
|
||||
expect(source).toContain("t('sidebar.v2_table_menu.new_rollup'");
|
||||
expect(source).toContain("keyword: 'Rollup'");
|
||||
expect(source).toContain('ADD ROLLUP rollup_name (column1, column2);');
|
||||
});
|
||||
|
||||
it('keeps Rollup label key available in every locale with matching placeholders', () => {
|
||||
const zhCnCatalog = catalogs['zh-CN'];
|
||||
requiredKeys.forEach(key => {
|
||||
expect(zhCnCatalog, `zh-CN:${key}`).toHaveProperty(key);
|
||||
const expectedPlaceholders = placeholdersOf(zhCnCatalog[key]);
|
||||
locales.forEach(locale => {
|
||||
expect(catalogs[locale], `${locale}:${key}`).toHaveProperty(key);
|
||||
expect(placeholdersOf(catalogs[locale][key]), `${locale}:${key}`).toEqual(expectedPlaceholders);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const extractHandleExportBlock = (): string => {
|
||||
const start = source.indexOf('const handleExport = async');
|
||||
const end = source.indexOf('const handleCopyTableAsInsert', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar table export feedback i18n', () => {
|
||||
it('opens SQL exports in the workbench while retaining progress export for other formats', () => {
|
||||
const block = extractHandleExportBlock();
|
||||
|
||||
expect(block).not.toContain('ExportTable(');
|
||||
expect(block).toContain("if (options.format === 'sql')");
|
||||
expect(block).toContain("await openTableSQLExportWorkbench(node, 'backup')");
|
||||
expect(block).toContain('runExportWithProgress({');
|
||||
expect(block).toContain('ExportTableWithOptions(');
|
||||
expect(block).toContain('...options');
|
||||
expect(block).toContain('jobId');
|
||||
expect(block).toContain('totalRowsHint');
|
||||
expect(block).toContain('totalRowsKnown');
|
||||
});
|
||||
|
||||
it('opens table backups for review while retaining automatic INSERT exports', () => {
|
||||
expect(source).toContain("const openTableSQLExportWorkbench = async (node: any, mode: 'backup' | 'dataOnly')");
|
||||
expect(source).not.toContain('showSQLExportOptionsDialog');
|
||||
expect(source).toContain('addTab(buildBatchTableExportWorkbenchTab({');
|
||||
expect(source).toContain('initialObjectNames: [tableName]');
|
||||
expect(source).toContain('contentMode: mode');
|
||||
expect(source).toContain('includeDropIfExists: false');
|
||||
expect(source).toContain("...(mode === 'backup' ? { launchKey } : { requestKey: launchKey })");
|
||||
expect(source).toContain("await openTableSQLExportWorkbench(node, 'dataOnly')");
|
||||
});
|
||||
|
||||
it('keeps the export workbench entry key available across locales', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.v2_table_menu.open_export_workbench'], `${locale}:sidebar.v2_table_menu.open_export_workbench`).toBeTruthy();
|
||||
expect(placeholders(catalog['sidebar.v2_table_menu.open_export_workbench'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.table_folder.columns',
|
||||
'sidebar.table_folder.indexes',
|
||||
'sidebar.table_folder.foreign_keys',
|
||||
'sidebar.table_folder.triggers',
|
||||
];
|
||||
|
||||
describe('Sidebar table folder i18n', () => {
|
||||
it('localizes table child folder titles', () => {
|
||||
["title: '列'", "title: '索引'", "title: '外键'", "title: '触发器'"].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps table folder keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.tab.table_structure',
|
||||
'sidebar.tab.design_table',
|
||||
'sidebar.tab.new_table',
|
||||
'sidebar.tab.table_overview',
|
||||
];
|
||||
|
||||
describe('Sidebar table tab title i18n', () => {
|
||||
it('localizes table design and overview tab titles', () => {
|
||||
[
|
||||
"title: `${forceReadOnly ? '表结构' : '设计表'} (${tableName})`",
|
||||
'title: `新建表 - ${dbName}`',
|
||||
'title: `表概览 - ${gDbName}${schemaName ? ` (${schemaName})` : \'\'}',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(source).toContain(`t('${key}'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps table tab title keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarBatchExport.ts', import.meta.url), 'utf8');
|
||||
const workbenchSource = readFileSync(new URL('./TableExportWorkbench.tsx', import.meta.url), 'utf8');
|
||||
const runnerSource = readFileSync(new URL('./useExportProgressRunner.ts', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'sidebar.action.batch_tables',
|
||||
'data_export.message.already_running',
|
||||
'data_export.message.export_success',
|
||||
'data_export.message.export_failed',
|
||||
] as const;
|
||||
|
||||
const extractHandleExportTablesBlock = (): string => {
|
||||
const start = source.indexOf('const openBatchTableWorkbench = () =>');
|
||||
const end = source.indexOf('const openBatchDatabaseWorkbench = () =>', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
};
|
||||
|
||||
const extractBatchTableExecutionBlock = (): string => {
|
||||
const start = workbenchSource.indexOf('const handleStartBatchTablesExport = async');
|
||||
const end = workbenchSource.indexOf('const handleStartBatchDatabasesExport = async', start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return workbenchSource.slice(start, end);
|
||||
};
|
||||
|
||||
const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe('Sidebar tables export feedback i18n', () => {
|
||||
it('opens the batch workbench immediately and delegates selected tables to its runner', () => {
|
||||
const block = extractHandleExportTablesBlock();
|
||||
const executionBlock = extractBatchTableExecutionBlock();
|
||||
|
||||
expect(block).toContain('resolveBatchWorkbenchContext(selectedNodesRef.current, connections)');
|
||||
expect(block).toContain('addTab(buildBatchTableExportWorkbenchTab({');
|
||||
expect(block).toContain('connectionId,');
|
||||
expect(block).toContain('dbName: dbName || undefined');
|
||||
expect(block).toContain("title: t('sidebar.action.batch_tables')");
|
||||
expect(block).not.toContain('requestKey:');
|
||||
expect(executionBlock).toContain('selectedObjectNames.length === 0');
|
||||
expect(executionBlock).toContain('await runExportWithProgress({');
|
||||
expect(executionBlock).toContain('ExportTablesSQLWithOptions(');
|
||||
expect(executionBlock).toContain('selectedObjectNames,');
|
||||
expect(runnerSource).toContain("message.warning(t('data_export.message.already_running'))");
|
||||
expect(runnerSource).toContain("message.success(t('data_export.message.export_success'))");
|
||||
expect(runnerSource).toContain("message.error(t('data_export.message.export_failed', { error: result.message }))");
|
||||
});
|
||||
|
||||
it('keeps tables export feedback keys available with stable placeholders', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
expect(placeholders(catalog['sidebar.action.batch_tables'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.already_running'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_success'])).toEqual([]);
|
||||
expect(placeholders(catalog['data_export.message.export_failed'])).toEqual(['error']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const key = 'sidebar.tab.trigger';
|
||||
|
||||
describe('Sidebar trigger tab title i18n', () => {
|
||||
it('localizes trigger tab titles', () => {
|
||||
expect(source).not.toContain('title: `触发器: ${triggerName}`');
|
||||
expect(source).toContain(`title: t('${key}', { name: triggerName })`);
|
||||
});
|
||||
|
||||
it('keeps the trigger tab key available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
expect(catalog[key], `${locale}:${key}`).toContain('{{name}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const sidebarV2UtilsSource = readFileSync(new URL('./sidebarV2Utils.ts', import.meta.url), 'utf8');
|
||||
const sidebarHelpersSource = readFileSync(new URL('./sidebar/sidebarHelpers.ts', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const requiredKeys = [
|
||||
'connection.sidebar.group.untitled',
|
||||
'connection.sidebar.group.badge',
|
||||
];
|
||||
|
||||
describe('Sidebar v2 connection group fallback i18n', () => {
|
||||
it('localizes v2 connection group fallback names and badges', () => {
|
||||
[
|
||||
"name: tag.name || '未命名分组'",
|
||||
"fallback = '组'",
|
||||
].forEach((snippet) => {
|
||||
expect(sidebarV2UtilsSource).not.toContain(snippet);
|
||||
expect(sidebarHelpersSource).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(sidebarV2UtilsSource).toContain("tag.name || t('connection.sidebar.group.untitled')");
|
||||
expect(sidebarHelpersSource).toContain("fallback = t('connection.sidebar.group.badge')");
|
||||
});
|
||||
|
||||
it('keeps v2 connection group fallback keys available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
requiredKeys.forEach((key) => {
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = [
|
||||
'./sidebar/useSidebarObjectActions.tsx',
|
||||
'./sidebar/sidebarLegacyNodeMenu.tsx',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar view create and edit i18n', () => {
|
||||
it('localizes view create and edit tab titles and menu labels', () => {
|
||||
expect(source).not.toContain('title: `编辑视图: ${viewName}`');
|
||||
expect(source).not.toContain('title: `新建视图`');
|
||||
expect(source).not.toContain("label: '新建视图'");
|
||||
expect(source).not.toContain("label: '编辑视图'");
|
||||
expect(source).toContain("title: t('sidebar.tab.edit_view'");
|
||||
expect(source).toContain("title: t('sidebar.tab.create_view')");
|
||||
expect(source).toContain("label: t('sidebar.menu.create_view')");
|
||||
expect(source).toContain("label: t('sidebar.menu.edit_view')");
|
||||
});
|
||||
|
||||
it('keeps view create and edit catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.tab.edit_view'], `${locale}:edit view tab`).toContain('{{name}}');
|
||||
expect(catalog['sidebar.tab.create_view'], `${locale}:create view tab`).toBeTruthy();
|
||||
expect(catalog['sidebar.menu.create_view'], `${locale}:create view menu`).toBeTruthy();
|
||||
expect(catalog['sidebar.menu.edit_view'], `${locale}:edit view menu`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const key = 'sidebar.menu.view_object_definition';
|
||||
|
||||
describe('Sidebar view definition menu i18n', () => {
|
||||
it('localizes routine and event view definition menu labels', () => {
|
||||
expect(source).not.toContain("label: '查看定义'");
|
||||
expect(source.match(/label: t\('sidebar\.menu\.view_object_definition'\)/g) || []).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('keeps the generic view definition key available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog[key], `${locale}:${key}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar view definition tab i18n', () => {
|
||||
it('localizes view and materialized view definition tab titles', () => {
|
||||
expect(source).not.toContain("title: `${isMaterialized ? '物化视图' : '视图'}: ${viewName}`");
|
||||
expect(source).toContain("title: t(isMaterialized ? 'sidebar.tab.materialized_view_definition' : 'sidebar.tab.view_definition'");
|
||||
});
|
||||
|
||||
it('keeps view definition tab placeholders aligned in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.tab.view_definition'], `${locale}:view definition`).toContain('{{name}}');
|
||||
expect(catalog['sidebar.tab.materialized_view_definition'], `${locale}:materialized view definition`).toContain('{{name}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('Sidebar view menu labels i18n', () => {
|
||||
it('localizes ordinary view context menu labels', () => {
|
||||
expect(source).not.toContain("label: '浏览视图数据'");
|
||||
expect(source).not.toContain("label: '查看视图定义'");
|
||||
expect(source).not.toContain("label: '重命名视图'");
|
||||
expect(source).not.toContain("label: '删除视图'");
|
||||
expect(source).toContain("label: t('sidebar.menu.browse_view_data')");
|
||||
expect(source).toContain("label: t('sidebar.menu.view_definition')");
|
||||
expect(source).toContain("label: t('sidebar.menu.rename_view')");
|
||||
expect(source).toContain("label: t('sidebar.menu.delete_view')");
|
||||
});
|
||||
|
||||
it('keeps ordinary view context menu catalog entries available in every locale', () => {
|
||||
locales.forEach((locale) => {
|
||||
const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>;
|
||||
expect(catalog['sidebar.menu.browse_view_data'], `${locale}:browse view data`).toBeTruthy();
|
||||
expect(catalog['sidebar.menu.view_definition'], `${locale}:view definition`).toBeTruthy();
|
||||
expect(catalog['sidebar.menu.rename_view'], `${locale}:rename view`).toBeTruthy();
|
||||
expect(catalog['sidebar.menu.delete_view'], `${locale}:delete view`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const themeSource = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
|
||||
const tabManagerSource = readFileSync(new URL('./TabManager.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const readRule = (selector: string): string => {
|
||||
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = themeSource.match(new RegExp(`${escapedSelector}\\s*\\{(?<body>[^}]*)\\}`, 's'));
|
||||
expect(match, `missing CSS rule for ${selector}`).not.toBeNull();
|
||||
return match?.groups?.body ?? '';
|
||||
};
|
||||
|
||||
describe('empty workbench layout', () => {
|
||||
it('keeps the start page in a single compact content column', () => {
|
||||
const workbenchRule = readRule('body[data-ui-version="v2"] .gn-v2-empty-workbench');
|
||||
|
||||
expect(workbenchRule).toContain('display: flex;');
|
||||
expect(workbenchRule).toContain('flex-direction: column;');
|
||||
expect(workbenchRule).not.toContain('grid-template-columns');
|
||||
});
|
||||
|
||||
it('removes the oversized quick-workflow side panel', () => {
|
||||
expect(themeSource).not.toContain('gn-v2-empty-panel');
|
||||
expect(themeSource).not.toContain('gn-v2-panel-heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('workbench tab native detach drag', () => {
|
||||
it('treats a captured native pointercancel as a possible cross-window release', () => {
|
||||
expect(tabManagerSource).toContain(
|
||||
"windowTarget.addEventListener('pointercancel', recordTerminalPointer, true)",
|
||||
);
|
||||
expect(tabManagerSource).toContain('terminalPointer: session?.terminalPointer');
|
||||
expect(tabManagerSource).toContain('shouldDetachAfterNativePointerCancel(release');
|
||||
expect(tabManagerSource).toContain('const hadActiveSession = detachDragSessionRef.current !== null;');
|
||||
expect(tabManagerSource).toContain('if (hadActiveSession) {\n dispatchDndPointerCancel();');
|
||||
});
|
||||
});
|
||||
@@ -1,212 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./TableDesigner.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const readLocale = (locale: string) => JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
|
||||
describe('TableDesigner i18n', () => {
|
||||
it('localizes designer title, toolbar, tabs, modals, and schema messages', () => {
|
||||
[
|
||||
"'未命名表'",
|
||||
"'默认库'",
|
||||
'字段`',
|
||||
'确认删除触发器',
|
||||
'触发器删除成功',
|
||||
'复制选中字段到新表',
|
||||
'修改表备注',
|
||||
'新增索引',
|
||||
'修改外键',
|
||||
'确认 SQL 变更',
|
||||
'请仔细检查 SQL',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
expect(source).toContain("t('table_designer.title.untitled_table'");
|
||||
expect(source).toContain("t('table_designer.title.default_database'");
|
||||
expect(source).toContain("t('table_designer.summary.columns'");
|
||||
expect(source).toContain("t('table_designer.message.trigger_deleted'");
|
||||
expect(source).toContain("t('table_designer.modal.copy_columns_title'");
|
||||
expect(source).toContain("t('table_designer.modal.confirm_sql_title'");
|
||||
});
|
||||
|
||||
it('keeps generated trigger SQL fallbacks raw and locale-stable', () => {
|
||||
expect(source).not.toContain("'-- 无法获取完整的触发器定义'");
|
||||
expect(source).not.toContain("'-- 请输入 CREATE TRIGGER 语句'");
|
||||
expect(source).not.toContain("t('table_designer.trigger.definition_unavailable'");
|
||||
expect(source).not.toContain("t('table_designer.trigger.template.enter_create'");
|
||||
expect(source).toContain('-- Trigger logic');
|
||||
expect(source).toContain('-- Enter a CREATE TRIGGER statement');
|
||||
expect(source).toContain('-- Trigger definition unavailable');
|
||||
});
|
||||
|
||||
it('localizes trigger edit tab title and DuckDB primary key warning while keeping raw names', () => {
|
||||
[
|
||||
'修改触发器:',
|
||||
'DuckDB 当前仅支持为无主键表新增主键;已有主键的修改或删除需要通过重建表完成。',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
"t('table_designer.tab.edit_trigger_title'",
|
||||
"t('table_designer.message.duckdb_primary_key_change_unsupported'",
|
||||
].forEach((snippet) => {
|
||||
expect(source).toContain(snippet);
|
||||
});
|
||||
|
||||
['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'].forEach((locale) => {
|
||||
const messages = readLocale(locale);
|
||||
|
||||
expect(messages['table_designer.tab.edit_trigger_title']).toBeTruthy();
|
||||
expect(messages['table_designer.tab.edit_trigger_title']).toContain('{{name}}');
|
||||
expect(messages['table_designer.message.duckdb_primary_key_change_unsupported']).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes remaining V2 and StarRocks technical labels without translating raw values', () => {
|
||||
[
|
||||
'SCHEMA DESIGNER',
|
||||
'Duplicate Key',
|
||||
'Primary Key',
|
||||
'Unique Key',
|
||||
'Aggregate Key',
|
||||
'Buckets Auto',
|
||||
'placeholder="Buckets"',
|
||||
'utf8mb4 (Recommended)',
|
||||
].forEach((snippet) => {
|
||||
expect(source).not.toContain(snippet);
|
||||
});
|
||||
|
||||
[
|
||||
"t('table_designer.title.schema_designer'",
|
||||
"t('table_designer.starrocks.key_model.duplicate'",
|
||||
"t('table_designer.column.primary_key'",
|
||||
"t('table_designer.starrocks.key_model.unique'",
|
||||
"t('table_designer.starrocks.key_model.aggregate'",
|
||||
"t('table_designer.starrocks.bucket_mode.auto'",
|
||||
"t('table_designer.starrocks.placeholder.bucket_count'",
|
||||
"t('table_designer.option.recommended_suffix'",
|
||||
].forEach((snippet) => {
|
||||
expect(source).toContain(snippet);
|
||||
});
|
||||
|
||||
['utf8mb4', 'DUPLICATE', 'PRIMARY', 'UNIQUE', 'AGGREGATE', 'AUTO'].forEach((rawValue) => {
|
||||
expect(source).toContain(rawValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes the default collation label suffix while keeping the raw collation value', () => {
|
||||
expect(source).not.toContain('utf8mb4_unicode_ci (Default)');
|
||||
expect(source).toContain('utf8mb4_unicode_ci');
|
||||
expect(source).toContain("t('table_designer.option.default'");
|
||||
});
|
||||
|
||||
it('does not use English Bucket fallback for newly localized non-English bucket labels', () => {
|
||||
['zh-CN', 'zh-TW', 'ja-JP', 'de-DE', 'ru-RU'].forEach((locale) => {
|
||||
const messages = readLocale(locale);
|
||||
|
||||
[
|
||||
messages['table_designer.starrocks.bucket_mode.auto'],
|
||||
messages['table_designer.starrocks.bucket_mode.number'],
|
||||
messages['table_designer.starrocks.placeholder.bucket_count'],
|
||||
].forEach((message) => {
|
||||
expect(message).toBeTruthy();
|
||||
expect(message).not.toContain('Bucket');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes StarRocks key column placeholders in Chinese locales while keeping raw examples', () => {
|
||||
['zh-CN', 'zh-TW'].forEach((locale) => {
|
||||
const message = readLocale(locale)['table_designer.starrocks.placeholder.key_columns'];
|
||||
|
||||
expect(message).toBeTruthy();
|
||||
expect(message).not.toContain('Key');
|
||||
expect(message).toContain('id');
|
||||
expect(message).toContain('date');
|
||||
});
|
||||
});
|
||||
|
||||
it('removes English words from Chinese StarRocks distribution labels', () => {
|
||||
['zh-CN', 'zh-TW'].forEach((locale) => {
|
||||
const messages = readLocale(locale);
|
||||
|
||||
[
|
||||
messages['table_designer.starrocks.distribution.hash'],
|
||||
messages['table_designer.starrocks.distribution.random'],
|
||||
].forEach((message) => {
|
||||
expect(message).toBeTruthy();
|
||||
expect(message).not.toContain('Hash');
|
||||
expect(message).not.toContain('Random');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('removes English StarRocks distribution words from Japanese and Russian labels', () => {
|
||||
[
|
||||
{
|
||||
locale: 'ja-JP',
|
||||
key: 'table_designer.starrocks.distribution.hash',
|
||||
forbidden: 'Hash',
|
||||
},
|
||||
{
|
||||
locale: 'ja-JP',
|
||||
key: 'table_designer.starrocks.distribution.random',
|
||||
forbidden: 'Random',
|
||||
},
|
||||
{
|
||||
locale: 'ru-RU',
|
||||
key: 'table_designer.starrocks.distribution.hash',
|
||||
forbidden: 'Hash',
|
||||
},
|
||||
{
|
||||
locale: 'ru-RU',
|
||||
key: 'table_designer.starrocks.distribution.random',
|
||||
forbidden: 'Random',
|
||||
},
|
||||
].forEach(({ locale, key, forbidden }) => {
|
||||
const message = readLocale(locale)[key];
|
||||
|
||||
expect(message).toBeTruthy();
|
||||
expect(message).not.toContain(forbidden);
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes StarRocks key model and primary key labels for key locales without English fallback words', () => {
|
||||
const expectationEntries = [
|
||||
{
|
||||
key: 'table_designer.starrocks.key_model.duplicate',
|
||||
forbidden: ['Duplicate', 'Key'],
|
||||
},
|
||||
{
|
||||
key: 'table_designer.starrocks.key_model.unique',
|
||||
forbidden: ['Unique', 'Key'],
|
||||
},
|
||||
{
|
||||
key: 'table_designer.starrocks.key_model.aggregate',
|
||||
forbidden: ['Aggregate', 'Key'],
|
||||
},
|
||||
{
|
||||
key: 'table_designer.column.primary_key',
|
||||
forbidden: ['Primary', 'Key'],
|
||||
},
|
||||
];
|
||||
|
||||
['zh-CN', 'zh-TW', 'ja-JP', 'de-DE', 'ru-RU'].forEach((locale) => {
|
||||
const messages = readLocale(locale);
|
||||
|
||||
expectationEntries.forEach(({ key, forbidden }) => {
|
||||
const message = messages[key];
|
||||
|
||||
expect(message).toBeTruthy();
|
||||
forbidden.forEach((word) => {
|
||||
expect(message).not.toContain(word);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./TableDesigner.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('TableDesigner metadata loading', () => {
|
||||
it('renders columns before slower auxiliary metadata requests complete', () => {
|
||||
expect(source).toContain('const [columnsLoading, setColumnsLoading] = useState(false);');
|
||||
expect(source).toContain('const [ddlLoading, setDdlLoading] = useState(false);');
|
||||
expect(source).toContain('const loadColumns = DBGetColumns(rpcConfig, dbName, tableName)');
|
||||
expect(source).toContain('await loadColumns;');
|
||||
expect(source).toContain('await Promise.allSettled([loadIndexes, loadForeignKeys, loadTriggers, loadDdl]);');
|
||||
expect(source).toContain('loading={columnsLoading}');
|
||||
expect(source).toContain('loading={indexesLoading}');
|
||||
expect(source).toContain('loading={foreignKeysLoading}');
|
||||
expect(source).toContain('loading={triggersLoading}');
|
||||
expect(source).not.toContain('const results = await Promise.all(promises);');
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const tableDesignerSource = readFileSync(
|
||||
new URL('./TableDesigner.tsx', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const getFunctionBlock = (source: string, name: string): string => {
|
||||
const start = source.indexOf(`const ${name} = () => {`);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
const nextFunction = source.indexOf('\n const ', start + 1);
|
||||
expect(nextFunction).toBeGreaterThan(start);
|
||||
return source.slice(start, nextFunction);
|
||||
};
|
||||
|
||||
describe('TableDesigner trigger edit entry', () => {
|
||||
it('opens trigger edits in an object-edit query tab instead of the fixed modal', () => {
|
||||
const editBlock = getFunctionBlock(tableDesignerSource, 'handleEditTrigger');
|
||||
|
||||
expect(editBlock).toContain('setActiveContext({ connectionId: tab.connectionId, dbName });');
|
||||
expect(editBlock).toContain('addTab({');
|
||||
expect(editBlock).toContain("type: 'query'");
|
||||
expect(editBlock).toContain("queryMode: 'object-edit'");
|
||||
expect(editBlock).toContain('buildEditableTriggerSql(selectedTrigger.name, createSql');
|
||||
expect(editBlock).toContain('dropSql: buildDropTriggerSql(selectedTrigger.name)');
|
||||
expect(editBlock).not.toContain('setIsTriggerEditModalOpen(true)');
|
||||
});
|
||||
|
||||
it('keeps trigger creation on the existing modal path', () => {
|
||||
const createBlock = getFunctionBlock(tableDesignerSource, 'handleCreateTrigger');
|
||||
|
||||
expect(createBlock).toContain("setTriggerEditMode('create')");
|
||||
expect(createBlock).toContain('setTriggerEditSql(generateTriggerTemplate())');
|
||||
expect(createBlock).toContain('setIsTriggerEditModalOpen(true)');
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('TableOverview v2 context menu', () => {
|
||||
it('renders card and list table context menus through a measured portal', () => {
|
||||
const source = readFileSync(new URL('./TableOverview.tsx', import.meta.url), 'utf8');
|
||||
const cardSource = source.slice(
|
||||
source.indexOf('const renderCardTableContent = (table: TableStatRow) => ('),
|
||||
source.indexOf('const renderListTable = (table: TableStatRow) => {'),
|
||||
);
|
||||
const listSource = source.slice(
|
||||
source.indexOf('const renderListTable = (table: TableStatRow) => {'),
|
||||
source.indexOf('if (loading) {'),
|
||||
);
|
||||
|
||||
expect(source).toContain("import { createPortal } from 'react-dom';");
|
||||
expect(source).toContain('resolveOverviewContextMenuPosition(event.clientX, event.clientY)');
|
||||
expect(source).toContain('v2ContextMenuPortalRef');
|
||||
expect(source).toContain('content?.scrollHeight');
|
||||
expect(source).toContain('gn-v2-table-overview-context-menu-portal');
|
||||
expect(source).toContain("['--gn-v2-context-menu-max-height' as any]");
|
||||
expect(source).toContain('renderV2OverviewTableContextMenu(v2ContextMenuTable)');
|
||||
expect(cardSource).toContain('onContextMenu={isV2Ui ? (event) => openV2OverviewContextMenu(event, table) : undefined}');
|
||||
expect(listSource).toContain('onContextMenu={isV2Ui ? (event) => openV2OverviewContextMenu(event, table) : undefined}');
|
||||
expect(cardSource).not.toContain('popupRender');
|
||||
expect(listSource).not.toContain('popupRender');
|
||||
});
|
||||
|
||||
it('opens table backups for review while retaining automatic INSERT exports', () => {
|
||||
const source = readFileSync(new URL('./TableOverview.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toContain('buildBatchTableExportWorkbenchTab({');
|
||||
expect(source).not.toContain('showSQLExportOptionsDialog');
|
||||
expect(source).toContain('initialObjectNames: [normalizedTableName]');
|
||||
expect(source).toContain('contentMode: mode');
|
||||
expect(source).toContain('includeDropIfExists: false');
|
||||
expect(source).toContain("...(mode === 'backup' ? { launchKey } : { requestKey: launchKey })");
|
||||
expect(source).toContain("await openTableSQLExportWorkbench(tableName, 'dataOnly')");
|
||||
expect(source).toContain("void openTableSQLExportWorkbench(tableName, 'backup')");
|
||||
expect(source).toContain("onClick: () => openTableSQLExportWorkbench(table.name, 'backup')");
|
||||
expect(source).not.toContain('ExportTableWithOptions');
|
||||
expect(source).not.toContain('useExportProgressDialog');
|
||||
expect(source).not.toContain('{exportProgressModal}');
|
||||
});
|
||||
});
|
||||
@@ -1,345 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./TableOverview.tsx', import.meta.url), 'utf8');
|
||||
const catalogFiles = [
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'en-US',
|
||||
'ja-JP',
|
||||
'de-DE',
|
||||
'ru-RU',
|
||||
] as const;
|
||||
const catalogs = Object.fromEntries(catalogFiles.map(language => [
|
||||
language,
|
||||
JSON.parse(readFileSync(new URL(`../../../shared/i18n/${language}.json`, import.meta.url), 'utf8')) as Record<string, string>,
|
||||
])) as Record<typeof catalogFiles[number], Record<string, string>>;
|
||||
|
||||
const placeholdersOf = (value: string): string[] => (
|
||||
Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), match => match[1]).sort()
|
||||
);
|
||||
|
||||
const cardSource = source.slice(
|
||||
source.indexOf('const renderCardTableContent = (table: TableStatRow) => ('),
|
||||
source.indexOf('const renderCardTable = (table: TableStatRow) => {'),
|
||||
);
|
||||
|
||||
const listSource = source.slice(
|
||||
source.indexOf('const renderListTable = (table: TableStatRow) => {'),
|
||||
source.indexOf('const renderCompactSortHeader = ('),
|
||||
);
|
||||
|
||||
const compactTableSource = source.slice(
|
||||
source.indexOf('const renderCompactSortHeader = ('),
|
||||
source.indexOf('if (loading) {'),
|
||||
);
|
||||
|
||||
const visibleTableSectionsSource = source.slice(
|
||||
source.indexOf('const visibleTableSections = useMemo<OverviewTableSection[]>(() => {'),
|
||||
source.indexOf('const v2ContextMenuTable = useMemo('),
|
||||
);
|
||||
const normalizedVisibleTableSectionsSource = visibleTableSectionsSource.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const renderOverviewSectionTitleSource = source.slice(
|
||||
source.indexOf('const renderOverviewSectionTitle = (section: OverviewTableSection) => {'),
|
||||
source.indexOf('const renderTableOverviewMetaBadges = useCallback((table: TableStatRow, compact = false) => {'),
|
||||
);
|
||||
const metaBadgesSource = source.slice(
|
||||
source.indexOf('const renderTableOverviewMetaBadges = useCallback((table: TableStatRow, compact = false) => {'),
|
||||
source.indexOf('const renderCardTableContent = (table: TableStatRow) => ('),
|
||||
);
|
||||
const normalizedRenderOverviewSectionTitleSource = renderOverviewSectionTitleSource.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const toggleOverviewTablePinnedSource = source.slice(
|
||||
source.indexOf('const toggleOverviewTablePinned = useCallback((tableName: string, pinned?: boolean) => {'),
|
||||
source.indexOf('const handleRenameTable = useCallback((tableName: string) => {'),
|
||||
);
|
||||
const normalizedToggleOverviewTablePinnedSource = toggleOverviewTablePinnedSource.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const tableOperationSource = source.slice(
|
||||
source.indexOf('const loadData = useCallback(async () => {'),
|
||||
source.indexOf('const buildMenuItems = useMemo<MenuProps'),
|
||||
);
|
||||
const normalizedTableOperationSource = tableOperationSource.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const aiPromptSource = source.slice(
|
||||
source.indexOf("const injectTablePromptToAI = useCallback(async (tableName: string, promptKind: 'explain' | 'query') => {"),
|
||||
source.indexOf(' // --- Theme ---'),
|
||||
);
|
||||
|
||||
const requiredTableOperationKeys = [
|
||||
'table_overview.metric.created_at',
|
||||
'table_overview.metric.comment',
|
||||
'table_overview.metric.updated_at',
|
||||
'table_overview.tooltip.table_view',
|
||||
'table_overview.tab.design_table_title',
|
||||
'table_overview.tab.table_structure_title',
|
||||
'table_overview.message.load_tables_failed',
|
||||
'table_overview.message.unknown_error',
|
||||
'table_overview.message.copy_structure_success',
|
||||
'table_overview.message.copy_table_name_empty',
|
||||
'table_overview.message.copy_table_name_success',
|
||||
'table_overview.message.copy_table_name_failed',
|
||||
'table_overview.message.exporting_table_format',
|
||||
'table_overview.message.export_success',
|
||||
'table_overview.message.export_failed',
|
||||
'table_overview.message.delete_table_success',
|
||||
'table_overview.message.delete_table_failed',
|
||||
'table_overview.message.table_data_action_loading',
|
||||
'table_overview.message.table_data_action_success',
|
||||
'table_overview.message.table_data_action_failed',
|
||||
'table_overview.message.rename_table_success',
|
||||
'table_overview.message.rename_table_failed',
|
||||
'table_overview.modal.delete_table.title',
|
||||
'table_overview.modal.delete_table.content',
|
||||
'table_overview.modal.table_data_action.title',
|
||||
'table_overview.modal.table_data_action.content',
|
||||
'table_overview.modal.rename_table.title',
|
||||
'table_overview.modal.rename_table.placeholder',
|
||||
'table_overview.validation.table_name_required',
|
||||
'table_overview.validation.table_name_unchanged',
|
||||
'table_overview.menu.new_query',
|
||||
'table_overview.menu.design_table',
|
||||
'table_overview.menu.table_structure',
|
||||
'table_overview.menu.copy_table_name',
|
||||
'table_overview.menu.copy_structure',
|
||||
'table_overview.menu.backup_table_sql',
|
||||
'table_overview.menu.rename_table',
|
||||
'table_overview.menu.danger_operations',
|
||||
'table_overview.menu.truncate_table',
|
||||
'table_overview.menu.clear_table',
|
||||
'table_overview.menu.delete_table',
|
||||
'table_overview.menu.export_table_data',
|
||||
'table_overview.menu.export_csv',
|
||||
'table_overview.menu.export_xlsx',
|
||||
'table_overview.menu.export_json',
|
||||
'table_overview.menu.export_markdown',
|
||||
'table_overview.menu.export_html',
|
||||
] as const;
|
||||
|
||||
const requiredAIPromptKeys = [
|
||||
'sidebar.message.ai_table_context_missing',
|
||||
'sidebar.ai_prompt.explain.intro',
|
||||
'sidebar.ai_prompt.explain.detail',
|
||||
'sidebar.ai_prompt.query.intro',
|
||||
'sidebar.ai_prompt.query.detail',
|
||||
] as const;
|
||||
|
||||
const requiredRollupKeys = [
|
||||
'sidebar.v2_table_menu.new_rollup',
|
||||
] as const;
|
||||
|
||||
describe('TableOverview i18n', () => {
|
||||
it('localizes the selected card and list overview copy with existing table_overview keys', () => {
|
||||
expect(cardSource).not.toContain('title="行数"');
|
||||
expect(cardSource).not.toContain('title="数据大小"');
|
||||
expect(cardSource).not.toContain('title="引擎"');
|
||||
expect(cardSource).not.toContain('最近修改');
|
||||
expect(cardSource).not.toContain('创建时间');
|
||||
expect(cardSource).toContain("title={t('table_overview.sort.rows')}");
|
||||
expect(cardSource).toContain("title={t('table_overview.metric.data_size')}");
|
||||
expect(cardSource).toContain("title={t('table_overview.metric.engine')}");
|
||||
expect(cardSource).toContain('{renderTableOverviewMetaBadges(table)}');
|
||||
expect(metaBadgesSource).toContain("t('table_overview.metric.updated_at')");
|
||||
expect(metaBadgesSource).toContain("t('table_overview.metric.created_at')");
|
||||
|
||||
expect(listSource).not.toContain('`${table.engine} 表`');
|
||||
expect(listSource).not.toContain("'双击打开数据,右键查看更多操作'");
|
||||
expect(listSource).not.toContain('最近修改');
|
||||
expect(listSource).not.toContain('创建时间');
|
||||
expect(listSource).not.toContain("<div style={{ color: textMuted }}>行数</div>");
|
||||
expect(listSource).not.toContain("<div style={{ color: textMuted }}>数据大小</div>");
|
||||
expect(listSource).not.toContain("<div style={{ color: textMuted }}>索引大小</div>");
|
||||
expect(listSource).not.toContain("<div style={{ color: textMuted }}>相对大小</div>");
|
||||
expect(listSource).toContain("t('table_overview.row.engine_table', { engine: table.engine })");
|
||||
expect(listSource).toContain("t('table_overview.row.open_hint')");
|
||||
expect(listSource).toContain('{renderTableOverviewMetaBadges(table, true)}');
|
||||
expect(listSource).toContain("t('table_overview.sort.rows')");
|
||||
expect(listSource).toContain("t('table_overview.metric.data_size')");
|
||||
expect(listSource).toContain("t('table_overview.metric.index_size')");
|
||||
expect(listSource).toContain("t('table_overview.metric.relative_size')");
|
||||
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('name', 'table_overview.sort.name')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('comment', 'table_overview.metric.comment')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('rows', 'table_overview.sort.rows', 'right')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('dataSize', 'table_overview.metric.data_size', 'right')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('indexSize', 'table_overview.metric.index_size', 'right')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('engine', 'table_overview.metric.engine')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('updateTime', 'table_overview.metric.updated_at')");
|
||||
expect(compactTableSource).toContain("renderCompactSortHeader('createTime', 'table_overview.metric.created_at')");
|
||||
});
|
||||
|
||||
it('localizes section titles for pinned and all groups with dedicated table_overview keys', () => {
|
||||
expect(visibleTableSectionsSource).not.toContain("title: '全部'");
|
||||
expect(visibleTableSectionsSource).not.toContain("title: '置顶'");
|
||||
expect(visibleTableSectionsSource).not.toContain("t('table_overview.section.all')");
|
||||
expect(visibleTableSectionsSource).not.toContain("t('table_overview.section.pinned')");
|
||||
expect(visibleTableSectionsSource).not.toContain('title:');
|
||||
expect(normalizedVisibleTableSectionsSource).toContain("return [{ key: 'all', kind: 'all', rows: visibleTables }];");
|
||||
expect(normalizedVisibleTableSectionsSource).toContain(
|
||||
"{ key: 'pinned', kind: 'pinned' as const, rows: pinnedRows }",
|
||||
);
|
||||
expect(normalizedVisibleTableSectionsSource).toContain(
|
||||
"{ key: 'all', kind: 'all' as const, rows: regularRows }",
|
||||
);
|
||||
|
||||
expect(renderOverviewSectionTitleSource).not.toContain("title: '全部'");
|
||||
expect(renderOverviewSectionTitleSource).not.toContain("title: '置顶'");
|
||||
expect(renderOverviewSectionTitleSource).not.toContain('<span>{section.title}</span>');
|
||||
expect(renderOverviewSectionTitleSource).toContain("t('table_overview.section.all')");
|
||||
expect(renderOverviewSectionTitleSource).toContain("t('table_overview.section.pinned')");
|
||||
expect(normalizedRenderOverviewSectionTitleSource).toContain(
|
||||
"const sectionTitle = section.kind === 'pinned' ? t('table_overview.section.pinned') : t('table_overview.section.all');",
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes toggleOverviewTablePinned success toast without raw pinned copy', () => {
|
||||
expect(toggleOverviewTablePinnedSource).not.toContain("'已置顶表'");
|
||||
expect(toggleOverviewTablePinnedSource).not.toContain("'已取消置顶'");
|
||||
expect(normalizedToggleOverviewTablePinnedSource).toContain(
|
||||
"message.success(shouldPin ? t('table_overview.message.pinned') : t('table_overview.message.unpinned'));",
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes table operation tabs, messages, modals and legacy menu labels', () => {
|
||||
[
|
||||
'获取表信息失败: ',
|
||||
'未知错误',
|
||||
'表结构',
|
||||
'设计表',
|
||||
'新建查询',
|
||||
'表结构已复制到剪贴板',
|
||||
'表名为空,无法复制',
|
||||
'表名已复制到剪贴板',
|
||||
'复制表名失败: ',
|
||||
'正在导出 ',
|
||||
'导出成功',
|
||||
'导出失败: ',
|
||||
'确认删除表',
|
||||
'确定删除表',
|
||||
'表删除成功',
|
||||
'删除失败: ',
|
||||
'确认${label}',
|
||||
'操作不可逆',
|
||||
'继续',
|
||||
'正在${progressLabel}',
|
||||
'${progressLabel}成功',
|
||||
'${progressLabel}失败',
|
||||
'重命名表',
|
||||
'输入新表名',
|
||||
'表名不能为空',
|
||||
'新旧表名相同',
|
||||
'表重命名成功',
|
||||
'重命名失败: ',
|
||||
'复制表名',
|
||||
'复制表结构',
|
||||
'备份表 (SQL)',
|
||||
'危险操作',
|
||||
'截断表',
|
||||
'清空表',
|
||||
'删除表',
|
||||
'导出表数据',
|
||||
'导出 CSV',
|
||||
'导出 Excel (XLSX)',
|
||||
'导出 JSON',
|
||||
'导出 Markdown',
|
||||
'导出 HTML',
|
||||
].forEach(text => {
|
||||
expect(tableOperationSource).not.toContain(text);
|
||||
});
|
||||
|
||||
[
|
||||
'table_overview.tab.design_table_title',
|
||||
'table_overview.tab.table_structure_title',
|
||||
"t('table_overview.message.copy_structure_failed'",
|
||||
"t('table_overview.message.copy_table_name_empty')",
|
||||
"t('table_overview.message.copy_table_name_success')",
|
||||
"t('table_overview.message.copy_table_name_failed'",
|
||||
"t('table_overview.modal.delete_table.title')",
|
||||
"t('table_overview.modal.delete_table.content'",
|
||||
"t('table_overview.modal.table_data_action.title'",
|
||||
"t('table_overview.modal.table_data_action.content'",
|
||||
"okText: t('common.continue')",
|
||||
"cancelText: t('common.cancel')",
|
||||
"t('table_overview.modal.rename_table.title')",
|
||||
"t('table_overview.modal.rename_table.placeholder')",
|
||||
"t('table_overview.validation.table_name_required')",
|
||||
"t('table_overview.validation.table_name_unchanged')",
|
||||
"t('table_overview.message.rename_table_success')",
|
||||
"t('table_overview.message.rename_table_failed'",
|
||||
"t('table_overview.menu.copy_table_name')",
|
||||
"t('table_overview.menu.table_structure')",
|
||||
].forEach(text => {
|
||||
expect(tableOperationSource).toContain(text);
|
||||
});
|
||||
|
||||
expect(tableOperationSource).not.toContain('message.error(res.message);');
|
||||
expect(normalizedTableOperationSource).toContain(
|
||||
"detail: res.message || t('table_overview.message.unknown_error')",
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps table operation catalog keys in all supported languages with matching placeholders', () => {
|
||||
const zhCnCatalog = catalogs['zh-CN'];
|
||||
requiredTableOperationKeys.forEach(key => {
|
||||
expect(zhCnCatalog, `zh-CN:${key}`).toHaveProperty(key);
|
||||
const expectedPlaceholders = placeholdersOf(zhCnCatalog[key]);
|
||||
catalogFiles.forEach(language => {
|
||||
expect(catalogs[language], `${language}:${key}`).toHaveProperty(key);
|
||||
expect(placeholdersOf(catalogs[language][key]), `${language}:${key}`).toEqual(expectedPlaceholders);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes AI table prompt shells while keeping table references and DDL raw', () => {
|
||||
[
|
||||
'当前表缺少连接上下文,无法发送给 AI',
|
||||
'请解释数据表 ${dbName}.${tableName} 的结构和业务含义。',
|
||||
'重点说明字段含义、主键/索引、潜在关联关系、典型查询场景和风险点。',
|
||||
'请基于数据表 ${dbName}.${tableName} 生成 3 条常用查询 SQL。',
|
||||
'要求包含:数据预览查询、按关键字段过滤查询、一个聚合或统计查询。',
|
||||
].forEach(text => {
|
||||
expect(aiPromptSource).not.toContain(text);
|
||||
});
|
||||
|
||||
requiredAIPromptKeys.forEach(key => {
|
||||
expect(aiPromptSource).toContain(`t('${key}'`);
|
||||
});
|
||||
|
||||
expect(aiPromptSource).toContain('DBShowCreateTable');
|
||||
expect(aiPromptSource).toContain('const tableRef = `${dbName}.${tableName}`;');
|
||||
expect(aiPromptSource).toContain('ddl ? `\\n\\`\\`\\`sql');
|
||||
expect(aiPromptSource).toContain('${ddl}');
|
||||
});
|
||||
|
||||
it('keeps reused AI prompt keys in all supported languages with matching placeholders', () => {
|
||||
const zhCnCatalog = catalogs['zh-CN'];
|
||||
requiredAIPromptKeys.forEach(key => {
|
||||
expect(zhCnCatalog, `zh-CN:${key}`).toHaveProperty(key);
|
||||
const expectedPlaceholders = placeholdersOf(zhCnCatalog[key]);
|
||||
catalogFiles.forEach(language => {
|
||||
expect(catalogs[language], `${language}:${key}`).toHaveProperty(key);
|
||||
expect(placeholdersOf(catalogs[language][key]), `${language}:${key}`).toEqual(expectedPlaceholders);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes StarRocks Rollup entry labels while keeping Rollup SQL raw', () => {
|
||||
expect(tableOperationSource).not.toContain("title: '新增 Rollup'");
|
||||
expect(tableOperationSource).toContain("t('sidebar.v2_table_menu.new_rollup'");
|
||||
expect(tableOperationSource).toContain("keyword: 'Rollup'");
|
||||
expect(tableOperationSource).toContain('ADD ROLLUP rollup_name (column1, column2);');
|
||||
});
|
||||
|
||||
it('keeps reused StarRocks Rollup key in all supported languages with matching placeholders', () => {
|
||||
const zhCnCatalog = catalogs['zh-CN'];
|
||||
requiredRollupKeys.forEach(key => {
|
||||
expect(zhCnCatalog, `zh-CN:${key}`).toHaveProperty(key);
|
||||
const expectedPlaceholders = placeholdersOf(zhCnCatalog[key]);
|
||||
catalogFiles.forEach(language => {
|
||||
expect(catalogs[language], `${language}:${key}`).toHaveProperty(key);
|
||||
expect(placeholdersOf(catalogs[language][key]), `${language}:${key}`).toEqual(expectedPlaceholders);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const css = readFileSync(new URL('../styles/v2-theme-workbench.css', import.meta.url), 'utf8');
|
||||
|
||||
describe('TableOverview compact table styles', () => {
|
||||
it('keeps the toolbar compact when the overview content is narrow', () => {
|
||||
expect(css).toContain('container-name: gn-table-overview;');
|
||||
expect(css).toMatch(
|
||||
/@container gn-table-overview \(max-width: 700px\)\s*\{[^}]*\.gn-table-overview-header\s*\{[^}]*gap:\s*8px !important;/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@container gn-table-overview \(max-width: 700px\)[\s\S]*\.gn-table-overview-summary\s*\{[^}]*display:\s*none;/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the legacy compact table readable in dark mode', () => {
|
||||
expect(css).toMatch(
|
||||
/body\[data-ui-version="legacy"\]\[data-theme="dark"\] \.gn-table-overview-compact-scroll\s*\{[^}]*background:\s*#141414;/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/body\[data-ui-version="legacy"\]\[data-theme="dark"\] \.gn-table-overview-compact-header\s*\{[^}]*background:\s*#1f1f1f;[^}]*color:\s*rgba\(255, 255, 255, 0\.65\);/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/body\[data-ui-version="legacy"\]\[data-theme="dark"\] \.gn-table-overview-compact-section\s*\{[^}]*background:\s*#1f1f1f;/s,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./WebAuthSettingsPanel.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'];
|
||||
|
||||
describe('WebAuthSettingsPanel environment-managed password', () => {
|
||||
it('disables password changes and explains how to update the password', () => {
|
||||
expect(source).toContain('passwordManagedByEnvironment: boolean;');
|
||||
expect(source).toContain('summary?.passwordManagedByEnvironment === true');
|
||||
expect(source).toContain("t('app.settings.web_auth.password.managed_by_environment')");
|
||||
expect(source).toContain('disabled={passwordManagedByEnvironment}');
|
||||
});
|
||||
|
||||
it('keeps the six-character and environment-managed copy in every locale', () => {
|
||||
for (const locale of locales) {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
expect(catalog['app.settings.web_auth.password.new_placeholder']).toContain('6');
|
||||
expect(catalog['app.settings.web_auth.password.managed_by_environment']).toContain('GONAVI_WEB_PASSWORD');
|
||||
expect(catalog['web_auth.error.password_managed_by_environment']).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const headerSource = readFileSync(new URL('./AIChatHeader.tsx', import.meta.url), 'utf8');
|
||||
|
||||
describe('AIChatHeader export affordance', () => {
|
||||
it('keeps chat export UI and markdown export implementation wired', () => {
|
||||
expect(headerSource).toContain('exportToMarkdown');
|
||||
expect(headerSource).toContain('gn-v2-ai-export-button');
|
||||
expect(headerSource).toContain("t('ai_chat.header.action.export')");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
const catalogs = Object.fromEntries(locales.map((locale) => [
|
||||
locale,
|
||||
JSON.parse(readFileSync(new URL(`../../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
|
||||
])) as Record<typeof locales[number], Record<string, string>>;
|
||||
|
||||
const placeholdersOf = (value: string): string[] => (
|
||||
Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), (match) => match[1]).sort()
|
||||
);
|
||||
|
||||
describe('SQL audit workbench i18n', () => {
|
||||
it('keeps every SQL audit key and placeholder set aligned in all six catalogs', () => {
|
||||
const keys = Object.keys(catalogs['en-US']).filter((key) => (
|
||||
key.startsWith('sql_audit.')
|
||||
|| key === 'app.tools.entry.sql_audit.title'
|
||||
|| key === 'app.tools.entry.sql_audit.description'
|
||||
|| key === 'tab_manager.kind_badge.sql_audit'
|
||||
|| key === 'tab_manager.hover.kind.sql_audit'
|
||||
));
|
||||
|
||||
expect(keys.length).toBeGreaterThan(100);
|
||||
keys.forEach((key) => {
|
||||
const expectedPlaceholders = placeholdersOf(catalogs['en-US'][key]);
|
||||
locales.forEach((locale) => {
|
||||
expect(catalogs[locale][key], `${locale}:${key}`).toBeTruthy();
|
||||
expect(placeholdersOf(catalogs[locale][key]), `${locale}:${key}`).toEqual(expectedPlaceholders);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('states the redacted/metadata-only privacy boundary in every language', () => {
|
||||
locales.forEach((locale) => {
|
||||
expect(catalogs[locale]['sql_audit.privacy.description']).toBeTruthy();
|
||||
expect(catalogs[locale]['sql_audit.settings.capture_mode.redacted']).toBeTruthy();
|
||||
expect(catalogs[locale]['sql_audit.settings.capture_mode.metadata']).toBeTruthy();
|
||||
expect(catalogs[locale]).not.toHaveProperty('sql_audit.settings.capture_mode.raw');
|
||||
expect(catalogs[locale]).not.toHaveProperty('sql_audit.settings.capture_mode.full');
|
||||
});
|
||||
});
|
||||
|
||||
it('labels writer gaps separately from tamper-proof integrity claims', () => {
|
||||
locales.forEach((locale) => {
|
||||
expect(catalogs[locale]['sql_audit.event_type.query_statement']).toBeTruthy();
|
||||
expect(catalogs[locale]['sql_audit.event_type.audit_gap']).toBeTruthy();
|
||||
expect(catalogs[locale]['sql_audit.health.degraded.description']).toContain('{{count}}');
|
||||
expect(catalogs[locale]['sql_audit.health.recovered.description']).toContain('audit_gap');
|
||||
expect(catalogs[locale]['sql_audit.health.disabled.title']).toBeTruthy();
|
||||
expect(catalogs[locale]['sql_audit.health.disabled.description']).toBeTruthy();
|
||||
expect(catalogs[locale]['sql_audit.health.capture_mode']).toBeTruthy();
|
||||
});
|
||||
expect(catalogs['en-US']['sql_audit.health.healthy.description']).toContain('not a tamper-proof guarantee');
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), 'utf8');
|
||||
const occurrences = (source: string, value: string): number => source.split(value).length - 1;
|
||||
|
||||
describe('application user-action SQL audit coverage', () => {
|
||||
it('audits explicit TableDesigner writes with one stable source', () => {
|
||||
const source = read('../TableDesigner.tsx');
|
||||
|
||||
expect(source).toContain('DBQueryAudited');
|
||||
expect(occurrences(source, 'DBQueryAudited(')).toBe(5);
|
||||
expect(occurrences(source, "'table_designer'")).toBe(5);
|
||||
expect(source).not.toContain('DBQuery(');
|
||||
});
|
||||
|
||||
it('audits explicit message publication', () => {
|
||||
const source = read('../MessagePublishModal.tsx');
|
||||
|
||||
expect(source).toContain('DBQueryAudited(');
|
||||
expect(source).toContain("'message_publish'");
|
||||
expect(source).not.toContain('DBQuery(');
|
||||
});
|
||||
|
||||
it('routes AI database probes through the fixed-source backend method', () => {
|
||||
const runtimeSource = read('../ai/aiLocalToolRuntime.ts');
|
||||
const codeBlockSource = read('../ai/messageBubble/AIMessageCodeBlock.tsx');
|
||||
|
||||
expect(runtimeSource).toContain('mod.DBQueryAI(config, dbName, sql)');
|
||||
expect(codeBlockSource).toContain('DBQueryAI(activeConnectionConfig');
|
||||
expect(runtimeSource).not.toContain('mod.DBQuery(config, dbName, sql)');
|
||||
});
|
||||
|
||||
it('keeps metadata, counts, browsing, and definition reads outside application audit', () => {
|
||||
const readOnlySources = [
|
||||
read('../DataViewer.tsx'),
|
||||
read('../DefinitionViewer.tsx'),
|
||||
read('../TriggerViewer.tsx'),
|
||||
read('../TableOverview.tsx'),
|
||||
read('../sidebar/sidebarMetadataLoaders.ts'),
|
||||
read('../sidebar/useSidebarTreeLoaders.tsx'),
|
||||
read('../sidebar/useSidebarV2ContextMenu.tsx'),
|
||||
];
|
||||
|
||||
readOnlySources.forEach((source) => {
|
||||
expect(source).toContain('DBQuery');
|
||||
expect(source).not.toContain('DBQueryAudited');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const readComponent = (path: string): string => readFileSync(new URL(path, import.meta.url), 'utf8');
|
||||
|
||||
describe('close-tab shortcut portal guards', () => {
|
||||
it.each([
|
||||
'./DataGridLegacyCellContextMenu.tsx',
|
||||
'./DataGridShell.tsx',
|
||||
'./Sidebar.tsx',
|
||||
'./TableOverview.tsx',
|
||||
'./RedisViewer.tsx',
|
||||
])('blocks background close commands while the interactive portal is visible: %s', (path) => {
|
||||
const source = readComponent(path);
|
||||
expect(source).toContain('data-gonavi-close-shortcut-guard="true"');
|
||||
expect(source).toContain('data-gonavi-close-shortcut-blocks-background="true"');
|
||||
});
|
||||
|
||||
it.each([
|
||||
'./FloatingQueryResultWindows.tsx',
|
||||
'./FloatingWorkbenchWindows.tsx',
|
||||
'./FloatingAIChatWindow.tsx',
|
||||
'./resultDiff/ResultDiffPanel.tsx',
|
||||
])('blocks routing after explicit detached-window interaction without global blocking: %s', (path) => {
|
||||
const source = readComponent(path);
|
||||
expect(source).toContain('data-gonavi-close-shortcut-guard="true"');
|
||||
expect(source).toContain('data-gonavi-close-shortcut-scope="blocked"');
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const modalSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./ResizableDraggableModal.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const modalCss = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./ResizableDraggableModal.css', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('ResizableDraggableModal guards', () => {
|
||||
it('routes component, static, and hook modals through the same draggable frame', () => {
|
||||
expect(modalSource).toContain('const DraggableResizableModalFrame: React.FC<DraggableResizableModalFrameProps>');
|
||||
expect(modalSource).toContain('<DraggableResizableModalFrame');
|
||||
expect(modalSource).toContain('const withDraggableModalRender = (config: ModalFuncProps): ModalFuncProps =>');
|
||||
expect(modalSource).toContain('ResizableDraggableModal.info = wrapModalFunc(AntdModal.info);');
|
||||
expect(modalSource).toContain('ResizableDraggableModal.success = wrapModalFunc(AntdModal.success);');
|
||||
expect(modalSource).toContain('ResizableDraggableModal.error = wrapModalFunc(AntdModal.error);');
|
||||
expect(modalSource).toContain('ResizableDraggableModal.warning = wrapModalFunc(AntdModal.warning);');
|
||||
expect(modalSource).toContain('ResizableDraggableModal.confirm = wrapModalFunc(AntdModal.confirm);');
|
||||
expect(modalSource).toContain('return [wrapHookModalApi(modalApi), contextHolder] as ReturnType<typeof AntdModal.useModal>;');
|
||||
expect(modalSource).toContain("const activeInteractionRef = useRef<'drag' | 'resize' | null>(null);");
|
||||
expect(modalSource).toContain('const [wrapperElement, setWrapperElement] = useState<HTMLDivElement | null>(null);');
|
||||
expect(modalSource).toContain('const bindWrapperRef = useCallback((node: HTMLDivElement | null) =>');
|
||||
expect(modalSource).toContain("wrapperElement.addEventListener('pointerdown', handleFrameStart);");
|
||||
expect(modalSource).toContain("wrapperElement.addEventListener('mousedown', handleFrameStart);");
|
||||
expect(modalSource).toContain("startResize('south-east', event);");
|
||||
expect(modalSource).toContain("wrapperElement?.closest('.ant-modal')");
|
||||
expect(modalSource).toContain("modalNode.style.width = `${size.width}px`;");
|
||||
expect(modalSource).toContain("window.addEventListener('click', suppressInteractionClick, { capture: true, once: true });");
|
||||
expect(modalSource).toContain("window.removeEventListener('click', suppressInteractionClick, true);");
|
||||
expect(modalSource).toContain("window.addEventListener('blur', handleAbortDrag);");
|
||||
expect(modalSource).toContain('if (moveEvent.buttons === 0)');
|
||||
});
|
||||
|
||||
it('applies resized width and height to the underlying AntD modal nodes', () => {
|
||||
expect(modalSource).toContain("style['--gn-modal-resized-width'] = `${size.width}px`;");
|
||||
expect(modalSource).toContain("style['--gn-modal-resized-height'] = `${size.height}px`;");
|
||||
expect(modalCss).toContain(".gn-resizable-draggable-modal[data-has-resized-width='true']");
|
||||
expect(modalCss).toContain('width: var(--gn-modal-resized-width);');
|
||||
expect(modalCss).toContain(".gn-resizable-draggable-modal[data-has-resized-height='true'] .ant-modal-content");
|
||||
expect(modalCss).toContain('height: var(--gn-modal-resized-height);');
|
||||
expect(modalCss).toMatch(/\.gn-modal-resize-handle\s*\{[^}]*pointer-events:\s*auto;/s);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user