🔥 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:
Syngnat
2026-07-26 20:39:11 +08:00
parent 663ef8259b
commit 9074cd9707
115 changed files with 361 additions and 6624 deletions

View File

@@ -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);
});
});

View File

@@ -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"');
});
});

View File

@@ -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'");
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});
});

View File

@@ -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: '按主键分析新增、更新、删除和相同行。'");
});
});

View File

@@ -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('新版命令搜索适合跳转连接、表和动作');
});
});

View File

@@ -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({');
});
});

View File

@@ -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,
);
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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}`');
});
});

View File

@@ -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('撤销此单元格修改');
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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,/);
});
});

View File

@@ -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();
});
});
});

View File

@@ -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('>对象设计<');
});
});

View File

@@ -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);
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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*\)/);
});
});

View File

@@ -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');
});
});

View File

@@ -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'");
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -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('加载索引失败');
});
});

View File

@@ -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;/);
});
});

View File

@@ -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)
})
})
})
})

View File

@@ -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')
})
})

View File

@@ -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}');
});
});

View File

@@ -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'");
});
});

View File

@@ -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'");
});
});

View File

@@ -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}');
});
});

View File

@@ -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')");
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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([]);
});
});
});

View File

@@ -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']);
});
});
});

View File

@@ -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']);
});
});
});

View File

@@ -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([]);
});
});
});

View File

@@ -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([]);
});
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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']);
});
});
});

View File

@@ -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();
}
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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);
});
});

View File

@@ -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']);
});
});
});

View File

@@ -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}}');
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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);
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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');
});
});

View File

@@ -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)');
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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 })");
});
});

View File

@@ -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');
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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}`)");
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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"]');
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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']);
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -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([]);
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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']);
});
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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}}');
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();');
});
});

View File

@@ -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);
});
});
});
});
});

View File

@@ -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);');
});
});

View File

@@ -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)');
});
});

View File

@@ -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}');
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -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,
);
});
});

View File

@@ -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();
}
});
});

View File

@@ -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')");
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});
});

View File

@@ -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"');
});
});

View File

@@ -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