"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { transformSourceCode } from '../../packages/plugin/src/core/astTransformer';
import type { DesignModeOptions } from '../../packages/plugin/src/types';
describe('astTransformer', () => {
const mockOptions: Required<DesignModeOptions> = {
enabled: true,
enableInProduction: false,
attributePrefix: 'data-source',
verbose: false,
exclude: ['node_modules'],
include: ['**/*.{js,jsx,ts,tsx}'],
};
beforeEach(() => {
vi.clearAllMocks();
});
describe('transformSourceCode', () => {
it('应该转换简单的JSX代码', () => {
const code = `
function App() {
return <div>Hello World</div>;
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
expect(result).toContain('data-source');
expect(result).toContain('div');
});
it('应该为JSX元素添加源码映射属性', () => {
const code = `
function App() {
return (
<div className="container">
<h1>Title</h1>
</div>
);
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toContain('data-source-element-id');
expect(result).toContain('data-source-position');
expect(result).toContain('data-source-info');
});
it('应该处理TypeScript代码', () => {
const code = `
interface Props {
title: string;
}
function App(props: Props) {
return <div>{props.title}</div>;
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
expect(result).toContain('data-source');
});
it('应该处理嵌套的JSX元素', () => {
const code = `
function App() {
return (
<div>
<header>
<nav>
<a href="/">Home</a>
</nav>
</header>
<main>
<section>
<h1>Content</h1>
</section>
</main>
</div>
);
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
// 应该为多个元素添加属性
const matches = result.match(/data-source-element-id/g);
expect(matches?.length).toBeGreaterThan(1);
});
it('应该处理带属性的JSX元素', () => {
const code = `
function App() {
return (
<div className="container" id="app">
<button onClick={() => {}} disabled>
Click me
</button>
</div>
);
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toContain('data-source');
expect(result).toContain('container');
});
it('应该处理函数组件', () => {
const code = `
const Button = ({ label }: { label: string }) => {
return <button>{label}</button>;
};
function App() {
return <Button label="Click" />;
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
expect(result).toContain('data-source');
});
it('应该处理类组件', () => {
const code = `
class App extends React.Component {
render() {
return <div>Hello</div>;
}
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
expect(result).toContain('data-source');
});
it('应该在转换失败时返回原始代码', () => {
const code = 'invalid syntax {{{{';
const result = transformSourceCode(code, 'test.tsx', mockOptions);
// 应该返回原始代码或处理后的代码
expect(result).toBeDefined();
});
it('应该使用自定义属性前缀', () => {
const customOptions: Required<DesignModeOptions> = {
...mockOptions,
attributePrefix: 'data-appdev',
};
const code = `
function App() {
return <div>Test</div>;
}
`;
const result = transformSourceCode(code, 'test.tsx', customOptions);
expect(result).toContain('data-appdev');
expect(result).not.toContain('data-source');
});
it('应该处理空代码', () => {
const code = '';
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
});
it('应该处理只有注释的代码', () => {
const code = `
// This is a comment
/* Another comment */
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
});
it('应该处理复杂的JSX表达式', () => {
const code = `
function App() {
const items = [1, 2, 3];
return (
<div>
{items.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
}
`;
const result = transformSourceCode(code, 'test.tsx', mockOptions);
expect(result).toBeDefined();
expect(result).toContain('data-source');
});
});
});

View File

@@ -0,0 +1,346 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { handleUpdate } from '../../packages/plugin/src/core/codeUpdater';
import * as fs from 'fs';
import type { IncomingMessage, ServerResponse } from 'http';
// Mock fs 模块
vi.mock('fs', async () => {
const actualFs = await vi.importActual<typeof import('fs')>('fs');
const mockFs = {
...actualFs,
statSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
existsSync: vi.fn(),
};
return {
...mockFs,
default: mockFs,
};
});
describe('codeUpdater', () => {
const mockRoot = '/test/project';
let mockReq: Partial<IncomingMessage>;
let mockRes: Partial<ServerResponse>;
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.statSync).mockReturnValue({
size: 1024,
isFile: () => true,
isDirectory: () => false,
} as any);
mockReq = {
method: 'POST',
on: vi.fn(),
};
mockRes = {
statusCode: 200,
setHeader: vi.fn(),
end: vi.fn(),
};
});
describe('handleUpdate', () => {
it('应该拒绝非POST请求', async () => {
mockReq.method = 'GET';
await handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
expect(mockRes.statusCode).toBe(405);
expect(mockRes.end).toHaveBeenCalledWith('Method Not Allowed');
});
it('应该处理样式更新请求', async () => {
const sourceCode = `
function App() {
return <div className="old-class">Content</div>;
}
`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(sourceCode);
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
const requestBody = JSON.stringify({
filePath: 'src/App.tsx',
line: 2,
column: 20,
newValue: 'new-class',
type: 'style',
});
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
// 模拟数据流
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
expect(fs.existsSync).toHaveBeenCalled();
expect(fs.readFileSync).toHaveBeenCalled();
});
it('应该处理内容更新请求', async () => {
const sourceCode = `
function App() {
return <div>Old Content</div>;
}
`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(sourceCode);
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
const requestBody = JSON.stringify({
filePath: 'src/App.tsx',
line: 2,
column: 20,
newValue: 'New Content',
type: 'content',
});
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
expect(fs.existsSync).toHaveBeenCalled();
expect(fs.readFileSync).toHaveBeenCalled();
});
it('应该处理文件不存在的情况', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const requestBody = JSON.stringify({
filePath: 'src/NonExistent.tsx',
line: 1,
column: 0,
newValue: 'test',
type: 'style',
});
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
expect(mockRes.statusCode).toBe(400);
const response = JSON.parse(mockRes.end?.mock.calls[0]?.[0] || '{}');
expect(response.success).toBe(false);
expect(response.message).toContain('File not found');
});
it('应该处理绝对路径', async () => {
const absolutePath = '/absolute/path/to/file.tsx';
const sourceCode = 'function App() { return <div>Test</div>; }';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(sourceCode);
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
const requestBody = JSON.stringify({
filePath: absolutePath,
line: 1,
column: 0,
newValue: 'test',
type: 'style',
});
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
expect(mockRes.statusCode).toBe(400);
const response = JSON.parse(mockRes.end?.mock.calls[0]?.[0] || '{}');
expect(response.success).toBe(false);
expect(response.message).toContain('Access denied');
});
it('应该处理相对路径', async () => {
const relativePath = 'src/App.tsx';
const sourceCode = 'function App() { return <div>Test</div>; }';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(sourceCode);
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
const requestBody = JSON.stringify({
filePath: relativePath,
line: 1,
column: 0,
newValue: 'test',
type: 'style',
});
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
// 应该解析为绝对路径
expect(fs.existsSync).toHaveBeenCalled();
});
it('应该处理无效的JSON请求体', async () => {
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
if (dataCallback!) {
dataCallback(Buffer.from('invalid json'));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
expect(mockRes.statusCode).toBe(500);
});
it('应该处理找不到匹配元素的情况', async () => {
const sourceCode = `
function App() {
return <div>Content</div>;
}
`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(sourceCode);
const requestBody = JSON.stringify({
filePath: 'src/App.tsx',
line: 999, // 不存在的行
column: 0,
newValue: 'test',
type: 'style',
});
let dataCallback: (chunk: Buffer) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
}) as any;
const updatePromise = handleUpdate(mockReq as IncomingMessage, mockRes as ServerResponse, mockRoot);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await updatePromise;
// 应该返回400错误因为找不到匹配的元素
const response = JSON.parse(mockRes.end?.mock.calls[0]?.[0] || '{}');
expect(response.success).toBe(false);
});
});
});

View File

@@ -0,0 +1,296 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { createServerMiddleware } from '../../packages/plugin/src/core/serverMiddleware';
import type { DesignModeOptions } from '../../packages/plugin/src/types';
// Mock fs 模块
vi.mock('fs', async () => {
const actualFs = await vi.importActual<typeof import('fs')>('fs');
const mockFs = {
...actualFs,
promises: {
...actualFs.promises,
access: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
stat: vi.fn(),
},
};
return {
...mockFs,
default: mockFs,
};
});
describe('serverMiddleware', () => {
const mockOptions: Required<DesignModeOptions> = {
enabled: true,
enableInProduction: false,
attributePrefix: 'data-source',
verbose: false,
exclude: ['node_modules'],
include: ['**/*.{js,jsx,ts,tsx}'],
};
const mockRootDir = '/test/project';
let mockReq: any;
let mockRes: any;
beforeEach(() => {
// 重置 mock
vi.clearAllMocks();
// 创建模拟的请求对象
mockReq = {
url: '',
method: 'GET',
on: vi.fn(),
};
// 创建模拟的响应对象
mockRes = {
statusCode: 200,
headers: {},
end: vi.fn(),
setHeader: vi.fn(),
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('createServerMiddleware', () => {
it('应该创建中间件函数', () => {
const middleware = createServerMiddleware(mockOptions, mockRootDir);
expect(typeof middleware).toBe('function');
});
it('应该处理健康检查请求', async () => {
mockReq.url = '/health';
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(200);
expect(mockRes.end).toHaveBeenCalled();
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.status).toBe('ok');
expect(responseData.plugin).toBe('@xagi/vite-plugin-design-mode');
expect(responseData.timestamp).toBeDefined();
});
it('应该处理获取源码请求 - 成功', async () => {
const elementId = 'src/App.tsx:4:5_div_test';
mockReq.url = `/get-source?elementId=${encodeURIComponent(elementId)}`;
const mockFileContent = 'import React from "react";\n\nfunction App() {\n return <div>Test</div>;\n}';
vi.mocked(fs.promises.access).mockResolvedValue(undefined);
vi.mocked(fs.promises.stat).mockResolvedValue({ mtime: new Date() } as any);
vi.mocked(fs.promises.readFile).mockResolvedValue(mockFileContent);
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(200);
expect(fs.promises.readFile).toHaveBeenCalled();
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.sourceInfo).toBeDefined();
expect(responseData.sourceInfo.fileName).toBe('src/App.tsx');
expect(responseData.sourceInfo.lineNumber).toBe(4);
expect(responseData.sourceInfo.columnNumber).toBe(5);
});
it('应该处理获取源码请求 - 缺少elementId参数', async () => {
mockReq.url = '/get-source';
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(400);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toContain('Missing elementId');
});
it('应该处理获取源码请求 - 无效的elementId格式', async () => {
mockReq.url = '/get-source?elementId=invalid';
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(400);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toContain('Invalid elementId');
});
it('应该处理修改源码请求 - 成功', async () => {
const elementId = 'src/App.tsx:4:5_div_test';
mockReq.url = '/modify-source';
mockReq.method = 'POST';
const mockFileContent = 'import React from "react";\n\nfunction App() {\n return <div className="old">Test</div>;\n}';
const mockUpdatedContent = 'import React from "react";\n\nfunction App() {\n return <div className="new">Test</div>;\n}';
vi.mocked(fs.promises.access).mockResolvedValue(undefined);
vi.mocked(fs.promises.readFile).mockResolvedValue(mockFileContent);
vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined);
// 模拟请求体
const requestBody = JSON.stringify({
elementId,
newStyles: 'new',
oldStyles: 'old',
});
let dataCallback: (chunk: any) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
});
const middleware = createServerMiddleware(mockOptions, mockRootDir);
const middlewarePromise = middleware(mockReq, mockRes);
// 模拟数据流
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await middlewarePromise;
await new Promise(resolve => setTimeout(resolve, 0));
expect(mockRes.statusCode).toBe(200);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.success).toBe(true);
});
it('应该处理修改源码请求 - 方法不允许', async () => {
mockReq.url = '/modify-source';
mockReq.method = 'GET';
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(405);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toContain('Method not allowed');
});
it('应该处理修改源码请求 - 缺少必需参数', async () => {
mockReq.url = '/modify-source';
mockReq.method = 'POST';
const requestBody = JSON.stringify({});
let dataCallback: (chunk: any) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
});
const middleware = createServerMiddleware(mockOptions, mockRootDir);
const middlewarePromise = middleware(mockReq, mockRes);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await middlewarePromise;
expect(mockRes.statusCode).toBe(400);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toContain('Missing required parameters');
});
it('应该处理404请求', async () => {
mockReq.url = '/unknown';
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(404);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toBe('Not found');
});
it('应该处理文件读取错误', async () => {
const elementId = 'src/App.tsx:4:5_div_test';
mockReq.url = `/get-source?elementId=${encodeURIComponent(elementId)}`;
vi.mocked(fs.promises.access).mockResolvedValue(undefined);
vi.mocked(fs.promises.readFile).mockRejectedValue(new Error('File not found'));
const middleware = createServerMiddleware(mockOptions, mockRootDir);
await middleware(mockReq, mockRes);
expect(mockRes.statusCode).toBe(500);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toBe('File not found');
});
it('应该处理文件写入错误', async () => {
const elementId = 'src/App.tsx:4:5_div_test';
mockReq.url = '/modify-source';
mockReq.method = 'POST';
const mockFileContent = 'import React from "react";\n\nfunction App() {\n return <div>Test</div>;\n}';
vi.mocked(fs.promises.access).mockResolvedValue(undefined);
vi.mocked(fs.promises.readFile).mockResolvedValue(mockFileContent);
vi.mocked(fs.promises.writeFile).mockRejectedValue(new Error('Permission denied'));
const requestBody = JSON.stringify({
elementId,
newStyles: 'new',
});
let dataCallback: (chunk: any) => void;
let endCallback: () => void;
mockReq.on = vi.fn((event: string, callback: any) => {
if (event === 'data') {
dataCallback = callback;
} else if (event === 'end') {
endCallback = callback;
}
});
const middleware = createServerMiddleware(mockOptions, mockRootDir);
const middlewarePromise = middleware(mockReq, mockRes);
if (dataCallback!) {
dataCallback(Buffer.from(requestBody));
}
if (endCallback!) {
endCallback();
}
await middlewarePromise;
await new Promise(resolve => setTimeout(resolve, 0));
expect(mockRes.statusCode).toBe(500);
const responseData = JSON.parse(mockRes.end.mock.calls[0][0]);
expect(responseData.error).toBe('Permission denied');
});
});
});

View File

@@ -0,0 +1,262 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createSourceMappingPlugin } from '../../packages/plugin/src/core/sourceMapper';
import type { DesignModeOptions } from '../../packages/plugin/src/types';
import * as babel from '@babel/standalone';
describe('sourceMapper', () => {
const mockOptions: Required<DesignModeOptions> = {
enabled: true,
enableInProduction: false,
attributePrefix: 'data-source',
verbose: false,
exclude: ['node_modules'],
include: ['**/*.{js,jsx,ts,tsx}'],
};
beforeEach(() => {
vi.clearAllMocks();
});
describe('createSourceMappingPlugin', () => {
it('应该创建Babel插件', () => {
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
expect(plugin).toBeDefined();
expect(plugin.visitor).toBeDefined();
expect(plugin.visitor.JSXOpeningElement).toBeDefined();
});
it('应该为JSX元素添加源码映射属性', () => {
const code = `
function App() {
return <div>Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source');
});
it('应该生成正确的elementId', () => {
const code = `
function App() {
return <div className="test">Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('src/App.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source-element-id');
});
it('应该添加位置信息属性', () => {
const code = `
function App() {
return <div>Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source-position');
});
it('应该添加完整的源码信息属性', () => {
const code = `
function App() {
return <div>Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source-info');
});
it('应该使用自定义属性前缀', () => {
const customOptions: Required<DesignModeOptions> = {
...mockOptions,
attributePrefix: 'data-appdev',
};
const code = `
function App() {
return <div>Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', customOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-appdev');
expect(result?.code).not.toContain('data-source');
});
it('应该处理嵌套的JSX元素', () => {
const code = `
function App() {
return (
<div>
<header>
<nav>Nav</nav>
</header>
</div>
);
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
// 应该为多个元素添加属性
const matches = result?.code.match(/data-source-element-id/g);
expect(matches?.length).toBeGreaterThan(1);
});
it('应该处理带id属性的元素', () => {
const code = `
function App() {
return <div id="app">Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source-element-id');
});
it('应该处理带className的元素', () => {
const code = `
function App() {
return <div className="container">Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source-element-id');
});
it('应该处理函数组件', () => {
const code = `
const Button = () => {
return <button>Click</button>;
};
function App() {
return <Button />;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
expect(result?.code).toContain('data-source');
});
it('应该处理没有位置信息的元素', () => {
const code = `
function App() {
return <div>Hello</div>;
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
// 即使没有位置信息,插件也应该正常工作
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
});
it('应该处理复杂的JSX结构', () => {
const code = `
function App() {
return (
<div className="app">
<header className="header">
<h1>Title</h1>
</header>
<main className="main">
<section>
<p>Content</p>
</section>
</main>
<footer className="footer">
<p>Footer</p>
</footer>
</div>
);
}
`;
const plugin = createSourceMappingPlugin('test.tsx', mockOptions);
const result = babel.transform(code, {
plugins: [plugin],
presets: ['react'],
});
expect(result?.code).toBeDefined();
// 应该为多个元素添加属性
const matches = result?.code.match(/data-source-element-id/g);
expect(matches?.length).toBeGreaterThan(3);
});
});
});

View File

@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest';
import { transformVueSfcTemplate } from '../../packages/plugin/src/core/vueSfcTransformer';
import type { DesignModeOptions } from '../../packages/plugin/src/types';
const options: Required<DesignModeOptions> = {
enabled: true,
enableInProduction: false,
attributePrefix: 'data-source',
verbose: false,
exclude: ['node_modules'],
include: ['src/**/*.{js,jsx,ts,tsx,vue}'],
enableBackup: false,
enableHistory: false,
framework: 'auto',
};
function decodeHtmlAttr(value: string): string {
return value
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}
describe('vueSfcTransformer', () => {
it('injects normalized source info fields with legacy compatibility aliases', () => {
const source = `<template>
<div class="wrap">
<h1>Hello Vue</h1>
</div>
</template>
<script setup lang="ts">
const message = 'ok';
</script>
`;
const transformed = transformVueSfcTemplate(source, 'src/pages/Home.vue', options);
const attrMatch = transformed.match(/data-source-info="([^"]+)"/);
expect(attrMatch).toBeTruthy();
const sourceInfo = JSON.parse(decodeHtmlAttr(attrMatch![1])) as Record<string, unknown>;
expect(sourceInfo.fileName).toBe('src/pages/Home.vue');
expect(sourceInfo.lineNumber).toBeTypeOf('number');
expect(sourceInfo.columnNumber).toBeTypeOf('number');
expect(sourceInfo.line).toBe(sourceInfo.lineNumber);
expect(sourceInfo.column).toBe(sourceInfo.columnNumber);
});
it('removes stale mapping attrs before reinjection', () => {
const source = `<template>
<div data-source-info="{&quot;fileName&quot;:&quot;old.vue&quot;}" data-source-position="1:1" data-source-element-id="old">
Hello
</div>
</template>`;
const transformed = transformVueSfcTemplate(source, 'src/pages/Home.vue', options);
const infoAttrs = transformed.match(/data-source-info=/g) ?? [];
expect(infoAttrs.length).toBe(1);
expect(transformed).not.toContain('old.vue');
});
});

View File

@@ -0,0 +1,60 @@
import { describe, it, expect } from 'vitest';
import { applyVueSfcTemplateUpdate } from '../../packages/plugin/src/core/vueSfcUpdater';
function getLineColumnByToken(source: string, token: string): { line: number; column: number } {
const index = source.indexOf(token);
if (index < 0) {
throw new Error(`Token not found: ${token}`);
}
const prefix = source.slice(0, index);
const lines = prefix.split('\n');
return {
line: lines.length,
column: (lines[lines.length - 1] ?? '').length + 1,
};
}
describe('vueSfcUpdater', () => {
it('updates static class in Vue template', () => {
const source = `<template>
<section>
<h1 class="title">Hello Vue</h1>
</section>
</template>
<script setup lang="ts">
const noop = true;
</script>
`;
const { line, column } = getLineColumnByToken(source, '<h1');
const updated = applyVueSfcTemplateUpdate(source, {
lineNumber: line,
columnNumber: column,
type: 'style',
newValue: 'title text-2xl',
});
expect(updated).toContain('<h1 class="title text-2xl">Hello Vue</h1>');
});
it('updates static text in Vue template', () => {
const source = `<template>
<section>
<p>Hello Vue</p>
</section>
</template>
<script setup>
const noop = true;
</script>
`;
const { line, column } = getLineColumnByToken(source, '<p');
const updated = applyVueSfcTemplateUpdate(source, {
lineNumber: line,
columnNumber: column,
type: 'content',
originalValue: 'Hello Vue',
newValue: 'Hello Agent',
});
expect(updated).toContain('<p>Hello Agent</p>');
});
});