"添加前端模板和运行代码模块"
This commit is contained in:
214
qiming-vite-plugin-design-mode/test/core/astTransformer.test.ts
Normal file
214
qiming-vite-plugin-design-mode/test/core/astTransformer.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
346
qiming-vite-plugin-design-mode/test/core/codeUpdater.test.ts
Normal file
346
qiming-vite-plugin-design-mode/test/core/codeUpdater.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
262
qiming-vite-plugin-design-mode/test/core/sourceMapper.test.ts
Normal file
262
qiming-vite-plugin-design-mode/test/core/sourceMapper.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/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="{"fileName":"old.vue"}" 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');
|
||||
});
|
||||
});
|
||||
@@ -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>');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user