mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-25 16:00:28 +08:00
367 lines
14 KiB
TypeScript
367 lines
14 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
||
import { describe, expect, it } from 'vitest'
|
||
|
||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const
|
||
const catalogs = Object.fromEntries(locales.map((locale) => [
|
||
locale,
|
||
JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
|
||
])) as Record<typeof locales[number], Record<string, string>>
|
||
|
||
const sqlAnalysisWorkbenchSource = readFileSync(new URL('./explain/SqlAnalysisWorkbench.tsx', import.meta.url), 'utf8')
|
||
const explainWorkbenchSource = readFileSync(new URL('./explain/ExplainWorkbench.tsx', import.meta.url), 'utf8')
|
||
const slowQueryPanelSource = readFileSync(new URL('./explain/SlowQueryPanel.tsx', import.meta.url), 'utf8')
|
||
const explainGraphSource = readFileSync(new URL('./explain/ExplainGraph.tsx', import.meta.url), 'utf8')
|
||
const explainSidebarSource = readFileSync(new URL('./explain/ExplainSidebar.tsx', import.meta.url), 'utf8')
|
||
const slowQueryRailButtonSource = readFileSync(new URL('./sidebar/SlowQueryRailButton.tsx', import.meta.url), 'utf8')
|
||
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8')
|
||
|
||
const stripLineComments = (source: string): string => (
|
||
source.replace(/^\s*\/\/.*$/gm, '')
|
||
)
|
||
|
||
const sqlAnalysisWorkbenchRuntimeSource = stripLineComments(sqlAnalysisWorkbenchSource)
|
||
const explainWorkbenchRuntimeSource = stripLineComments(explainWorkbenchSource)
|
||
const slowQueryPanelRuntimeSource = stripLineComments(slowQueryPanelSource)
|
||
const explainGraphRuntimeSource = stripLineComments(explainGraphSource)
|
||
const explainSidebarRuntimeSource = stripLineComments(explainSidebarSource)
|
||
const slowQueryRailButtonRuntimeSource = stripLineComments(slowQueryRailButtonSource)
|
||
|
||
const placeholdersOf = (value: string): string[] => (
|
||
Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), (match) => match[1]).sort()
|
||
)
|
||
|
||
const requiredKeys = [
|
||
'sql_analysis.workbench.validation.sql_required',
|
||
'sql_analysis.workbench.alert.connection_missing_title',
|
||
'sql_analysis.workbench.alert.connection_missing_description',
|
||
'sql_analysis.workbench.title',
|
||
'sql_analysis.workbench.view.slow_query',
|
||
'sql_analysis.workbench.view.diagnose',
|
||
'sql_analysis.workbench.editor.placeholder',
|
||
'sql_analysis.workbench.editor.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.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.message.cleared',
|
||
'sql_analysis.slow_query.error.clear_failed',
|
||
'sql_analysis.slow_query.sort.duration',
|
||
'sql_analysis.slow_query.sort.rows_read',
|
||
'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.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.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.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.rows_read')",
|
||
"t('sql_analysis.slow_query.sort.recent')",
|
||
"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.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('record.sqlPreview')
|
||
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)
|
||
})
|
||
})
|
||
})
|
||
})
|