test(ai): 改用行为断言验证代码块依赖边界

This commit is contained in:
mango
2026-07-29 18:46:04 +08:00
parent 61358b1a62
commit caba7b8cbb

View File

@@ -1,27 +1,133 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import React from 'react';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { describe, expect, it, vi } from 'vitest';
const source = readFileSync(new URL('./AIMessageCodeBlock.tsx', import.meta.url), 'utf8');
const viteConfigSource = readFileSync(new URL('../../../../vite.config.ts', import.meta.url), 'utf8');
import viteConfig from '../../../../vite.config';
import { buildOverlayWorkbenchTheme } from '../../../utils/overlayWorkbenchTheme';
import { AIMessageCodeBlock } from './AIMessageCodeBlock';
const dependencyMocks = vi.hoisted(() => ({
completeRegistryLoad: vi.fn(),
mermaidInitialize: vi.fn(),
mermaidLoad: vi.fn(),
mermaidRender: vi.fn(async () => ({ svg: '<svg>diagram</svg>' })),
prismLightLoad: vi.fn(),
registerLanguage: vi.fn(),
}));
vi.mock('react-syntax-highlighter', () => {
dependencyMocks.completeRegistryLoad();
return { default: () => null };
});
vi.mock('react-syntax-highlighter/dist/esm/prism-light', () => {
dependencyMocks.prismLightLoad();
return {
default: Object.assign(() => null, {
registerLanguage: dependencyMocks.registerLanguage,
}),
};
});
vi.mock('mermaid', () => {
dependencyMocks.mermaidLoad();
return {
default: {
initialize: dependencyMocks.mermaidInitialize,
render: dependencyMocks.mermaidRender,
},
};
});
vi.mock('antd', () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => children,
message: { error: vi.fn() },
}));
vi.mock('@ant-design/icons', () => ({
CheckOutlined: () => null,
CopyOutlined: () => null,
PlayCircleOutlined: () => null,
}));
vi.mock('../../common/ResizableDraggableModal', () => ({
default: Object.assign(() => null, { confirm: vi.fn() }),
}));
const codeHighlightDependencies = [
'react-syntax-highlighter/dist/esm/prism-light',
'react-syntax-highlighter/dist/esm/languages/prism/bash',
'react-syntax-highlighter/dist/esm/languages/prism/css',
'react-syntax-highlighter/dist/esm/languages/prism/diff',
'react-syntax-highlighter/dist/esm/languages/prism/go',
'react-syntax-highlighter/dist/esm/languages/prism/ini',
'react-syntax-highlighter/dist/esm/languages/prism/java',
'react-syntax-highlighter/dist/esm/languages/prism/javascript',
'react-syntax-highlighter/dist/esm/languages/prism/json',
'react-syntax-highlighter/dist/esm/languages/prism/jsx',
'react-syntax-highlighter/dist/esm/languages/prism/markdown',
'react-syntax-highlighter/dist/esm/languages/prism/markup',
'react-syntax-highlighter/dist/esm/languages/prism/php',
'react-syntax-highlighter/dist/esm/languages/prism/python',
'react-syntax-highlighter/dist/esm/languages/prism/ruby',
'react-syntax-highlighter/dist/esm/languages/prism/rust',
'react-syntax-highlighter/dist/esm/languages/prism/sql',
'react-syntax-highlighter/dist/esm/languages/prism/toml',
'react-syntax-highlighter/dist/esm/languages/prism/tsx',
'react-syntax-highlighter/dist/esm/languages/prism/typescript',
'react-syntax-highlighter/dist/esm/languages/prism/yaml',
'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus',
'react-syntax-highlighter/dist/esm/styles/prism/vs',
];
const renderCodeBlock = (className: string, children: string) => React.createElement(AIMessageCodeBlock, {
className,
children,
darkMode: false,
overlayTheme: buildOverlayWorkbenchTheme(false),
});
describe('AIMessageCodeBlock dependency boundary', () => {
it('does not pull the complete syntax-highlighter language registry into the AI panel chunk', () => {
expect(source).not.toMatch(/from\s+['"]react-syntax-highlighter['"]/);
expect(source).toMatch(/react-syntax-highlighter\/dist\/esm\/prism-light/);
it('loads the lightweight syntax highlighter without the complete language registry', () => {
expect(dependencyMocks.completeRegistryLoad).not.toHaveBeenCalled();
expect(dependencyMocks.prismLightLoad).toHaveBeenCalledOnce();
expect(dependencyMocks.registerLanguage).toHaveBeenCalledTimes(20);
});
it('loads Mermaid only when a Mermaid fenced block is rendered', () => {
expect(source).not.toMatch(/^import\s+mermaid\s+from\s+['"]mermaid['"];?$/m);
expect(source).toMatch(/await\s+import\(['"]mermaid['"]\)/);
});
it('loads Mermaid only when a Mermaid fenced block is rendered', async () => {
let renderer: ReactTestRenderer | undefined;
let mermaidContainer: { innerHTML: string } | undefined;
it('pre-bundles every static syntax-highlighter entry before Wails opens the panel', () => {
const syntaxHighlighterImports = Array.from(source.matchAll(
/from\s+['"](react-syntax-highlighter\/dist\/esm\/[^'"]+)['"]/g,
)).map((match) => match[1]);
try {
act(() => {
renderer = create(renderCodeBlock('language-sql', 'SELECT 1;'), {
createNodeMock: (element) => {
if (element.type === 'div' && element.props.className === 'ai-mermaid-container') {
mermaidContainer = { innerHTML: '' };
return mermaidContainer;
}
return {};
},
});
});
expect(dependencyMocks.mermaidLoad).not.toHaveBeenCalled();
for (const dependency of syntaxHighlighterImports) {
expect(viteConfigSource).toContain(`'${dependency}'`);
await act(async () => {
renderer?.update(renderCodeBlock('language-mermaid', 'graph TD; A-->B;'));
});
await vi.waitFor(() => {
expect(dependencyMocks.mermaidLoad).toHaveBeenCalledOnce();
expect(dependencyMocks.mermaidInitialize).toHaveBeenCalledWith({ startOnLoad: false, theme: 'default' });
expect(dependencyMocks.mermaidRender).toHaveBeenCalledWith(expect.stringMatching(/^mermaid-/), 'graph TD; A-->B;');
expect(mermaidContainer?.innerHTML).toBe('<svg>diagram</svg>');
});
} finally {
act(() => renderer?.unmount());
}
});
it('pre-bundles every static syntax-highlighter dependency', () => {
const includedDependencies = viteConfig.optimizeDeps?.include || [];
expect(includedDependencies).toEqual(expect.arrayContaining(codeHighlightDependencies));
});
});