Files
MyGoNavi/frontend/src/components/SnippetSettingsModal.i18n.test.tsx
Syngnat 8e3a183abb ♻️ refactor(frontend-test): 剥离混合型测试中的寄生源码文本断言
- 基于 TypeScript AST 精确识别 readFileSync 派生变量及其断言,仅删除不改写保留代码
- 断言全为源码文本的 it() 块整块移除,混合块中只剔除单条源码断言
- 清理随之失效的变量声明、空 it()/describe() 与无用 import,6 个文件因无用例残留而删除
- 累计移除 1283 处代码区间、8270 行,守卫基线由 130 条收敛至 12 条
- 全量测试失败集合无新增,TabManager 悬浮信息用例的 CRLF 误报一并消除

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:52:35 +08:00

432 lines
14 KiB
TypeScript

import React from 'react';
import { act, create } from 'react-test-renderer';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { I18nProvider } from '../i18n/provider';
import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
import SnippetSettingsModal from './SnippetSettingsModal';
const storeState = vi.hoisted(() => ({
sqlSnippets: [] as Array<Record<string, unknown>>,
saveSqlSnippet: vi.fn(),
deleteSqlSnippet: vi.fn(),
resetBuiltinSqlSnippet: vi.fn(),
}));
const messageApi = vi.hoisted(() => ({
warning: vi.fn(),
success: vi.fn(),
}));
vi.mock('../store', () => ({
useStore: (selector: (state: typeof storeState) => unknown) => selector(storeState),
}));
vi.mock('../i18n/runtime', () => ({
syncLanguageRuntime: vi.fn(async () => undefined),
}));
vi.mock('antd', async () => {
const React = await import('react');
const Button = ({
children,
icon,
onClick,
...props
}: {
children?: React.ReactNode;
icon?: React.ReactNode;
onClick?: () => void;
}) => React.createElement('button', { ...props, onClick }, icon, children);
const Input = ({
value,
onChange,
placeholder,
...props
}: {
value?: string;
onChange?: (event: { target: { value: string } }) => void;
placeholder?: string;
}) => React.createElement('input', {
...props,
value,
placeholder,
onChange: (event: { target: { value: string } }) => onChange?.(event),
});
Input.TextArea = ({
value,
onChange,
placeholder,
children,
...props
}: {
value?: string;
onChange?: (event: { target: { value: string } }) => void;
placeholder?: string;
children?: React.ReactNode;
}) => React.createElement('textarea', {
...props,
value,
placeholder,
onChange: (event: { target: { value: string } }) => onChange?.(event),
}, children);
const List = ({
dataSource,
renderItem,
}: {
dataSource: unknown[];
renderItem: (item: unknown) => React.ReactNode;
}) => React.createElement(
'div',
null,
dataSource.map((item, index) => React.createElement(React.Fragment, { key: index }, renderItem(item))),
);
List.Item = ({
children,
...props
}: {
children?: React.ReactNode;
}) => React.createElement('div', props, children);
const Tag = ({
children,
...props
}: {
children?: React.ReactNode;
}) => React.createElement('span', props, children);
const Popconfirm = ({
title,
description,
children,
}: {
title?: React.ReactNode;
description?: React.ReactNode;
children?: React.ReactNode;
}) => React.createElement('div', null, title, description, children);
const Collapse = ({
items,
}: {
items?: Array<{ key: string; label: React.ReactNode; children: React.ReactNode }>;
}) => React.createElement(
'div',
null,
items?.map((item) => React.createElement('section', { key: item.key }, item.label, item.children)),
);
const Typography = {
Text: ({
children,
...props
}: {
children?: React.ReactNode;
}) => React.createElement('code', props, children),
};
return {
Modal: ({
open,
title,
children,
}: {
open?: boolean;
title?: React.ReactNode;
children?: React.ReactNode;
}) => (open ? React.createElement('div', null, title, children) : null),
Button,
Input,
List,
Tag,
Popconfirm,
message: messageApi,
Collapse,
Typography,
};
});
vi.mock('@ant-design/icons', async () => {
const React = await import('react');
return {
PlusOutlined: () => React.createElement('span', null, 'plus'),
DeleteOutlined: () => React.createElement('span', null, 'delete'),
UndoOutlined: () => React.createElement('span', null, 'undo'),
SaveOutlined: () => React.createElement('span', null, 'save'),
CodeOutlined: () => React.createElement('span', null, 'code'),
};
});
const overlayTheme: OverlayWorkbenchTheme = {
isDark: false,
shellBg: '#fff',
shellBorder: '1px solid #eee',
shellShadow: 'none',
shellBackdropFilter: 'none',
sectionBg: '#fff',
sectionBorder: '1px solid #eee',
mutedText: '#666',
titleText: '#111',
iconBg: '#f5f5f5',
iconColor: '#1677ff',
hoverBg: '#f5f5f5',
selectedBg: '#e6f4ff',
selectedText: '#1677ff',
divider: '#eee',
};
const renderModal = async (props: Partial<React.ComponentProps<typeof SnippetSettingsModal>> = {}) => {
let renderer: ReturnType<typeof create>;
await act(async () => {
renderer = create(
<I18nProvider
preference="en-US"
systemLanguages={['en-US']}
onPreferenceChange={() => undefined}
>
<SnippetSettingsModal
open
onClose={() => undefined}
darkMode={false}
overlayTheme={overlayTheme}
{...props}
/>
</I18nProvider>,
);
});
return renderer!;
};
const getText = (node: any): string => (
(node.children || [])
.map((child: any) => (typeof child === 'string' ? child : getText(child)))
.join('')
);
const getJsonText = (node: any): string => {
if (!node) return '';
if (typeof node === 'string') return node;
if (Array.isArray(node)) return node.map((item) => getJsonText(item)).join('');
return (node.children || []).map((child: any) => getJsonText(child)).join('');
};
describe('SnippetSettingsModal i18n', () => {
beforeEach(() => {
storeState.sqlSnippets = [];
storeState.saveSqlSnippet.mockReset();
storeState.deleteSqlSnippet.mockReset();
storeState.resetBuiltinSqlSnippet.mockReset();
messageApi.warning.mockReset();
messageApi.success.mockReset();
});
it('lets the tool center provide the title when embedded', async () => {
const renderer = await renderModal({ embedded: true });
const root = renderer.root;
expect(() => root.findByProps({ className: 'gn-embedded-modal-header' })).toThrow();
expect(getJsonText(renderer.toJSON())).toContain('Select a snippet on the left to edit, or click "New Snippet"');
const standaloneRenderer = await renderModal();
const standaloneText = getJsonText(standaloneRenderer.toJSON());
expect(standaloneText).toContain('Snippet Management');
expect(standaloneText).toContain('Manage SQL snippets and prefix completion.');
});
it('renders the modal shell in English and localizes save validation feedback', async () => {
const renderer = await renderModal();
const initialText = getJsonText(renderer.toJSON());
expect(initialText).toContain('Snippet Management');
expect(initialText).toContain('Manage SQL snippets and prefix completion.');
expect(initialText).toContain('Snippet List');
expect(initialText).toContain('New Snippet');
expect(initialText).toContain('Select a snippet on the left to edit, or click "New Snippet"');
const newButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('New Snippet'))[0];
await act(async () => {
newButton.props.onClick();
});
const editorText = getJsonText(renderer.toJSON());
expect(editorText).toContain('Save');
expect(editorText).toContain('Close');
expect(editorText).toContain('Prefix');
expect(editorText).toContain('Name');
expect(editorText).toContain('Description (optional)');
expect(editorText).toContain('Snippet Body');
expect(editorText).toContain('Snippet syntax notes (editable)');
expect(editorText).toContain('Placeholder syntax reference');
expect(editorText).toContain('Built-in variables (auto-replaced when expanded):');
expect(editorText).toContain('Example: SELECT ${1:column_name} FROM ${2:table_name}');
const saveButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('Save'))[0];
await act(async () => {
saveButton.props.onClick();
});
expect(messageApi.warning).toHaveBeenCalledWith('Prefix is required');
});
it('renders a bounded content region and fixed action row for long syntax help', async () => {
const renderer = await renderModal();
const newButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('New Snippet'))[0];
await act(async () => {
newButton.props.onClick();
});
const contentRegion = renderer.root.findByProps({ 'data-sql-snippet-content-region': 'true' });
const editorPanelScrollRegion = renderer.root.findByProps({ 'data-sql-snippet-editor-panel-scroll-region': 'true' });
const editorScrollRegion = renderer.root.findByProps({ 'data-sql-snippet-editor-scroll-region': 'true' });
const syntaxReferenceScrollRegion = renderer.root.findByProps({ 'data-sql-snippet-syntax-reference-scroll-region': 'true' });
const actionRow = renderer.root.findByProps({ 'data-sql-snippet-action-row': 'true' });
expect(contentRegion.props.style).toMatchObject({
flex: '1 1 420px',
minHeight: 0,
overflow: 'hidden',
});
expect(editorPanelScrollRegion.props.style).toMatchObject({
height: '100%',
minHeight: 0,
overflowY: 'auto',
overflowX: 'hidden',
overscrollBehavior: 'contain',
});
expect(editorScrollRegion.props.style).toMatchObject({
flex: '0 0 auto',
minHeight: 0,
marginTop: 10,
});
expect(syntaxReferenceScrollRegion.props.style).toMatchObject({
maxHeight: 'min(220px, 32vh)',
overflowY: 'auto',
overflowX: 'hidden',
overscrollBehavior: 'contain',
});
expect(actionRow.props.style).toMatchObject({
flex: '0 0 auto',
gap: 10,
justifyContent: 'flex-end',
paddingTop: 8,
marginTop: 8,
});
});
it('uses the full embedded body height and keeps the action row outside the scrollable content', async () => {
const renderer = await renderModal({ embedded: true });
const newButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('New Snippet'))[0];
await act(async () => {
newButton.props.onClick();
});
const embeddedBody = renderer.root.findByProps({ className: 'gn-embedded-modal-body' });
const contentRegion = renderer.root.findByProps({ 'data-sql-snippet-content-region': 'true' });
const actionRow = renderer.root.findByProps({ 'data-sql-snippet-action-row': 'true' });
expect(embeddedBody.props.style).toMatchObject({
height: '100%',
maxHeight: '100%',
minHeight: 0,
overflow: 'hidden',
});
expect(contentRegion.props.style).toMatchObject({
flex: '1 1 0',
minHeight: 0,
overflow: 'hidden',
});
expect(actionRow.props.style).toMatchObject({
flex: '0 0 auto',
gap: 8,
paddingTop: 12,
marginTop: 0,
});
});
it('uses a flat master-detail workspace only when embedded', async () => {
const embeddedRenderer = await renderModal({ embedded: true, onBack: () => undefined });
const embeddedContent = embeddedRenderer.root.findByProps({ 'data-sql-snippet-content-region': 'true' });
const embeddedMaster = embeddedRenderer.root.findByProps({ 'data-sql-snippet-master-panel': 'true' });
expect(embeddedContent.props.style).toMatchObject({
gap: 0,
borderTop: overlayTheme.sectionBorder,
fontFamily: 'var(--gn-font-sans)',
});
expect(embeddedMaster.props.style).toMatchObject({
width: 196,
borderRadius: 0,
border: 'none',
borderRight: overlayTheme.sectionBorder,
background: 'transparent',
});
const newButton = embeddedRenderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('New Snippet'))[0];
await act(async () => {
newButton.props.onClick();
});
const embeddedEditor = embeddedRenderer.root.findByProps({ 'data-sql-snippet-editor-panel-scroll-region': 'true' });
const syntaxHelpEditor = embeddedRenderer.root.findByProps({ 'data-sql-snippet-syntax-help-editor': 'true' });
expect(embeddedEditor.props.style).toMatchObject({
padding: '0 4px 0 16px',
borderRadius: 0,
border: 'none',
background: 'transparent',
});
expect(syntaxHelpEditor.props.style).toMatchObject({
fontFamily: 'var(--gn-font-sans)',
});
const embeddedActionLabels = embeddedRenderer.root
.findAll((node: any) => node.type === 'button')
.map((node: any) => getText(node));
const backIndex = embeddedActionLabels.findIndex((label) => label.includes('Back'));
const closeIndex = embeddedActionLabels.findIndex((label) => label.includes('Close'));
const saveIndex = embeddedActionLabels.findIndex((label) => label.includes('Save'));
expect(backIndex).toBeGreaterThan(-1);
expect(backIndex).toBeLessThan(closeIndex);
expect(closeIndex).toBeLessThan(saveIndex);
const standaloneRenderer = await renderModal();
const standaloneMaster = standaloneRenderer.root.findByProps({ 'data-sql-snippet-master-panel': 'true' });
expect(standaloneMaster.props.style).toMatchObject({
width: 220,
borderRadius: 14,
border: overlayTheme.sectionBorder,
background: overlayTheme.sectionBg,
});
});
it('uses compact action buttons so the footer does not consume editor height', async () => {
const renderer = await renderModal({ embedded: true, onBack: () => undefined });
const newButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('New Snippet'))[0];
await act(async () => {
newButton.props.onClick();
});
const saveButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('Save'))[0];
const closeButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('Close'))[0];
const backButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('Back'))[0];
expect(saveButton.props.size).toBe('middle');
expect(saveButton.props.style).toMatchObject({ minWidth: 84 });
expect(closeButton.props.size).toBe('middle');
expect(closeButton.props.style).toMatchObject({ minWidth: 84 });
expect(backButton.props.size).toBe('middle');
expect(backButton.props.style).toMatchObject({ minWidth: 96 });
});
});