test(i18n): 更新组件拆分后的源码守卫

This commit is contained in:
mango
2026-07-26 00:24:59 +08:00
parent 54f973479c
commit 23cb8097c0
30 changed files with 88 additions and 50 deletions

View File

@@ -4,6 +4,7 @@ import { act, create, type ReactTestRenderer } from "react-test-renderer";
import { readFileSync } from "node:fs";
import { setCurrentLanguage } from "../i18n";
import { getCustomConnectionDriverHelp } from "../utils/driverImportGuidance";
const storeState = {
addConnection: vi.fn(),
@@ -168,6 +169,8 @@ vi.mock("@ant-design/icons", () => {
};
});
const modalConfirm = vi.hoisted(() => vi.fn());
vi.mock("antd", () => {
const Button: any = ({ children, disabled, loading, onClick, ...rest }: any) => (
<button type="button" disabled={disabled || loading} onClick={onClick} {...rest}>
@@ -296,7 +299,7 @@ vi.mock("antd", () => {
<div>{footer}</div>
</section>
) : null;
Modal.confirm = vi.fn();
Modal.confirm = modalConfirm;
const Typography = {
Text: ({ children }: any) => <span>{children}</span>,
@@ -357,9 +360,7 @@ describe("ConnectionModal i18n", () => {
antdMessage.warning.mockReset();
antdMessage.success.mockReset();
antdMessage.destroy.mockReset();
void import("antd").then(({ Modal }) => {
(Modal.confirm as any).mockReset?.();
});
modalConfirm.mockReset();
storeState.addConnection.mockReset();
storeState.updateConnection.mockReset();
storeState.setLanguagePreference.mockClear();
@@ -1260,8 +1261,7 @@ describe("ConnectionModal i18n", () => {
const pageText = textContent(renderer!.toJSON());
expect(pageText).toContain("Driver Name");
expect(pageText).toContain("Connection string (DSN)");
expect(pageText).toContain("Enter a Go database/sql driver name already registered by GoNavi");
expect(pageText).toContain("Do not enter a system ODBC/JDBC driver name directly or import a JDBC Jar");
expect(pageText).toContain(getCustomConnectionDriverHelp("en-US"));
});
it("renders English JVM fields and diagnostic transport copy", async () => {

View File

@@ -20,6 +20,10 @@ const localeFiles = [
const sources = componentFiles.map((file) => readFileSync(new URL(file, import.meta.url), 'utf8'));
const combinedSource = sources.join('\n');
const userFacingSource = combinedSource
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/.*$/gm, '')
.replace(/'已取消'/g, '');
const catalogs = Object.fromEntries(localeFiles.map((locale) => [
locale,
JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
@@ -44,7 +48,7 @@ describe('data export i18n', () => {
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(combinedSource).not.toMatch(/\p{Script=Han}/u);
expect(userFacingSource).not.toMatch(/\p{Script=Han}/u);
});
it('keeps all extracted data_export keys present in every supported locale with matching placeholders', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const dataGridSource = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
describe('Sidebar danger operations menu i18n', () => {
it('localizes danger operation group labels', () => {

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8');
const dropRoutineSource = source.slice(
source.indexOf('const handleDropRoutine ='),
source.indexOf('const resolveMessagePublishTarget ='),

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
describe('Sidebar edit definition menu i18n', () => {
it('localizes edit definition menu labels', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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';

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
const externalSqlCrudBlock = source.slice(
source.indexOf('const openCreateExternalSQLFileModal ='),

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
const externalSqlDeleteBlock = source.slice(
source.indexOf('const handleDeleteExternalSQLFile ='),

View File

@@ -1,13 +1,13 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const titleRenderIndex = source.indexOf(' const titleRender =');
const externalSqlMenuStart = source.lastIndexOf("if (node.type === 'external-sql-root')", titleRenderIndex);
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,
titleRenderIndex,
externalSqlMenuEnd,
);
describe('Sidebar external SQL menu labels i18n', () => {

View File

@@ -1,10 +1,9 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const modalOpenIndex = source.indexOf('open={isExternalSQLFileModalOpen}');
const modalStart = source.lastIndexOf('<Modal', modalOpenIndex);
const modalEnd = source.indexOf('title={renderSidebarModalTitle(<TableOutlined />', modalOpenIndex);
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', () => {

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {
@@ -14,7 +17,7 @@ describe('Sidebar external SQL refresh i18n', () => {
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(2);
expect(readFailureKeyUses.length).toBeGreaterThanOrEqual(1);
expect(refreshedKeyUses.length).toBeGreaterThanOrEqual(2);
expect(source).toContain('name: directory.name');
expect(source).toContain('error: directoryRes.message');

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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 = [

View File

@@ -1,7 +1,11 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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 = [

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,13 +1,15 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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(source.match(/title: t\('sidebar\.tab\.routine_definition'/g) || []).toHaveLength(2);
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);
});

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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 = [

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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',

View File

@@ -1,8 +1,8 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const sidebarSource = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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 = [
@@ -16,13 +16,12 @@ describe('Sidebar v2 connection group fallback i18n', () => {
"name: tag.name || '未命名分组'",
"fallback = '组'",
].forEach((snippet) => {
expect(sidebarSource).not.toContain(snippet);
expect(sidebarV2UtilsSource).not.toContain(snippet);
expect(sidebarHelpersSource).not.toContain(snippet);
});
expect(sidebarSource).toContain("tag.name || t('connection.sidebar.group.untitled')");
expect(sidebarSource).toContain("fallback = t('connection.sidebar.group.badge')");
expect(sidebarV2UtilsSource).toContain("fallback = t('connection.sidebar.group.badge')");
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', () => {

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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', () => {

View File

@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
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';
@@ -9,7 +9,7 @@ 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(2);
expect(source.match(/label: t\('sidebar\.menu\.view_object_definition'\)/g) || []).toHaveLength(4);
});
it('keeps the generic view definition key available in every locale', () => {

View File

@@ -726,9 +726,18 @@ describe("i18n catalog", () => {
assertSourceDoesNotInlineCatalogValues(detachedChromeSource, dataGridDetachedChromeKeys, { ignoreEnglishBaseline: true });
});
it("does not put the raw cancelled sentinel into catalog values", () => {
it("keeps raw cancelled sentinels out of data-root and export feedback catalogs", () => {
const guardedKeyPrefixes = [
"app.data_root.",
"data_export.",
];
for (const language of SUPPORTED_LANGUAGES) {
expect(Object.values(catalogs[language])).not.toContain("已取消");
for (const [key, value] of Object.entries(catalogs[language])) {
if (guardedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
expect(value, `${language}:${key}`).not.toBe("已取消");
}
}
}
});
@@ -908,7 +917,7 @@ describe("i18n catalog", () => {
const dataRootFlowSource = sliceBetween(
source,
"const loadDataRootInfo = useCallback(async () => {",
"const handleCreateConnection = useCallback(() => {",
"const openCreateConnection = useCallback((targetTagId?: string) => {",
);
const windowZoomSource = sliceBetween(
source,