From 9074cd9707c6cc9b3c7b5e70c4cbc15dcaa7ee99 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 26 Jul 2026 20:39:11 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20remove(frontend-test):=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=BA=90=E7=A0=81=E6=96=87=E6=9C=AC=E6=96=AD?= =?UTF-8?q?=E8=A8=80=E5=9E=8B=E6=B5=8B=E8=AF=95=E5=B9=B6=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E5=85=A8=E5=B1=80=20i18n=20=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 105 个纯静态测试文件(6624 行):仅把源码当字符串断言、不执行被测代码 - 新增 testPolicy 守卫:禁止新增读源码文本的测试,131 个存量文件记入只减不增的基线 - 新增 i18n keyResolution:校验源码 t() 引用的 key 均可经完整解析链得到译文 - 新增 i18n catalogIntegrity:校验占位符跨语言一致、无空译文 - 补齐 6 个语言包缺失的 common.retry,修复审计面板重试按钮显示字面量 key Co-Authored-By: Claude Fable 5 --- frontend/src/App.about-brand-lockup.test.ts | 100 ---- frontend/src/App.close-tab-shortcut.test.ts | 70 --- frontend/src/App.settings-center.test.ts | 431 ------------------ frontend/src/App.sidebar-collapse.test.ts | 137 ------ frontend/src/App.tab-display.i18n.test.ts | 75 --- frontend/src/App.tools-entry.i18n.test.ts | 22 - frontend/src/App.ui-version.test.ts | 95 ---- frontend/src/App.update-quit-guard.test.ts | 100 ---- .../src/App.window-controls-spacing.test.ts | 15 - .../ConnectionModal.edit-password.test.tsx | 412 ----------------- .../components/DataExportFlow.i18n.test.ts | 69 --- .../DataGrid.auto-commit-delay.i18n.test.ts | 19 - .../DataGrid.auto-commit.i18n.test.ts | 14 - .../DataGrid.cell-undo-menu.i18n.test.ts | 17 - .../DataGrid.cell-undo.i18n.test.ts | 24 - ...aGrid.embedded-designer-title.i18n.test.ts | 23 - .../DataGrid.export-columns.test.ts | 18 - .../DataGrid.row-number.i18n.test.ts | 20 - .../DataGrid.secondary-actions.i18n.test.ts | 12 - .../DataGridToolbarFrame.i18n.test.ts | 26 -- .../DataImportWorkbench.i18n.test.ts | 73 --- .../src/components/DataSyncModal.i18n.test.ts | 118 ----- .../components/DefinitionViewer.i18n.test.ts | 51 --- .../src/components/ExportTitles.i18n.test.ts | 24 - .../MessagePublishModal.i18n.test.ts | 169 ------- .../src/components/QueryEditor.i18n.test.ts | 66 --- .../QueryEditor.legacy-layout.test.ts | 13 - ...Editor.sql-analysis-workbench.i18n.test.ts | 410 ----------------- ...QueryEditor.sql-analysis-workbench.test.ts | 92 ---- .../QueryEditorToolbar.ai-trigger.test.tsx | 20 - .../RedisCommandEditor.i18n.test.ts | 34 -- .../src/components/RedisMonitor.i18n.test.ts | 46 -- .../src/components/RedisViewer.i18n.test.ts | 87 ---- .../Sidebar.message-publish.test.tsx | 29 -- .../components/SidebarAIPrompt.i18n.test.ts | 52 --- .../SidebarBatchActions.i18n.test.ts | 130 ------ .../SidebarBatchClearFeedback.i18n.test.ts | 102 ----- ...arBatchDatabaseExportFeedback.i18n.test.ts | 93 ---- ...ebarBatchObjectExportFeedback.i18n.test.ts | 59 --- ...idebarBatchObjectLoadFeedback.i18n.test.ts | 45 -- .../SidebarCommandSearch.i18n.test.ts | 81 ---- .../SidebarCopyObjectName.i18n.test.ts | 57 --- .../SidebarCopyStructure.i18n.test.ts | 22 - .../SidebarCreateRoutine.i18n.test.ts | 26 -- ...arCreateRoutineDuckDBTemplate.i18n.test.ts | 23 - .../SidebarDangerOperationsMenu.i18n.test.ts | 11 - ...SidebarDatabaseExportFeedback.i18n.test.ts | 51 --- .../SidebarDeleteRoutineMenu.i18n.test.ts | 30 -- .../SidebarDropRoutineConfirm.i18n.test.ts | 36 -- .../SidebarEditDefinitionMenu.i18n.test.ts | 11 - .../SidebarEditEventTab.i18n.test.ts | 22 - ...SidebarEditRoutineSqlTemplate.i18n.test.ts | 23 - .../SidebarEditRoutineTab.i18n.test.ts | 23 - .../SidebarEditViewSqlTemplate.i18n.test.ts | 24 - .../components/SidebarEventTab.i18n.test.ts | 22 - ...idebarExternalSqlCrudFeedback.i18n.test.ts | 58 --- ...ebarExternalSqlDeleteFeedback.i18n.test.ts | 53 --- .../SidebarExternalSqlMenuLabels.i18n.test.ts | 62 --- .../SidebarExternalSqlModal.i18n.test.ts | 53 --- ...idebarExternalSqlOpenFeedback.i18n.test.ts | 26 -- .../SidebarExternalSqlRefresh.i18n.test.ts | 25 - .../components/SidebarFilterSync.i18n.test.ts | 49 -- .../SidebarLocateMessages.i18n.test.ts | 56 --- .../SidebarManagementModals.i18n.test.ts | 67 --- ...barMaterializedViewCreateMenu.i18n.test.ts | 22 - ...barMaterializedViewMenuLabels.i18n.test.ts | 22 - .../SidebarNewTableUnsupported.i18n.test.ts | 21 - .../SidebarObjectActions.i18n.test.ts | 193 -------- .../SidebarObjectGroups.i18n.test.ts | 80 ---- .../SidebarRedisDbMenu.i18n.test.ts | 27 -- .../SidebarResidualActions.i18n.test.ts | 114 ----- .../SidebarRoutineDefinitionTab.i18n.test.ts | 25 - .../SidebarSQLExportOptions.test.ts | 44 -- .../SidebarSavedQueriesTree.i18n.test.ts | 57 --- .../SidebarSchemaExportFeedback.i18n.test.ts | 71 --- .../SidebarSphinxCapability.i18n.test.ts | 41 -- .../SidebarStarRocksRollup.i18n.test.ts | 42 -- .../SidebarTableExportFeedback.i18n.test.ts | 51 --- .../SidebarTableFolders.i18n.test.ts | 33 -- .../components/SidebarTableTabs.i18n.test.ts | 37 -- .../SidebarTablesExportFeedback.i18n.test.ts | 67 --- .../components/SidebarTriggerTab.i18n.test.ts | 22 - .../SidebarV2GroupFallback.i18n.test.ts | 35 -- .../SidebarViewCreateEdit.i18n.test.ts | 31 -- .../SidebarViewDefinitionMenu.i18n.test.ts | 21 - .../SidebarViewDefinitionTab.i18n.test.ts | 20 - .../SidebarViewMenuLabels.i18n.test.ts | 28 -- .../TabManager.workbench-layout.test.ts | 40 -- .../src/components/TableDesigner.i18n.test.ts | 212 --------- .../TableDesigner.metadata-load.test.ts | 19 - .../TableDesigner.trigger-edit.test.ts | 38 -- .../TableOverview.context-menu.test.ts | 45 -- .../src/components/TableOverview.i18n.test.ts | 345 -------------- .../components/TableOverview.styles.test.ts | 28 -- .../WebAuthSettingsPanel.env-password.test.ts | 25 - .../src/components/ai/AIChatHeader.test.tsx | 12 - .../audit/SqlAuditWorkbench.i18n.test.ts | 56 --- .../audit/userActionAuditCoverage.test.ts | 50 -- .../components/closeTabShortcutGuards.test.ts | 29 -- .../common/ResizableDraggableModal.test.ts | 49 -- .../countdownDangerConfirm.wiring.test.ts | 59 --- .../settings/CustomThemeManager.test.ts | 45 -- frontend/src/i18n/catalogIntegrity.test.ts | 62 +++ frontend/src/i18n/keyResolution.test.ts | 82 ++++ .../src/nativeDetachedMain.styles.test.ts | 59 --- frontend/src/sidebarTreeScrollCss.test.ts | 27 -- .../src/testPolicy-sourceReadingBaseline.json | 132 ++++++ frontend/src/testPolicy.test.ts | 79 ++++ frontend/src/v2-theme.contrast.test.ts | 59 --- shared/i18n/de-DE.json | 1 + shared/i18n/en-US.json | 1 + shared/i18n/ja-JP.json | 1 + shared/i18n/ru-RU.json | 1 + shared/i18n/zh-CN.json | 1 + shared/i18n/zh-TW.json | 1 + 115 files changed, 361 insertions(+), 6624 deletions(-) delete mode 100644 frontend/src/App.about-brand-lockup.test.ts delete mode 100644 frontend/src/App.close-tab-shortcut.test.ts delete mode 100644 frontend/src/App.settings-center.test.ts delete mode 100644 frontend/src/App.sidebar-collapse.test.ts delete mode 100644 frontend/src/App.tab-display.i18n.test.ts delete mode 100644 frontend/src/App.tools-entry.i18n.test.ts delete mode 100644 frontend/src/App.ui-version.test.ts delete mode 100644 frontend/src/App.update-quit-guard.test.ts delete mode 100644 frontend/src/App.window-controls-spacing.test.ts delete mode 100644 frontend/src/components/ConnectionModal.edit-password.test.tsx delete mode 100644 frontend/src/components/DataExportFlow.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.auto-commit-delay.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.auto-commit.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.cell-undo-menu.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.cell-undo.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.embedded-designer-title.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.export-columns.test.ts delete mode 100644 frontend/src/components/DataGrid.row-number.i18n.test.ts delete mode 100644 frontend/src/components/DataGrid.secondary-actions.i18n.test.ts delete mode 100644 frontend/src/components/DataGridToolbarFrame.i18n.test.ts delete mode 100644 frontend/src/components/DataImportWorkbench.i18n.test.ts delete mode 100644 frontend/src/components/DataSyncModal.i18n.test.ts delete mode 100644 frontend/src/components/DefinitionViewer.i18n.test.ts delete mode 100644 frontend/src/components/ExportTitles.i18n.test.ts delete mode 100644 frontend/src/components/MessagePublishModal.i18n.test.ts delete mode 100644 frontend/src/components/QueryEditor.i18n.test.ts delete mode 100644 frontend/src/components/QueryEditor.legacy-layout.test.ts delete mode 100644 frontend/src/components/QueryEditor.sql-analysis-workbench.i18n.test.ts delete mode 100644 frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts delete mode 100644 frontend/src/components/QueryEditorToolbar.ai-trigger.test.tsx delete mode 100644 frontend/src/components/RedisCommandEditor.i18n.test.ts delete mode 100644 frontend/src/components/RedisMonitor.i18n.test.ts delete mode 100644 frontend/src/components/RedisViewer.i18n.test.ts delete mode 100644 frontend/src/components/Sidebar.message-publish.test.tsx delete mode 100644 frontend/src/components/SidebarAIPrompt.i18n.test.ts delete mode 100644 frontend/src/components/SidebarBatchActions.i18n.test.ts delete mode 100644 frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarCommandSearch.i18n.test.ts delete mode 100644 frontend/src/components/SidebarCopyObjectName.i18n.test.ts delete mode 100644 frontend/src/components/SidebarCopyStructure.i18n.test.ts delete mode 100644 frontend/src/components/SidebarCreateRoutine.i18n.test.ts delete mode 100644 frontend/src/components/SidebarCreateRoutineDuckDBTemplate.i18n.test.ts delete mode 100644 frontend/src/components/SidebarDangerOperationsMenu.i18n.test.ts delete mode 100644 frontend/src/components/SidebarDatabaseExportFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarDeleteRoutineMenu.i18n.test.ts delete mode 100644 frontend/src/components/SidebarDropRoutineConfirm.i18n.test.ts delete mode 100644 frontend/src/components/SidebarEditDefinitionMenu.i18n.test.ts delete mode 100644 frontend/src/components/SidebarEditEventTab.i18n.test.ts delete mode 100644 frontend/src/components/SidebarEditRoutineSqlTemplate.i18n.test.ts delete mode 100644 frontend/src/components/SidebarEditRoutineTab.i18n.test.ts delete mode 100644 frontend/src/components/SidebarEditViewSqlTemplate.i18n.test.ts delete mode 100644 frontend/src/components/SidebarEventTab.i18n.test.ts delete mode 100644 frontend/src/components/SidebarExternalSqlCrudFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarExternalSqlDeleteFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarExternalSqlMenuLabels.i18n.test.ts delete mode 100644 frontend/src/components/SidebarExternalSqlModal.i18n.test.ts delete mode 100644 frontend/src/components/SidebarExternalSqlOpenFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarExternalSqlRefresh.i18n.test.ts delete mode 100644 frontend/src/components/SidebarFilterSync.i18n.test.ts delete mode 100644 frontend/src/components/SidebarLocateMessages.i18n.test.ts delete mode 100644 frontend/src/components/SidebarManagementModals.i18n.test.ts delete mode 100644 frontend/src/components/SidebarMaterializedViewCreateMenu.i18n.test.ts delete mode 100644 frontend/src/components/SidebarMaterializedViewMenuLabels.i18n.test.ts delete mode 100644 frontend/src/components/SidebarNewTableUnsupported.i18n.test.ts delete mode 100644 frontend/src/components/SidebarObjectActions.i18n.test.ts delete mode 100644 frontend/src/components/SidebarObjectGroups.i18n.test.ts delete mode 100644 frontend/src/components/SidebarRedisDbMenu.i18n.test.ts delete mode 100644 frontend/src/components/SidebarResidualActions.i18n.test.ts delete mode 100644 frontend/src/components/SidebarRoutineDefinitionTab.i18n.test.ts delete mode 100644 frontend/src/components/SidebarSQLExportOptions.test.ts delete mode 100644 frontend/src/components/SidebarSavedQueriesTree.i18n.test.ts delete mode 100644 frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarSphinxCapability.i18n.test.ts delete mode 100644 frontend/src/components/SidebarStarRocksRollup.i18n.test.ts delete mode 100644 frontend/src/components/SidebarTableExportFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarTableFolders.i18n.test.ts delete mode 100644 frontend/src/components/SidebarTableTabs.i18n.test.ts delete mode 100644 frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarTriggerTab.i18n.test.ts delete mode 100644 frontend/src/components/SidebarV2GroupFallback.i18n.test.ts delete mode 100644 frontend/src/components/SidebarViewCreateEdit.i18n.test.ts delete mode 100644 frontend/src/components/SidebarViewDefinitionMenu.i18n.test.ts delete mode 100644 frontend/src/components/SidebarViewDefinitionTab.i18n.test.ts delete mode 100644 frontend/src/components/SidebarViewMenuLabels.i18n.test.ts delete mode 100644 frontend/src/components/TabManager.workbench-layout.test.ts delete mode 100644 frontend/src/components/TableDesigner.i18n.test.ts delete mode 100644 frontend/src/components/TableDesigner.metadata-load.test.ts delete mode 100644 frontend/src/components/TableDesigner.trigger-edit.test.ts delete mode 100644 frontend/src/components/TableOverview.context-menu.test.ts delete mode 100644 frontend/src/components/TableOverview.i18n.test.ts delete mode 100644 frontend/src/components/TableOverview.styles.test.ts delete mode 100644 frontend/src/components/WebAuthSettingsPanel.env-password.test.ts delete mode 100644 frontend/src/components/ai/AIChatHeader.test.tsx delete mode 100644 frontend/src/components/audit/SqlAuditWorkbench.i18n.test.ts delete mode 100644 frontend/src/components/audit/userActionAuditCoverage.test.ts delete mode 100644 frontend/src/components/closeTabShortcutGuards.test.ts delete mode 100644 frontend/src/components/common/ResizableDraggableModal.test.ts delete mode 100644 frontend/src/components/common/countdownDangerConfirm.wiring.test.ts delete mode 100644 frontend/src/components/settings/CustomThemeManager.test.ts create mode 100644 frontend/src/i18n/catalogIntegrity.test.ts create mode 100644 frontend/src/i18n/keyResolution.test.ts delete mode 100644 frontend/src/nativeDetachedMain.styles.test.ts delete mode 100644 frontend/src/sidebarTreeScrollCss.test.ts create mode 100644 frontend/src/testPolicy-sourceReadingBaseline.json create mode 100644 frontend/src/testPolicy.test.ts delete mode 100644 frontend/src/v2-theme.contrast.test.ts diff --git a/frontend/src/App.about-brand-lockup.test.ts b/frontend/src/App.about-brand-lockup.test.ts deleted file mode 100644 index 6fcf8768..00000000 --- a/frontend/src/App.about-brand-lockup.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/frontend/src/App.close-tab-shortcut.test.ts b/frontend/src/App.close-tab-shortcut.test.ts deleted file mode 100644 index 32b336ca..00000000 --- a/frontend/src/App.close-tab-shortcut.test.ts +++ /dev/null @@ -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('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"'); - }); -}); diff --git a/frontend/src/App.settings-center.test.ts b/frontend/src/App.settings-center.test.ts deleted file mode 100644 index 99b127c6..00000000 --- a/frontend/src/App.settings-center.test.ts +++ /dev/null @@ -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('preferences');"); - expect(appSource).toContain("const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(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('
'); - 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(" { - 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( - ' {'); - 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( - "
{t('app.data_root.log_directory.title')}
", - ); - 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( - ' {'); - 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(/ { - 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(''); - expect(appSource).toContain(''); - 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(' { - 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(' { - const settingsModalStart = appSource.indexOf( - "title={renderUtilityModalTitle(, 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('', 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'"); - }); -}); diff --git a/frontend/src/App.sidebar-collapse.test.ts b/frontend/src/App.sidebar-collapse.test.ts deleted file mode 100644 index f2825dba..00000000 --- a/frontend/src/App.sidebar-collapse.test.ts +++ /dev/null @@ -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(''); - expect(appSource).toContain(''); - expect(appSource).toContain("'app.sidebar.collapse'"); - expect(appSource).toContain("'app.sidebar.expand'"); - expect(appSource).toContain("case 'focusSidebarSearch':"); - expect(appSource).toContain('handleFocusSidebarSearch();'); - expect(appSource).toContain(''); - 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('', siderIndex); - const triggerStartIndex = appSource.lastIndexOf('', titlebarToggleIndex); - const triggerSource = appSource.slice(triggerStartIndex, triggerEndIndex); - const siderSource = appSource.slice(siderIndex, siderEndIndex); - const explorerActionsIndex = sidebarSource.indexOf('
'); - const fixedRailIndex = sidebarSource.indexOf('', explorerActionsIndex); - const explorerToggleIndex = sidebarSource.indexOf('data-sidebar-toggle-placement="explorer-header"', explorerActionsIndex); - const explorerToggleEndIndex = sidebarSource.indexOf('', explorerToggleIndex); - const explorerToggleStartIndex = sidebarSource.lastIndexOf(''); - const railPrimaryActionsIndex = connectionRailSource.indexOf('
{ - 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 && { - 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); - }); - }); -}); diff --git a/frontend/src/App.tools-entry.i18n.test.ts b/frontend/src/App.tools-entry.i18n.test.ts deleted file mode 100644 index 74a8195b..00000000 --- a/frontend/src/App.tools-entry.i18n.test.ts +++ /dev/null @@ -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: '按主键分析新增、更新、删除和相同行。'"); - }); -}); diff --git a/frontend/src/App.ui-version.test.ts b/frontend/src/App.ui-version.test.ts deleted file mode 100644 index 7c25ccfc..00000000 --- a/frontend/src/App.ui-version.test.ts +++ /dev/null @@ -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(''); - expect(appSource).toContain(''); - // 旧版布局仍保留侧栏导航 - 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\('\[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('新版命令搜索适合跳转连接、表和动作'); - }); -}); diff --git a/frontend/src/App.update-quit-guard.test.ts b/frontend/src/App.update-quit-guard.test.ts deleted file mode 100644 index 89851959..00000000 --- a/frontend/src/App.update-quit-guard.test.ts +++ /dev/null @@ -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;'); - expect(appSource).toContain('const handleApplicationQuitRequest = useCallback(async ('); - expect(appSource).toContain('confirmedAction?: ApplicationQuitConfirmedAction,'); - expect(appSource).toContain('cancelledAction?: () => void,'); - expect(appSource).toContain('const runConfirmedAction = async (): Promise => {'); - const runnerStart = appSource.indexOf('const runConfirmedAction = async (): Promise => {'); - 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({'); - }); -}); diff --git a/frontend/src/App.window-controls-spacing.test.ts b/frontend/src/App.window-controls-spacing.test.ts deleted file mode 100644 index 7a21e97c..00000000 --- a/frontend/src/App.window-controls-spacing.test.ts +++ /dev/null @@ -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, - ); - }); -}); diff --git a/frontend/src/components/ConnectionModal.edit-password.test.tsx b/frontend/src/components/ConnectionModal.edit-password.test.tsx deleted file mode 100644 index 2322886c..00000000 --- a/frontend/src/components/ConnectionModal.edit-password.test.tsx +++ /dev/null @@ -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 发现目标节点。"', - '当前', - 'message="SRV 与 SSH 隧道同时启用时,可能依赖本地 DNS 解析能力"', - 'label={mongoSrv ? "附加 SRV 主机(可选)" : "附加节点地址"}', - '? "可输入多个候选主机名,格式:host;若留空则仅使用上方主机。"', - ': "可输入多个节点地址,格式:host:port(回车确认)"', - 'label="副本集名称(可选)"', - 'label="副本集用户名(可选)"', - 'placeholder="留空沿用主用户名"', - 'label="副本集密码(可选)"', - 'emptyPlaceholder: "留空沿用主密码"', - 'retainedLabel: "已保存副本集密码"', - 'clearLabel: "清除已保存副本集密码"', - '当前已保存副本集密码。留空表示继续沿用,输入新值表示替换。', - '自动发现成员', - 'title: "角色"', - 'title: "健康"', - '? "正常" : "异常"', - 'label="认证库 (authSource)"', - 'placeholder="默认使用 database 或 admin"', - '读偏好 (readPreference)', - '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); - }); - }); -}); diff --git a/frontend/src/components/DataExportFlow.i18n.test.ts b/frontend/src/components/DataExportFlow.i18n.test.ts deleted file mode 100644 index 33bfe911..00000000 --- a/frontend/src/components/DataExportFlow.i18n.test.ts +++ /dev/null @@ -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, -])) as Record>; - -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); - }); - }); - }); -}); diff --git a/frontend/src/components/DataGrid.auto-commit-delay.i18n.test.ts b/frontend/src/components/DataGrid.auto-commit-delay.i18n.test.ts deleted file mode 100644 index acabc16d..00000000 --- a/frontend/src/components/DataGrid.auto-commit-delay.i18n.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/frontend/src/components/DataGrid.auto-commit.i18n.test.ts b/frontend/src/components/DataGrid.auto-commit.i18n.test.ts deleted file mode 100644 index 6c031686..00000000 --- a/frontend/src/components/DataGrid.auto-commit.i18n.test.ts +++ /dev/null @@ -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}`'); - }); -}); diff --git a/frontend/src/components/DataGrid.cell-undo-menu.i18n.test.ts b/frontend/src/components/DataGrid.cell-undo-menu.i18n.test.ts deleted file mode 100644 index a949bdcc..00000000 --- a/frontend/src/components/DataGrid.cell-undo-menu.i18n.test.ts +++ /dev/null @@ -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('撤销此单元格修改'); - }); - }); -}); diff --git a/frontend/src/components/DataGrid.cell-undo.i18n.test.ts b/frontend/src/components/DataGrid.cell-undo.i18n.test.ts deleted file mode 100644 index e1c11a42..00000000 --- a/frontend/src/components/DataGrid.cell-undo.i18n.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/frontend/src/components/DataGrid.embedded-designer-title.i18n.test.ts b/frontend/src/components/DataGrid.embedded-designer-title.i18n.test.ts deleted file mode 100644 index f2f10f47..00000000 --- a/frontend/src/components/DataGrid.embedded-designer-title.i18n.test.ts +++ /dev/null @@ -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; - - expect(catalog['data_grid.embedded_designer.title']).toEqual(expect.any(String)); - expect(catalog['data_grid.embedded_designer.title']).toContain('{{tableName}}'); - }); - }); -}); diff --git a/frontend/src/components/DataGrid.export-columns.test.ts b/frontend/src/components/DataGrid.export-columns.test.ts deleted file mode 100644 index 458e81f4..00000000 --- a/frontend/src/components/DataGrid.export-columns.test.ts +++ /dev/null @@ -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,/); - }); -}); diff --git a/frontend/src/components/DataGrid.row-number.i18n.test.ts b/frontend/src/components/DataGrid.row-number.i18n.test.ts deleted file mode 100644 index f150e728..00000000 --- a/frontend/src/components/DataGrid.row-number.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog[rowNumberAriaKey], `${locale}:${rowNumberAriaKey}`).toBeTruthy(); - }); - }); -}); diff --git a/frontend/src/components/DataGrid.secondary-actions.i18n.test.ts b/frontend/src/components/DataGrid.secondary-actions.i18n.test.ts deleted file mode 100644 index 38c00385..00000000 --- a/frontend/src/components/DataGrid.secondary-actions.i18n.test.ts +++ /dev/null @@ -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('>对象设计<'); - }); -}); diff --git a/frontend/src/components/DataGridToolbarFrame.i18n.test.ts b/frontend/src/components/DataGridToolbarFrame.i18n.test.ts deleted file mode 100644 index 16f870d1..00000000 --- a/frontend/src/components/DataGridToolbarFrame.i18n.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/frontend/src/components/DataImportWorkbench.i18n.test.ts b/frontend/src/components/DataImportWorkbench.i18n.test.ts deleted file mode 100644 index 62c5d568..00000000 --- a/frontend/src/components/DataImportWorkbench.i18n.test.ts +++ /dev/null @@ -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; - - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/DataSyncModal.i18n.test.ts b/frontend/src/components/DataSyncModal.i18n.test.ts deleted file mode 100644 index 97de84db..00000000 --- a/frontend/src/components/DataSyncModal.i18n.test.ts +++ /dev/null @@ -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('
'); - expect(source).toMatch(/embedded\s*\?\s*\(\s*dataSyncContent\s*\)/); - }); -}); diff --git a/frontend/src/components/DefinitionViewer.i18n.test.ts b/frontend/src/components/DefinitionViewer.i18n.test.ts deleted file mode 100644 index ea8d0aef..00000000 --- a/frontend/src/components/DefinitionViewer.i18n.test.ts +++ /dev/null @@ -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(/数据库:|>类型:/); - 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'); - }); -}); diff --git a/frontend/src/components/ExportTitles.i18n.test.ts b/frontend/src/components/ExportTitles.i18n.test.ts deleted file mode 100644 index 218349b4..00000000 --- a/frontend/src/components/ExportTitles.i18n.test.ts +++ /dev/null @@ -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'"); - }); -}); diff --git a/frontend/src/components/MessagePublishModal.i18n.test.ts b/frontend/src/components/MessagePublishModal.i18n.test.ts deleted file mode 100644 index 8fd1099a..00000000 --- a/frontend/src/components/MessagePublishModal.i18n.test.ts +++ /dev/null @@ -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; - - [ - '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; - - [ - '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); - }); - }); - }); -}); diff --git a/frontend/src/components/QueryEditor.i18n.test.ts b/frontend/src/components/QueryEditor.i18n.test.ts deleted file mode 100644 index 87840be9..00000000 --- a/frontend/src/components/QueryEditor.i18n.test.ts +++ /dev/null @@ -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('加载索引失败'); - }); -}); diff --git a/frontend/src/components/QueryEditor.legacy-layout.test.ts b/frontend/src/components/QueryEditor.legacy-layout.test.ts deleted file mode 100644 index 42a182ee..00000000 --- a/frontend/src/components/QueryEditor.legacy-layout.test.ts +++ /dev/null @@ -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;/); - }); -}); diff --git a/frontend/src/components/QueryEditor.sql-analysis-workbench.i18n.test.ts b/frontend/src/components/QueryEditor.sql-analysis-workbench.i18n.test.ts deleted file mode 100644 index 26d3c0e3..00000000 --- a/frontend/src/components/QueryEditor.sql-analysis-workbench.i18n.test.ts +++ /dev/null @@ -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, -])) as Record> - -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) - }) - }) - }) -}) diff --git a/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts b/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts deleted file mode 100644 index 572a8233..00000000 --- a/frontend/src/components/QueryEditor.sql-analysis-workbench.test.ts +++ /dev/null @@ -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(' { - 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(' { - 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(' { - 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') - }) -}) diff --git a/frontend/src/components/QueryEditorToolbar.ai-trigger.test.tsx b/frontend/src/components/QueryEditorToolbar.ai-trigger.test.tsx deleted file mode 100644 index d4dbab06..00000000 --- a/frontend/src/components/QueryEditorToolbar.ai-trigger.test.tsx +++ /dev/null @@ -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={}'); - expect(source).toContain('menu={{ items: aiMenuItems }}'); - expect(source).toContain('title={isV2Ui ? aiMoreTitle : undefined}'); - }); -}); diff --git a/frontend/src/components/RedisCommandEditor.i18n.test.ts b/frontend/src/components/RedisCommandEditor.i18n.test.ts deleted file mode 100644 index 0fe30acf..00000000 --- a/frontend/src/components/RedisCommandEditor.i18n.test.ts +++ /dev/null @@ -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'"); - }); -}); diff --git a/frontend/src/components/RedisMonitor.i18n.test.ts b/frontend/src/components/RedisMonitor.i18n.test.ts deleted file mode 100644 index dec328b0..00000000 --- a/frontend/src/components/RedisMonitor.i18n.test.ts +++ /dev/null @@ -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'"); - }); -}); diff --git a/frontend/src/components/RedisViewer.i18n.test.ts b/frontend/src/components/RedisViewer.i18n.test.ts deleted file mode 100644 index 950832da..00000000 --- a/frontend/src/components/RedisViewer.i18n.test.ts +++ /dev/null @@ -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}'); - }); -}); diff --git a/frontend/src/components/Sidebar.message-publish.test.tsx b/frontend/src/components/Sidebar.message-publish.test.tsx deleted file mode 100644 index ecc3d629..00000000 --- a/frontend/src/components/Sidebar.message-publish.test.tsx +++ /dev/null @@ -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(' { - 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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarBatchActions.i18n.test.ts b/frontend/src/components/SidebarBatchActions.i18n.test.ts deleted file mode 100644 index 92423270..00000000 --- a/frontend/src/components/SidebarBatchActions.i18n.test.ts +++ /dev/null @@ -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; - 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([]); - }); - }); -}); diff --git a/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts deleted file mode 100644 index 87baa728..00000000 --- a/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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']); - }); - }); -}); diff --git a/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts deleted file mode 100644 index 18af1551..00000000 --- a/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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']); - }); - }); -}); diff --git a/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts deleted file mode 100644 index 695e468c..00000000 --- a/frontend/src/components/SidebarBatchObjectExportFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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([]); - }); - }); -}); diff --git a/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts deleted file mode 100644 index fa0642d8..00000000 --- a/frontend/src/components/SidebarBatchObjectLoadFeedback.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - expect(placeholders(catalog[key])).toEqual([]); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarCommandSearch.i18n.test.ts b/frontend/src/components/SidebarCommandSearch.i18n.test.ts deleted file mode 100644 index 0e2179f1..00000000 --- a/frontend/src/components/SidebarCommandSearch.i18n.test.ts +++ /dev/null @@ -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'", - '导航', - '选择', - '只搜表对象', - '发送给 AI', - ].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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarCopyObjectName.i18n.test.ts b/frontend/src/components/SidebarCopyObjectName.i18n.test.ts deleted file mode 100644 index 73404a77..00000000 --- a/frontend/src/components/SidebarCopyObjectName.i18n.test.ts +++ /dev/null @@ -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; - 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']); - }); - }); -}); diff --git a/frontend/src/components/SidebarCopyStructure.i18n.test.ts b/frontend/src/components/SidebarCopyStructure.i18n.test.ts deleted file mode 100644 index 87e4d2ac..00000000 --- a/frontend/src/components/SidebarCopyStructure.i18n.test.ts +++ /dev/null @@ -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; - - expect(catalog['table_overview.message.copy_structure_success']).toBeTruthy(); - } - }); -}); diff --git a/frontend/src/components/SidebarCreateRoutine.i18n.test.ts b/frontend/src/components/SidebarCreateRoutine.i18n.test.ts deleted file mode 100644 index 26744c4d..00000000 --- a/frontend/src/components/SidebarCreateRoutine.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog['sidebar.tab.create_function'], `${locale}:create function`).toBeTruthy(); - expect(catalog['sidebar.tab.create_procedure'], `${locale}:create procedure`).toBeTruthy(); - }); - }); -}); diff --git a/frontend/src/components/SidebarCreateRoutineDuckDBTemplate.i18n.test.ts b/frontend/src/components/SidebarCreateRoutineDuckDBTemplate.i18n.test.ts deleted file mode 100644 index b63391f4..00000000 --- a/frontend/src/components/SidebarCreateRoutineDuckDBTemplate.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/SidebarDangerOperationsMenu.i18n.test.ts b/frontend/src/components/SidebarDangerOperationsMenu.i18n.test.ts deleted file mode 100644 index 7e353d0b..00000000 --- a/frontend/src/components/SidebarDangerOperationsMenu.i18n.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/frontend/src/components/SidebarDatabaseExportFeedback.i18n.test.ts b/frontend/src/components/SidebarDatabaseExportFeedback.i18n.test.ts deleted file mode 100644 index b905813c..00000000 --- a/frontend/src/components/SidebarDatabaseExportFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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']); - }); - }); -}); diff --git a/frontend/src/components/SidebarDeleteRoutineMenu.i18n.test.ts b/frontend/src/components/SidebarDeleteRoutineMenu.i18n.test.ts deleted file mode 100644 index d0e28d36..00000000 --- a/frontend/src/components/SidebarDeleteRoutineMenu.i18n.test.ts +++ /dev/null @@ -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; - 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; - const zhTW = JSON.parse(readFileSync(new URL('../../../shared/i18n/zh-TW.json', import.meta.url), 'utf8')) as Record; - expect(zhCN['sidebar.menu.delete_routine']).toBe('删除{{type}}'); - expect(zhTW['sidebar.menu.delete_routine']).toBe('刪除{{type}}'); - }); -}); diff --git a/frontend/src/components/SidebarDropRoutineConfirm.i18n.test.ts b/frontend/src/components/SidebarDropRoutineConfirm.i18n.test.ts deleted file mode 100644 index 931f5644..00000000 --- a/frontend/src/components/SidebarDropRoutineConfirm.i18n.test.ts +++ /dev/null @@ -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; - 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}}'); - }); - }); -}); diff --git a/frontend/src/components/SidebarEditDefinitionMenu.i18n.test.ts b/frontend/src/components/SidebarEditDefinitionMenu.i18n.test.ts deleted file mode 100644 index 6bb6d1a8..00000000 --- a/frontend/src/components/SidebarEditDefinitionMenu.i18n.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/frontend/src/components/SidebarEditEventTab.i18n.test.ts b/frontend/src/components/SidebarEditEventTab.i18n.test.ts deleted file mode 100644 index cf1c67ac..00000000 --- a/frontend/src/components/SidebarEditEventTab.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - expect(catalog[key], `${locale}:${key}`).toContain('{{name}}'); - }); - }); -}); diff --git a/frontend/src/components/SidebarEditRoutineSqlTemplate.i18n.test.ts b/frontend/src/components/SidebarEditRoutineSqlTemplate.i18n.test.ts deleted file mode 100644 index 3e31f379..00000000 --- a/frontend/src/components/SidebarEditRoutineSqlTemplate.i18n.test.ts +++ /dev/null @@ -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; - 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}}'); - }); - }); -}); diff --git a/frontend/src/components/SidebarEditRoutineTab.i18n.test.ts b/frontend/src/components/SidebarEditRoutineTab.i18n.test.ts deleted file mode 100644 index a0d5a2c3..00000000 --- a/frontend/src/components/SidebarEditRoutineTab.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/SidebarEditViewSqlTemplate.i18n.test.ts b/frontend/src/components/SidebarEditViewSqlTemplate.i18n.test.ts deleted file mode 100644 index 5d271467..00000000 --- a/frontend/src/components/SidebarEditViewSqlTemplate.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/SidebarEventTab.i18n.test.ts b/frontend/src/components/SidebarEventTab.i18n.test.ts deleted file mode 100644 index a4a3caf1..00000000 --- a/frontend/src/components/SidebarEventTab.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - expect(catalog[key], `${locale}:${key}`).toContain('{{name}}'); - }); - }); -}); diff --git a/frontend/src/components/SidebarExternalSqlCrudFeedback.i18n.test.ts b/frontend/src/components/SidebarExternalSqlCrudFeedback.i18n.test.ts deleted file mode 100644 index d5e7e845..00000000 --- a/frontend/src/components/SidebarExternalSqlCrudFeedback.i18n.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/frontend/src/components/SidebarExternalSqlDeleteFeedback.i18n.test.ts b/frontend/src/components/SidebarExternalSqlDeleteFeedback.i18n.test.ts deleted file mode 100644 index 38938659..00000000 --- a/frontend/src/components/SidebarExternalSqlDeleteFeedback.i18n.test.ts +++ /dev/null @@ -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)'); - }); -}); diff --git a/frontend/src/components/SidebarExternalSqlMenuLabels.i18n.test.ts b/frontend/src/components/SidebarExternalSqlMenuLabels.i18n.test.ts deleted file mode 100644 index 3e66c467..00000000 --- a/frontend/src/components/SidebarExternalSqlMenuLabels.i18n.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/frontend/src/components/SidebarExternalSqlModal.i18n.test.ts b/frontend/src/components/SidebarExternalSqlModal.i18n.test.ts deleted file mode 100644 index 5c67f496..00000000 --- a/frontend/src/components/SidebarExternalSqlModal.i18n.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/frontend/src/components/SidebarExternalSqlOpenFeedback.i18n.test.ts b/frontend/src/components/SidebarExternalSqlOpenFeedback.i18n.test.ts deleted file mode 100644 index 38e285d3..00000000 --- a/frontend/src/components/SidebarExternalSqlOpenFeedback.i18n.test.ts +++ /dev/null @@ -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 })"); - }); -}); diff --git a/frontend/src/components/SidebarExternalSqlRefresh.i18n.test.ts b/frontend/src/components/SidebarExternalSqlRefresh.i18n.test.ts deleted file mode 100644 index c7287bd7..00000000 --- a/frontend/src/components/SidebarExternalSqlRefresh.i18n.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/frontend/src/components/SidebarFilterSync.i18n.test.ts b/frontend/src/components/SidebarFilterSync.i18n.test.ts deleted file mode 100644 index 77179312..00000000 --- a/frontend/src/components/SidebarFilterSync.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarLocateMessages.i18n.test.ts b/frontend/src/components/SidebarLocateMessages.i18n.test.ts deleted file mode 100644 index 8b6a645a..00000000 --- a/frontend/src/components/SidebarLocateMessages.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarManagementModals.i18n.test.ts b/frontend/src/components/SidebarManagementModals.i18n.test.ts deleted file mode 100644 index c3c63320..00000000 --- a/frontend/src/components/SidebarManagementModals.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarMaterializedViewCreateMenu.i18n.test.ts b/frontend/src/components/SidebarMaterializedViewCreateMenu.i18n.test.ts deleted file mode 100644 index 44e2516e..00000000 --- a/frontend/src/components/SidebarMaterializedViewCreateMenu.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog['sidebar.v2_database_menu.new_materialized_view'], `${locale}:new materialized view`).toBeTruthy(); - }); - }); -}); diff --git a/frontend/src/components/SidebarMaterializedViewMenuLabels.i18n.test.ts b/frontend/src/components/SidebarMaterializedViewMenuLabels.i18n.test.ts deleted file mode 100644 index 266f7251..00000000 --- a/frontend/src/components/SidebarMaterializedViewMenuLabels.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/SidebarNewTableUnsupported.i18n.test.ts b/frontend/src/components/SidebarNewTableUnsupported.i18n.test.ts deleted file mode 100644 index 0012b1d5..00000000 --- a/frontend/src/components/SidebarNewTableUnsupported.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); -}); diff --git a/frontend/src/components/SidebarObjectActions.i18n.test.ts b/frontend/src/components/SidebarObjectActions.i18n.test.ts deleted file mode 100644 index 7e6bfc99..00000000 --- a/frontend/src/components/SidebarObjectActions.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarObjectGroups.i18n.test.ts b/frontend/src/components/SidebarObjectGroups.i18n.test.ts deleted file mode 100644 index 0aea2f38..00000000 --- a/frontend/src/components/SidebarObjectGroups.i18n.test.ts +++ /dev/null @@ -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; - 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; - visibilityKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarRedisDbMenu.i18n.test.ts b/frontend/src/components/SidebarRedisDbMenu.i18n.test.ts deleted file mode 100644 index 27c7922e..00000000 --- a/frontend/src/components/SidebarRedisDbMenu.i18n.test.ts +++ /dev/null @@ -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}`)"); - }); -}); diff --git a/frontend/src/components/SidebarResidualActions.i18n.test.ts b/frontend/src/components/SidebarResidualActions.i18n.test.ts deleted file mode 100644 index b4ec7fab..00000000 --- a/frontend/src/components/SidebarResidualActions.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarRoutineDefinitionTab.i18n.test.ts b/frontend/src/components/SidebarRoutineDefinitionTab.i18n.test.ts deleted file mode 100644 index d0be87fa..00000000 --- a/frontend/src/components/SidebarRoutineDefinitionTab.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/SidebarSQLExportOptions.test.ts b/frontend/src/components/SidebarSQLExportOptions.test.ts deleted file mode 100644 index b27f851d..00000000 --- a/frontend/src/components/SidebarSQLExportOptions.test.ts +++ /dev/null @@ -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"]'); - }); -}); diff --git a/frontend/src/components/SidebarSavedQueriesTree.i18n.test.ts b/frontend/src/components/SidebarSavedQueriesTree.i18n.test.ts deleted file mode 100644 index 3b3871d2..00000000 --- a/frontend/src/components/SidebarSavedQueriesTree.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts b/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts deleted file mode 100644 index 4a0ef49e..00000000 --- a/frontend/src/components/SidebarSchemaExportFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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']); - }); - }); -}); diff --git a/frontend/src/components/SidebarSphinxCapability.i18n.test.ts b/frontend/src/components/SidebarSphinxCapability.i18n.test.ts deleted file mode 100644 index 4bd9376a..00000000 --- a/frontend/src/components/SidebarSphinxCapability.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarStarRocksRollup.i18n.test.ts b/frontend/src/components/SidebarStarRocksRollup.i18n.test.ts deleted file mode 100644 index 99eb7169..00000000 --- a/frontend/src/components/SidebarStarRocksRollup.i18n.test.ts +++ /dev/null @@ -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, -])) as Record>; - -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); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarTableExportFeedback.i18n.test.ts b/frontend/src/components/SidebarTableExportFeedback.i18n.test.ts deleted file mode 100644 index a6db5dbb..00000000 --- a/frontend/src/components/SidebarTableExportFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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([]); - }); - }); -}); diff --git a/frontend/src/components/SidebarTableFolders.i18n.test.ts b/frontend/src/components/SidebarTableFolders.i18n.test.ts deleted file mode 100644 index b7d3c175..00000000 --- a/frontend/src/components/SidebarTableFolders.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarTableTabs.i18n.test.ts b/frontend/src/components/SidebarTableTabs.i18n.test.ts deleted file mode 100644 index dc676322..00000000 --- a/frontend/src/components/SidebarTableTabs.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts b/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts deleted file mode 100644 index a8521d60..00000000 --- a/frontend/src/components/SidebarTablesExportFeedback.i18n.test.ts +++ /dev/null @@ -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; - 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']); - }); - }); -}); diff --git a/frontend/src/components/SidebarTriggerTab.i18n.test.ts b/frontend/src/components/SidebarTriggerTab.i18n.test.ts deleted file mode 100644 index 4c9724bf..00000000 --- a/frontend/src/components/SidebarTriggerTab.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - expect(catalog[key], `${locale}:${key}`).toContain('{{name}}'); - }); - }); -}); diff --git a/frontend/src/components/SidebarV2GroupFallback.i18n.test.ts b/frontend/src/components/SidebarV2GroupFallback.i18n.test.ts deleted file mode 100644 index 1e74634d..00000000 --- a/frontend/src/components/SidebarV2GroupFallback.i18n.test.ts +++ /dev/null @@ -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; - requiredKeys.forEach((key) => { - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); - }); -}); diff --git a/frontend/src/components/SidebarViewCreateEdit.i18n.test.ts b/frontend/src/components/SidebarViewCreateEdit.i18n.test.ts deleted file mode 100644 index a6128d10..00000000 --- a/frontend/src/components/SidebarViewCreateEdit.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/SidebarViewDefinitionMenu.i18n.test.ts b/frontend/src/components/SidebarViewDefinitionMenu.i18n.test.ts deleted file mode 100644 index 2918626c..00000000 --- a/frontend/src/components/SidebarViewDefinitionMenu.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog[key], `${locale}:${key}`).toBeTruthy(); - }); - }); -}); diff --git a/frontend/src/components/SidebarViewDefinitionTab.i18n.test.ts b/frontend/src/components/SidebarViewDefinitionTab.i18n.test.ts deleted file mode 100644 index e68a4c71..00000000 --- a/frontend/src/components/SidebarViewDefinitionTab.i18n.test.ts +++ /dev/null @@ -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; - expect(catalog['sidebar.tab.view_definition'], `${locale}:view definition`).toContain('{{name}}'); - expect(catalog['sidebar.tab.materialized_view_definition'], `${locale}:materialized view definition`).toContain('{{name}}'); - }); - }); -}); diff --git a/frontend/src/components/SidebarViewMenuLabels.i18n.test.ts b/frontend/src/components/SidebarViewMenuLabels.i18n.test.ts deleted file mode 100644 index 91696fc2..00000000 --- a/frontend/src/components/SidebarViewMenuLabels.i18n.test.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/frontend/src/components/TabManager.workbench-layout.test.ts b/frontend/src/components/TabManager.workbench-layout.test.ts deleted file mode 100644 index 7c956a76..00000000 --- a/frontend/src/components/TabManager.workbench-layout.test.ts +++ /dev/null @@ -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*\\{(?[^}]*)\\}`, '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();'); - }); -}); diff --git a/frontend/src/components/TableDesigner.i18n.test.ts b/frontend/src/components/TableDesigner.i18n.test.ts deleted file mode 100644 index 7fee5e3d..00000000 --- a/frontend/src/components/TableDesigner.i18n.test.ts +++ /dev/null @@ -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; - -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); - }); - }); - }); - }); -}); diff --git a/frontend/src/components/TableDesigner.metadata-load.test.ts b/frontend/src/components/TableDesigner.metadata-load.test.ts deleted file mode 100644 index 29f5ede1..00000000 --- a/frontend/src/components/TableDesigner.metadata-load.test.ts +++ /dev/null @@ -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);'); - }); -}); diff --git a/frontend/src/components/TableDesigner.trigger-edit.test.ts b/frontend/src/components/TableDesigner.trigger-edit.test.ts deleted file mode 100644 index 7de3c25d..00000000 --- a/frontend/src/components/TableDesigner.trigger-edit.test.ts +++ /dev/null @@ -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)'); - }); -}); diff --git a/frontend/src/components/TableOverview.context-menu.test.ts b/frontend/src/components/TableOverview.context-menu.test.ts deleted file mode 100644 index 5d7b6763..00000000 --- a/frontend/src/components/TableOverview.context-menu.test.ts +++ /dev/null @@ -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}'); - }); -}); diff --git a/frontend/src/components/TableOverview.i18n.test.ts b/frontend/src/components/TableOverview.i18n.test.ts deleted file mode 100644 index 824f7ede..00000000 --- a/frontend/src/components/TableOverview.i18n.test.ts +++ /dev/null @@ -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, -])) as Record>; - -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(() => {'), - 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 {"), - 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("
行数
"); - expect(listSource).not.toContain("
数据大小
"); - expect(listSource).not.toContain("
索引大小
"); - expect(listSource).not.toContain("
相对大小
"); - 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('{section.title}'); - 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); - }); - }); - }); -}); diff --git a/frontend/src/components/TableOverview.styles.test.ts b/frontend/src/components/TableOverview.styles.test.ts deleted file mode 100644 index 1fe5f8e6..00000000 --- a/frontend/src/components/TableOverview.styles.test.ts +++ /dev/null @@ -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, - ); - }); -}); diff --git a/frontend/src/components/WebAuthSettingsPanel.env-password.test.ts b/frontend/src/components/WebAuthSettingsPanel.env-password.test.ts deleted file mode 100644 index 043e6c6d..00000000 --- a/frontend/src/components/WebAuthSettingsPanel.env-password.test.ts +++ /dev/null @@ -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; - 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(); - } - }); -}); diff --git a/frontend/src/components/ai/AIChatHeader.test.tsx b/frontend/src/components/ai/AIChatHeader.test.tsx deleted file mode 100644 index baddbbf4..00000000 --- a/frontend/src/components/ai/AIChatHeader.test.tsx +++ /dev/null @@ -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')"); - }); -}); diff --git a/frontend/src/components/audit/SqlAuditWorkbench.i18n.test.ts b/frontend/src/components/audit/SqlAuditWorkbench.i18n.test.ts deleted file mode 100644 index 146d299c..00000000 --- a/frontend/src/components/audit/SqlAuditWorkbench.i18n.test.ts +++ /dev/null @@ -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, -])) as Record>; - -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'); - }); -}); diff --git a/frontend/src/components/audit/userActionAuditCoverage.test.ts b/frontend/src/components/audit/userActionAuditCoverage.test.ts deleted file mode 100644 index 11ff0d27..00000000 --- a/frontend/src/components/audit/userActionAuditCoverage.test.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/frontend/src/components/closeTabShortcutGuards.test.ts b/frontend/src/components/closeTabShortcutGuards.test.ts deleted file mode 100644 index a6188521..00000000 --- a/frontend/src/components/closeTabShortcutGuards.test.ts +++ /dev/null @@ -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"'); - }); -}); diff --git a/frontend/src/components/common/ResizableDraggableModal.test.ts b/frontend/src/components/common/ResizableDraggableModal.test.ts deleted file mode 100644 index 4a9eb618..00000000 --- a/frontend/src/components/common/ResizableDraggableModal.test.ts +++ /dev/null @@ -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'); - expect(modalSource).toContain(''); - 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;'); - expect(modalSource).toContain("const activeInteractionRef = useRef<'drag' | 'resize' | null>(null);"); - expect(modalSource).toContain('const [wrapperElement, setWrapperElement] = useState(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); - }); -}); diff --git a/frontend/src/components/common/countdownDangerConfirm.wiring.test.ts b/frontend/src/components/common/countdownDangerConfirm.wiring.test.ts deleted file mode 100644 index 731728e9..00000000 --- a/frontend/src/components/common/countdownDangerConfirm.wiring.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { describe, expect, it } from 'vitest'; - -const sidebarObjectActionsSource = readFileSync( - new URL('../sidebar/useSidebarObjectActions.tsx', import.meta.url), - 'utf8', -); -const tableOverviewSource = readFileSync( - new URL('../TableOverview.tsx', import.meta.url), - 'utf8', -); -const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; - -const sliceBetween = (source: string, start: string, end: string): string => { - const startIndex = source.indexOf(start); - const endIndex = source.indexOf(end, startIndex + start.length); - expect(startIndex).toBeGreaterThanOrEqual(0); - expect(endIndex).toBeGreaterThan(startIndex); - return source.slice(startIndex, endIndex); -}; - -describe('countdown danger confirmation wiring', () => { - it('protects every table and database right-click DROP entry point', () => { - const sidebarDeleteDatabase = sliceBetween( - sidebarObjectActionsSource, - 'const handleDeleteDatabase =', - 'const handleRenameTable =', - ); - const sidebarDeleteTable = sliceBetween( - sidebarObjectActionsSource, - 'const handleDeleteTable =', - 'const handleTableDataDangerAction =', - ); - const overviewDeleteTable = sliceBetween( - tableOverviewSource, - 'const handleDeleteTable =', - 'const handleTableDataDangerAction =', - ); - - [sidebarDeleteDatabase, sidebarDeleteTable, overviewDeleteTable].forEach((source) => { - expect(source).toContain('showCountdownDangerConfirm({'); - expect(source).not.toContain('Modal.confirm({'); - }); - }); - - it('keeps countdown text and placeholders available in every locale', () => { - locales.forEach((locale) => { - const catalog = JSON.parse(readFileSync( - new URL(`../../../../shared/i18n/${locale}.json`, import.meta.url), - 'utf8', - )) as Record; - - expect(catalog['common.destructive_confirm.action_countdown'], `${locale}:action`).toContain('{{action}}'); - expect(catalog['common.destructive_confirm.action_countdown'], `${locale}:seconds`).toContain('{{seconds}}'); - expect(catalog['common.destructive_confirm.countdown'], `${locale}:countdown`).toContain('{{seconds}}'); - expect(catalog['common.destructive_confirm.ready'], `${locale}:ready`).toBeTruthy(); - }); - }); -}); diff --git a/frontend/src/components/settings/CustomThemeManager.test.ts b/frontend/src/components/settings/CustomThemeManager.test.ts deleted file mode 100644 index eeab5478..00000000 --- a/frontend/src/components/settings/CustomThemeManager.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; - -const source = readFileSync( - fileURLToPath(new globalThis.URL('./CustomThemeManager.tsx', import.meta.url)), - 'utf8', -); -const styles = readFileSync( - fileURLToPath(new globalThis.URL('./CustomThemeManager.css', import.meta.url)), - 'utf8', -); - -describe('CustomThemeManager', () => { - it('supports the complete import and management workflow', () => { - expect(source).toContain('accept=".css,text/css"'); - expect(source).toContain("requestFile({ kind: 'import' })"); - expect(source).toContain("requestFile({ kind: 'replace', themeId: theme.id })"); - expect(source).toContain('selectCustomTheme(theme.id)'); - expect(source).toContain('selectCustomTheme(null)'); - expect(source).toContain('updateCustomTheme(theme.id, { baseMode })'); - expect(source).toContain('removeCustomTheme(theme.id)'); - expect(source).toContain('CUSTOM_THEME_TEMPLATE'); - }); - - it('provides accessible selection, destructive confirmation, and responsive layout', () => { - expect(source).toContain('
'); - expect(source).toContain(''); - expect(source).toContain('BUILTIN_CUSTOM_THEME_PRESETS.map((preset) =>'); - expect(source).toContain('type="radio"'); - expect(source).toContain('name="gonavi-custom-theme-selection"'); - expect(source).toContain('checked={active}'); - expect(source).toContain('onChange={() => handleSelect(theme)}'); - expect(source).toContain('onChange={() => handleSelect(preset, displayName)}'); - expect(source).toContain('const builtinThemeTitleId = useId();'); - expect(source).toContain('aria-describedby={`${descriptionId} ${modeId}`}'); - expect(source).toContain(' [ + locale, + JSON.parse(readFileSync( + fileURLToPath(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url)), + 'utf8', + )) as Record, +])) as Record>; + +/** 译文中的 {{param}} 占位符;缺失或拼错会让界面直接显示原始占位符文本 */ +const placeholdersOf = (value: string): string[] => Array.from( + value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), + (match) => match[1], +).sort(); + +describe('i18n catalog integrity', () => { + it('keeps placeholder sets identical across every locale', () => { + const keys = Object.keys(catalogs['en-US']); + expect(keys.length).toBeGreaterThan(8000); + + const mismatched: string[] = []; + for (const key of keys) { + const expected = placeholdersOf(catalogs['en-US'][key]); + for (const locale of LOCALES) { + const actual = placeholdersOf(catalogs[locale][key]); + if (actual.join('|') !== expected.join('|')) { + mismatched.push(`${locale}:${key} expected [${expected}] got [${actual}]`); + } + } + } + + expect(mismatched).toEqual([]); + }); + + it('has no blank translation in any locale', () => { + const blank: string[] = []; + for (const locale of LOCALES) { + for (const [key, value] of Object.entries(catalogs[locale])) { + if (typeof value !== 'string' || value.trim() === '') blank.push(`${locale}:${key}`); + } + } + + expect(blank).toEqual([]); + }); +}); diff --git a/frontend/src/i18n/keyResolution.test.ts b/frontend/src/i18n/keyResolution.test.ts new file mode 100644 index 00000000..9905c3e5 --- /dev/null +++ b/frontend/src/i18n/keyResolution.test.ts @@ -0,0 +1,82 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +/** + * 全局不变式:源码中 t('字面量') 引用的每个 key,都必须能经完整解析链得到译文。 + * + * 解析链复刻自 src/i18n/index.ts 的 t(): + * toCatalogKey 前缀映射 → shared/i18n/.json → legacyTranslate(shared/i18n/messages.ts) + * 任一环节命中即视为可解析;全部落空时会原样返回 key, + * 用户界面上会直接显示形如 "common.retry" 的字面量。 + * + * 本用例取代了此前散落在上百个组件测试中的「某 key 在各语言包存在」断言: + * 那些断言只覆盖有人恰好想到的 key,本用例覆盖全部引用点且不会随重构失效。 + * 语言包之间的 key 集合一致性由 shared/i18n/catalog_test.go 的 TestCatalogKeysMatch 保证。 + */ + +const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); +const frontendSrc = `${repoRoot}frontend/src`; + +// 与 src/i18n/index.ts 的 toCatalogKey 保持一致 +const ACTION_ALIASES: Record = { + 'common.action.cancel': 'common.cancel', + 'common.action.close': 'common.close', + 'common.action.confirm': 'common.confirm', + 'common.action.continue': 'common.continue', + 'common.action.delete': 'common.delete', + 'common.action.save': 'common.save', +}; + +const toCatalogKey = (key: string): string => { + if (ACTION_ALIASES[key]) return ACTION_ALIASES[key]; + if (key.startsWith('connection.modal.')) { + return `connection_modal.${key.slice('connection.modal.'.length)}`; + } + if (key.startsWith('driver.manager.')) { + return `driver_manager.${key.slice('driver.manager.'.length)}`; + } + return key; +}; + +const collectRuntimeSources = (directory: string): string[] => readdirSync(directory) + .flatMap((entry) => { + const absolutePath = `${directory}/${entry}`; + if (statSync(absolutePath).isDirectory()) { + return /^(?:node_modules|dist)$/.test(entry) ? [] : collectRuntimeSources(absolutePath); + } + if (!/\.(?:ts|tsx)$/.test(entry) || /\.(?:test|spec)\.(?:ts|tsx)$/.test(entry)) { + return []; + } + return [absolutePath]; + }); + +describe('i18n key resolution', () => { + it('resolves every literal t() key through the catalog or legacy fallback', () => { + const catalogKeys = new Set(Object.keys( + JSON.parse(readFileSync(`${repoRoot}shared/i18n/zh-CN.json`, 'utf8')) as Record, + )); + const legacySource = readFileSync(`${repoRoot}shared/i18n/messages.ts`, 'utf8'); + const legacyKeys = new Set( + Array.from(legacySource.matchAll(/^\s{4}["']([a-zA-Z0-9_.-]+)["']\s*:/gm), (match) => match[1]), + ); + + const unresolved: string[] = []; + let scanned = 0; + for (const file of collectRuntimeSources(frontendSrc)) { + const source = readFileSync(file, 'utf8'); + for (const match of source.matchAll(/\bt\(\s*'([a-zA-Z0-9_.-]+)'/g)) { + const key = match[1]; + scanned += 1; + const mapped = toCatalogKey(key); + if (catalogKeys.has(mapped) || legacyKeys.has(key) || legacyKeys.has(mapped)) continue; + unresolved.push(`${key} (${file.slice(repoRoot.length)})`); + } + } + + // 下限断言:扫描器若因目录结构或正则失效而扫不到东西,本用例必须报错而不是静默通过 + expect(scanned).toBeGreaterThan(3000); + expect(catalogKeys.size).toBeGreaterThan(8000); + expect(unresolved).toEqual([]); + }); +}); diff --git a/frontend/src/nativeDetachedMain.styles.test.ts b/frontend/src/nativeDetachedMain.styles.test.ts deleted file mode 100644 index cf223b50..00000000 --- a/frontend/src/nativeDetachedMain.styles.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; - -const detachedEntrySource = readFileSync( - fileURLToPath(new globalThis.URL('./nativeDetachedMain.tsx', import.meta.url)), - 'utf8', -); -const detachedAppSource = readFileSync( - fileURLToPath(new globalThis.URL('./components/NativeDetachedWindowApp.tsx', import.meta.url)), - 'utf8', -); -const aiChatPanelSource = readFileSync( - fileURLToPath(new globalThis.URL('./components/AIChatPanel.tsx', import.meta.url)), - 'utf8', -); -const dataGridSource = readFileSync( - fileURLToPath(new globalThis.URL('./components/DataGrid.tsx', import.meta.url)), - 'utf8', -); -const workbenchContentSource = readFileSync( - fileURLToPath(new globalThis.URL('./components/WorkbenchTabContent.tsx', import.meta.url)), - 'utf8', -); -const workbenchThemeSource = readFileSync( - fileURLToPath(new globalThis.URL('./styles/v2-theme-workbench.css', import.meta.url)), - 'utf8', -); -const aiThemeSource = readFileSync( - fileURLToPath(new globalThis.URL('./styles/v2-theme-ai.css', import.meta.url)), - 'utf8', -); - -describe('native detached window styles', () => { - it('keeps feature styles out of the detached entry bootstrap', () => { - expect(detachedEntrySource).toContain("import './App.css';"); - expect(detachedEntrySource).toContain("import './v2-theme.css';"); - expect(detachedEntrySource).not.toContain("import './styles/v2-theme-workbench.css';"); - expect(detachedEntrySource).not.toContain("import './styles/v2-theme-ai.css';"); - }); - - it('loads workbench and AI assets only from their feature paths', () => { - expect(detachedAppSource).toContain( - "const WorkbenchTabContent = React.lazy(() => import('./WorkbenchTabContent'));", - ); - expect(detachedAppSource).not.toMatch(/import\s+WorkbenchTabContent\s+from/); - expect(aiChatPanelSource).toContain("import '../styles/v2-theme-ai.css';"); - expect(workbenchContentSource).toContain("import '../styles/v2-theme-workbench.css';"); - expect(dataGridSource).toContain("import '../styles/v2-theme-workbench.css';"); - expect(workbenchThemeSource).not.toContain('.gn-v2-ai-panel'); - expect(aiThemeSource).not.toContain('.gn-v2-data-grid-column-quick-find'); - }); - - it('keeps the v2 AI header actions inside the frameless window edge', () => { - expect(aiThemeSource).toMatch( - /body\[data-ui-version="v2"\] \.gn-v2-ai-header-top \{[^}]*box-sizing: border-box;/s, - ); - }); -}); diff --git a/frontend/src/sidebarTreeScrollCss.test.ts b/frontend/src/sidebarTreeScrollCss.test.ts deleted file mode 100644 index 5350f525..00000000 --- a/frontend/src/sidebarTreeScrollCss.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { readFileSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const appCss = readFileSync(path.resolve(__dirname, './App.css'), 'utf8'); - -describe('sidebar tree horizontal scroll css', () => { - it('lets sidebar tree titles keep their full content width for horizontal scrolling', () => { - expect(appCss).toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-list-holder,\s*\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-list-holder-inner\s*\{[^}]*min-width:\s*100%;/s); - expect(appCss).not.toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-list-holder,\s*\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-list-holder-inner\s*\{[^}]*max-content/s); - - expect(appCss).toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-treenode\s*\{[^}]*width:\s*auto;[^}]*min-width:\s*100%;/s); - expect(appCss).not.toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-treenode\s*\{[^}]*width:\s*max-content/s); - - expect(appCss).toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-node-content-wrapper\s*\{[^}]*width:\s*max-content\s*!important;[^}]*min-width:\s*100%;/s); - expect(appCss).not.toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-node-content-wrapper\s*\{[^}]*min-width:\s*0;/s); - - expect(appCss).toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-switcher\s*\{[^}]*width:\s*24px;[^}]*min-width:\s*24px;/s); - expect(appCss).toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-iconEle\s*\{[^}]*width:\s*16px;[^}]*min-width:\s*16px;/s); - - expect(appCss).toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-title\s*\{[^}]*min-width:\s*max-content;[^}]*overflow:\s*visible;[^}]*text-overflow:\s*clip;/s); - expect(appCss).not.toMatch(/\.sidebar-tree-scroll-shell\s+\.ant-tree\s+\.ant-tree-title\s*\{[^}]*ellipsis/s); - }); -}); diff --git a/frontend/src/testPolicy-sourceReadingBaseline.json b/frontend/src/testPolicy-sourceReadingBaseline.json new file mode 100644 index 00000000..084f79c3 --- /dev/null +++ b/frontend/src/testPolicy-sourceReadingBaseline.json @@ -0,0 +1,132 @@ +[ + "App.ai-panel-error-boundary.test.ts", + "App.tool-center.test.ts", + "components/AIChatPanel.message-boundary.test.tsx", + "components/AISettingsModal.edit-password.test.tsx", + "components/ConnectionModal.i18n.test.tsx", + "components/DataGrid.ddl.test.tsx", + "components/DataGrid.layout.test.tsx", + "components/DataSyncModal.entry-mode.test.ts", + "components/DataViewer.primary-key.test.tsx", + "components/DatabaseIcons.test.tsx", + "components/DriverManagerModal.i18n.test.tsx", + "components/DriverManagerModal.test.tsx", + "components/FindInDatabaseModal.test.tsx", + "components/ImportPreviewModal.test.tsx", + "components/JVMAuditViewer.test.tsx", + "components/JVMMonitoringDashboard.test.tsx", + "components/JVMOverview.test.tsx", + "components/LogPanel.test.tsx", + "components/QueryEditor.external-sql-save.test.tsx", + "components/QueryEditor.results-and-drop.test.tsx", + "components/QueryEditorResultsPanel.i18n.test.ts", + "components/QueryEditorToolbar.i18n.test.ts", + "components/QueryEditorToolbar.layout.test.tsx", + "components/QueryEditorTransaction.i18n.test.ts", + "components/RedisMonitor.polling.test.tsx", + "components/SQLExportOptionsDialog.test.tsx", + "components/SecurityUpdateBanner.i18n.test.tsx", + "components/SecurityUpdateIntroModal.i18n.test.tsx", + "components/SecurityUpdateProgressModal.i18n.test.tsx", + "components/SecurityUpdateSettingsModal.i18n.test.tsx", + "components/Sidebar.locate-toolbar.test.tsx", + "components/Sidebar.v2-metadata.test.tsx", + "components/SidebarConnectionGroupNewConnection.test.tsx", + "components/SidebarDamengTableCount.test.ts", + "components/SidebarDatabaseMenu.order.test.ts", + "components/SidebarNestedGroupMenu.test.ts", + "components/SnippetSettingsModal.i18n.test.tsx", + "components/TabManager.adaptive-width.test.ts", + "components/TabManager.background-task-detach.test.ts", + "components/TabManager.hover.test.tsx", + "components/TableCopy.i18n.test.tsx", + "components/TableDesignerSqlPreview.test.tsx", + "components/TableExportWorkbench.test.tsx", + "components/TriggerViewer.object-edit.test.tsx", + "components/WorkspaceHeavyModules.lazy.test.ts", + "components/ai/AIBuiltinToolsCatalog.test.tsx", + "components/ai/AIChatAttachmentStrip.i18n.test.tsx", + "components/ai/AIChatComposerActions.i18n.test.tsx", + "components/ai/AIChatContextPreview.i18n.test.tsx", + "components/ai/AIChatHeader.i18n.test.tsx", + "components/ai/AIChatInput.i18n.test.tsx", + "components/ai/AIChatPanelModeContent.test.tsx", + "components/ai/AIChatProviderModelSelect.i18n.test.tsx", + "components/ai/AIChatWelcome.i18n.test.tsx", + "components/ai/AIContextSelectorModal.test.tsx", + "components/ai/AIHistoryDrawer.i18n.test.tsx", + "components/ai/AIHistoryDrawer.test.tsx", + "components/ai/AIMCPArgumentHints.test.tsx", + "components/ai/AIMCPCommandDraftPreview.test.tsx", + "components/ai/AIMCPEnvHints.test.tsx", + "components/ai/AIMCPHTTPServerPanel.test.tsx", + "components/ai/AIMCPRemoteQuickStartPanel.test.tsx", + "components/ai/AIMCPServerFormPanel.test.tsx", + "components/ai/AIMCPServerValidationPanel.test.tsx", + "components/ai/AIMCPToolSchemaSummary.test.tsx", + "components/ai/AIMessageBubble.test.tsx", + "components/ai/AISettingsChoiceSections.test.tsx", + "components/ai/AISettingsPromptsSection.test.tsx", + "components/ai/AISettingsProvidersSection.test.tsx", + "components/ai/AISettingsSidebar.test.tsx", + "components/ai/AISettingsSkillsSection.test.tsx", + "components/ai/AISlashCommandMenu.test.tsx", + "components/ai/aiAppHealthInsights.test.ts", + "components/ai/aiChatAttachments.test.ts", + "components/ai/aiDatabaseBundleTools.i18n.test.ts", + "components/ai/aiDatabaseToolExecutor.i18n.test.ts", + "components/ai/aiMCPInsights.test.ts", + "components/ai/aiPromptInsights.test.ts", + "components/ai/aiProviderInsights.test.ts", + "components/ai/aiRedisTopologyInsights.test.ts", + "components/ai/aiRuntimeInsights.test.ts", + "components/ai/aiSafetyInsights.test.ts", + "components/ai/aiSettingsModalConfig.test.tsx", + "components/ai/aiSetupHealthInsights.test.ts", + "components/ai/aiSlashCommands.test.ts", + "components/ai/aiSnapshotInspectionToolExecutor.i18n.test.ts", + "components/ai/aiSystemContextMessages.test.ts", + "components/ai/mcpClientInstallPanelState.test.ts", + "components/ai/useAIChatContextBinding.test.tsx", + "components/ai/useAIChatLocalTools.test.tsx", + "components/ai/useAIChatRuntimeResources.test.tsx", + "components/ai/useAIChatStreamSubscription.test.tsx", + "components/audit/SqlAuditWorkbench.test.tsx", + "components/audit/SqlAuditWorkbench.wiring.test.ts", + "components/explain/ExplainGraphSidebar.test.ts", + "components/sidebarV2Utils.active-query.test.ts", + "components/tableDesignerSchemaSql.test.ts", + "i18n/catalog.test.ts", + "main.browserMock.test.ts", + "store.test.ts", + "utils/aiChatRuntime.test.ts", + "utils/aiChatSendShortcut.test.ts", + "utils/aiComposerNotice.test.ts", + "utils/aiTableSchemaTool.test.ts", + "utils/aiToolRegistry.test.ts", + "utils/appVersionDisplay.test.ts", + "utils/connectionExport.test.ts", + "utils/connectionModalPresentation.test.ts", + "utils/connectionTypeCatalog.test.ts", + "utils/dataGridWhereFilter.test.ts", + "utils/driverImportGuidance.test.ts", + "utils/exportProgress.test.ts", + "utils/jvmAiPlan.test.ts", + "utils/jvmDiagnosticPlan.test.ts", + "utils/jvmDiagnosticPresentation.test.ts", + "utils/jvmMonitoringPresentation.test.ts", + "utils/jvmResourcePresentation.test.ts", + "utils/mcpServerDraftSeed.test.ts", + "utils/mcpServerGuidance.test.ts", + "utils/mcpServerValidation.test.ts", + "utils/messagePublish.test.ts", + "utils/overlayZIndex.test.ts", + "utils/savedQueryPersistence.test.ts", + "utils/secureConfigBootstrap.test.ts", + "utils/securityUpdateRepairFlow.test.ts", + "utils/sqlAnalysisTab.test.ts", + "utils/sqlErrorSemantics.test.ts", + "utils/sqlFileTabDirty.test.ts", + "utils/sqlSnippetDefaults.test.ts", + "utils/triggerEditSql.test.ts" +] diff --git a/frontend/src/testPolicy.test.ts b/frontend/src/testPolicy.test.ts new file mode 100644 index 00000000..15b5c042 --- /dev/null +++ b/frontend/src/testPolicy.test.ts @@ -0,0 +1,79 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +/** + * 测试策略守卫:禁止新增「读取源码文件并断言其文本」型测试。 + * + * 背景:这类测试把源码当字符串读进来,断言它包含某段代码片段(甚至断言片段的字符下标顺序)。 + * 它们不执行被测代码,因此无法发现任何行为缺陷——实测把 isCloseShortcutInteractionBlocked + * 改为恒返回 false(守卫彻底失效)后,对应的守卫测试仍 9/9 通过;同时它们会因换行符、 + * 格式化、等价重构而误报,并使每次源码改动都被迫连带改测试。 + * + * 正确做法: + * - 验证行为 → 渲染组件或调用函数后断言其输出 + * - 验证全局不变式(如「所有 t() 引用的 key 都能解析」)→ 写成覆盖全仓库的扫描型用例, + * 见 src/i18n/keyResolution.test.ts、src/i18n/catalogIntegrity.test.ts + * - 验证架构约束(如「所有 Modal 必须走公共包装」)→ 同上,写成一条全局用例而非逐组件复制 + * + * 本守卫以基线名单记录存量债务:名单内的文件允许暂时保留,但**只能减少不能增加**。 + * 清理某个文件后请从 testPolicy-sourceReadingBaseline.json 中删除对应条目。 + */ + +// Windows 下 fileURLToPath 返回反斜杠路径,统一归一化为正斜杠以便与拼接结果比较 +const srcRoot = `${fileURLToPath(new URL('.', import.meta.url)).replace(/[\\/]+$/, '').split('\\').join('/')}/`; + +const baseline = new Set( + JSON.parse(readFileSync(`${srcRoot}testPolicy-sourceReadingBaseline.json`, 'utf8')) as string[], +); + +/** 经评审认可的全局不变式扫描用例:读源码是为了覆盖全仓库,而非锁定某个文件的实现文本 */ +const SANCTIONED_GLOBAL_SCANNERS = new Set([ + 'i18n/keyResolution.test.ts', + 'testPolicy.test.ts', +]); + +const collectTestFiles = (directory: string): string[] => readdirSync(directory) + .flatMap((entry) => { + const absolutePath = `${directory}/${entry}`; + if (statSync(absolutePath).isDirectory()) { + return /^(?:node_modules|dist)$/.test(entry) ? [] : collectTestFiles(absolutePath); + } + return /\.(?:test|spec)\.(?:ts|tsx)$/.test(entry) ? [absolutePath] : []; + }); + +/** + * 判定「从磁盘读取 .ts/.tsx 源码」。读 JSON 等数据文件不在禁止之列—— + * 被禁止的是把源码当字符串断言,而非读取测试夹具。 + */ +const readsSourceFromDisk = (source: string): boolean => ( + /\breaddirSync\b|\breadFileSync\b/.test(source) && /\.tsx?['"`]/.test(source) +); + +describe('test policy: no new source-text assertions', () => { + const testFiles = collectTestFiles(srcRoot.slice(0, -1)) + .map((file) => file.slice(srcRoot.length)); + + it('scans a plausible number of test files', () => { + expect(testFiles.length).toBeGreaterThan(300); + }); + + it('adds no new test that reads source files from disk', () => { + const offenders = testFiles.filter((file) => { + if (baseline.has(file) || SANCTIONED_GLOBAL_SCANNERS.has(file)) return false; + return readsSourceFromDisk(readFileSync(`${srcRoot}${file}`, 'utf8')); + }); + + expect(offenders).toEqual([]); + }); + + it('keeps the debt baseline free of stale entries', () => { + const present = new Set(testFiles); + const stale = [...baseline].filter((file) => { + if (!present.has(file)) return true; + return !readsSourceFromDisk(readFileSync(`${srcRoot}${file}`, 'utf8')); + }); + + expect(stale).toEqual([]); + }); +}); diff --git a/frontend/src/v2-theme.contrast.test.ts b/frontend/src/v2-theme.contrast.test.ts deleted file mode 100644 index db3acc3a..00000000 --- a/frontend/src/v2-theme.contrast.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { describe, expect, it } from 'vitest'; - -const source = readFileSync(new URL('./v2-theme.css', import.meta.url), 'utf8'); - -const readRuleBlock = (selector: string): string => { - const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = source.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`)); - if (!match?.[1]) throw new Error(`Missing CSS rule: ${selector}`); - return match[1]; -}; - -const readHexProperty = (block: string, property: string): string => { - const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = block.match(new RegExp(`${escaped}\\s*:\\s*(#[0-9a-f]{6})`, 'i')); - if (!match?.[1]) throw new Error(`Missing hexadecimal custom property: ${property}`); - return match[1]; -}; - -const relativeLuminance = (hex: string): number => { - const channels = [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16) / 255); - const [red, green, blue] = channels.map((channel) => ( - channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4 - )); - return 0.2126 * red + 0.7152 * green + 0.0722 * blue; -}; - -const contrastRatio = (foreground: string, background: string): number => { - const light = Math.max(relativeLuminance(foreground), relativeLuminance(background)); - const dark = Math.min(relativeLuminance(foreground), relativeLuminance(background)); - return (light + 0.05) / (dark + 0.05); -}; - -describe('v2 theme connection identity contrast', () => { - it('uses dedicated connected-state tokens and distinct Host/DB tab colors', () => { - const light = readRuleBlock('body[data-ui-version="v2"][data-theme="light"]'); - const dark = readRuleBlock('body[data-ui-version="v2"][data-theme="dark"]'); - const host = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-host'); - const database = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-database'); - const treeDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tree-status::before'); - - expect(host).toContain('color: var(--gn-info);'); - expect(database).toContain('color: var(--gn-accent);'); - expect(treeDot).toContain('width: 9px;'); - expect(treeDot).toContain('height: 9px;'); - expect(source).not.toContain('.gn-v2-live-dot'); - expect(source).toContain('background: var(--gn-status-connected);'); - - for (const [mode, block] of [['light', light], ['dark', dark]] as const) { - const panel2 = readHexProperty(block, '--gn-bg-panel-2'); - for (const property of ['--gn-info', '--gn-accent', '--gn-status-connected']) { - expect( - contrastRatio(readHexProperty(block, property), panel2), - `${mode} ${property} must contrast with --gn-bg-panel-2`, - ).toBeGreaterThanOrEqual(4.5); - } - } - }); -}); diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 471020f7..e307b24a 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -3204,6 +3204,7 @@ "common.named_item": "{{name}}", "common.ok": "OK", "common.refresh": "Aktualisieren", + "common.retry": "Erneut versuchen", "common.save": "Speichern", "common.search": "Suchen", "common.success": "Erfolg", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 965788f4..20596024 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -3204,6 +3204,7 @@ "common.named_item": "{{name}}", "common.ok": "OK", "common.refresh": "Refresh", + "common.retry": "Retry", "common.save": "Save", "common.search": "Search", "common.success": "Success", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index fe32b1de..281195a6 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -3204,6 +3204,7 @@ "common.named_item": "{{name}}", "common.ok": "OK", "common.refresh": "更新", + "common.retry": "再試行", "common.save": "保存", "common.search": "検索", "common.success": "成功", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 23443435..1a08a53f 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -3204,6 +3204,7 @@ "common.named_item": "{{name}}", "common.ok": "OK", "common.refresh": "Обновить", + "common.retry": "Повторить", "common.save": "Сохранить", "common.search": "Поиск", "common.success": "Успешно", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index bd5441d3..24e0a456 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -3204,6 +3204,7 @@ "common.named_item": "{{name}}", "common.ok": "确定", "common.refresh": "刷新", + "common.retry": "重试", "common.save": "保存", "common.search": "搜索", "common.success": "成功", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 0c953149..45df3444 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -3204,6 +3204,7 @@ "common.named_item": "{{name}}", "common.ok": "確定", "common.refresh": "重新整理", + "common.retry": "重試", "common.save": "儲存", "common.search": "搜尋", "common.success": "成功",