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

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,185 @@
# 测试文档
本目录包含 `@xagi/vite-plugin-design-mode` 插件的单元测试。
## 测试结构
```
test/
├── index.test.ts # 主插件入口测试
├── core/
│ ├── serverMiddleware.test.ts # 服务器中间件测试
│ ├── astTransformer.test.ts # AST转换器测试
│ ├── codeUpdater.test.ts # 代码更新器测试
│ └── sourceMapper.test.ts # 源码映射器测试
└── utils/
└── babelHelpers.test.ts # Babel辅助函数测试
```
## 运行测试
### 运行所有测试
```bash
npm test
# 或
pnpm test
```
### 运行测试(单次运行,不监听)
```bash
npm run test:run
```
### 运行测试并生成覆盖率报告
```bash
npm run test:coverage
```
覆盖率报告将生成在 `coverage/` 目录中。
### 使用UI界面运行测试
```bash
npm run test:ui
```
这将打开一个交互式的测试UI界面。
## 测试覆盖范围
### 核心功能测试
1. **主插件入口 (`index.test.ts`)**
- 插件初始化
- 配置选项处理
- Vite hooks 集成
- 文件过滤逻辑
2. **服务器中间件 (`serverMiddleware.test.ts`)**
- 健康检查端点
- 获取源码端点
- 修改源码端点
- 错误处理
3. **AST转换器 (`astTransformer.test.ts`)**
- JSX代码转换
- 源码映射属性注入
- TypeScript支持
- 嵌套元素处理
4. **代码更新器 (`codeUpdater.test.ts`)**
- 样式更新
- 内容更新
- 文件路径处理
- 错误处理
5. **源码映射器 (`sourceMapper.test.ts`)**
- Babel插件创建
- 元素ID生成
- 位置信息注入
- 自定义属性前缀
6. **Babel辅助函数 (`babelHelpers.test.ts`)**
- React组件名识别
- JSX元素名称提取
- 属性值提取
- 位置字符串处理
## 编写新测试
### 测试文件命名
- 测试文件应以 `.test.ts``.spec.ts` 结尾
- 测试文件应放在与被测试文件相同的目录结构中
### 测试结构
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { functionToTest } from '../../src/module';
describe('模块名称', () => {
beforeEach(() => {
// 每个测试前的设置
vi.clearAllMocks();
});
describe('功能描述', () => {
it('应该执行预期行为', () => {
// 测试代码
expect(result).toBe(expected);
});
});
});
```
### Mock使用
使用 Vitest 的 `vi.mock()` 来模拟依赖:
```typescript
vi.mock('fs', () => {
return {
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
};
});
```
### 异步测试
使用 `async/await` 处理异步操作:
```typescript
it('应该处理异步操作', async () => {
const result = await asyncFunction();
expect(result).toBeDefined();
});
```
## 测试最佳实践
1. **测试隔离**: 每个测试应该独立运行,不依赖其他测试的状态
2. **清晰命名**: 测试描述应该清楚地说明测试的目的
3. **覆盖边界情况**: 测试正常流程、错误情况和边界条件
4. **Mock外部依赖**: 使用Mock来隔离被测试的代码
5. **保持测试简单**: 每个测试应该只测试一个功能点
6. **使用描述性的断言**: 使用有意义的错误消息
## 持续集成
测试可以在CI/CD流程中自动运行
```yaml
# GitHub Actions 示例
- name: Run tests
run: npm run test:run
- name: Generate coverage
run: npm run test:coverage
```
## 故障排除
### 测试失败
1. 检查测试环境是否正确配置
2. 确认所有依赖已安装
3. 查看测试输出中的错误信息
4. 检查Mock是否正确设置
### 覆盖率低
1. 检查是否有未测试的代码路径
2. 添加更多边界情况测试
3. 确保所有公共API都有测试覆盖
## 相关资源
- [Vitest 文档](https://vitest.dev/)
- [Testing Library](https://testing-library.com/)
- [Jest 迁移指南](https://vitest.dev/guide/migration.html)

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>');
});
});

View File

@@ -0,0 +1,284 @@
// @ts-nocheck
import { describe, it, expect, vi, beforeEach } from 'vitest';
import appdevDesignModePlugin from '../packages/plugin/src/index';
import type { Plugin } from 'vite';
describe('@xagi/vite-plugin-design-mode', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('插件初始化', () => {
it('应该创建插件实例', () => {
const plugin = appdevDesignModePlugin();
expect(plugin).toBeDefined();
expect(plugin.name).toBe('@xagi/vite-plugin-design-mode');
});
it('应该使用默认选项', () => {
const plugin = appdevDesignModePlugin();
expect(plugin).toBeDefined();
// 插件应该具有预期的方法
expect(typeof plugin.config).toBe('function');
expect(typeof plugin.configureServer).toBe('function');
expect(typeof plugin.transform).toBe('function');
expect(typeof plugin.transformIndexHtml).toBe('function');
expect(typeof plugin.buildStart).toBe('function');
expect(typeof plugin.buildEnd).toBe('function');
});
it('应该接受自定义选项', () => {
const plugin = appdevDesignModePlugin({
enabled: false,
verbose: true,
attributePrefix: 'data-test',
exclude: ['custom-exclude'],
include: ['custom-include'],
});
expect(plugin).toBeDefined();
expect(plugin.name).toBe('@xagi/vite-plugin-design-mode');
});
});
describe('config hook', () => {
it('应该在开发模式下启用', () => {
const plugin = appdevDesignModePlugin({ enabled: true });
const result = plugin.config?.({}, { command: 'serve' });
expect(result).toBeDefined();
expect(result?.define).toBeDefined();
expect(result?.define?.__APPDEV_DESIGN_MODE__).toBe(true);
});
it('应该在构建模式下禁用(如果未启用生产模式)', () => {
const plugin = appdevDesignModePlugin({
enabled: true,
enableInProduction: false,
});
const result = plugin.config?.({}, { command: 'build' });
expect(result).toEqual({});
});
it('应该在构建模式下启用(如果启用生产模式)', () => {
const plugin = appdevDesignModePlugin({
enabled: true,
enableInProduction: true,
});
const result = plugin.config?.({}, { command: 'build' });
expect(result).toBeDefined();
expect(result?.define?.__APPDEV_DESIGN_MODE__).toBe(true);
});
it('应该在禁用时返回空配置', () => {
const plugin = appdevDesignModePlugin({ enabled: false });
const result = plugin.config?.({}, { command: 'serve' });
expect(result).toEqual({});
});
it('应该设置verbose标志', () => {
const plugin = appdevDesignModePlugin({ verbose: true });
const result = plugin.config?.({}, { command: 'serve' });
expect(result?.define?.__APPDEV_DESIGN_MODE_VERBOSE__).toBe(true);
});
});
describe('transform hook', () => {
it('应该在禁用时返回原始代码', async () => {
const plugin = appdevDesignModePlugin({ enabled: false });
const code = 'function App() { return <div>Test</div>; }';
const result = await plugin.transform?.(code, 'test.tsx', {});
expect(result).toBe(code);
});
it('应该处理匹配的文件', async () => {
const plugin = appdevDesignModePlugin({ enabled: true });
const code = 'function App() { return <div>Test</div>; }';
const result = await plugin.transform?.(code, 'src/App.tsx', {});
expect(result).toBeDefined();
// 应该被转换(添加了源码映射属性)
if (typeof result === 'object' && result !== null) {
expect(result.code).toBeDefined();
}
});
it('应该排除node_modules中的文件', async () => {
const plugin = appdevDesignModePlugin({ enabled: true });
const code = 'function App() { return <div>Test</div>; }';
const result = await plugin.transform?.(code, 'node_modules/test.tsx', {});
expect(result).toBe(code);
});
it('应该处理转换错误', async () => {
const plugin = appdevDesignModePlugin({
enabled: true,
verbose: false,
});
// 无效的代码应该返回原始代码
const code = 'invalid syntax {{{{';
const result = await plugin.transform?.(code, 'test.tsx', {});
expect(result).toBeDefined();
});
});
describe('shouldProcessFile', () => {
it('应该处理匹配include模式的文件', async () => {
const plugin = appdevDesignModePlugin({
enabled: true,
include: ['**/*.tsx'],
});
const code = 'function App() { return <div>Test</div>; }';
const result = await plugin.transform?.(code, 'src/App.tsx', {});
expect(result).toBeDefined();
});
it('应该排除匹配exclude模式的文件', async () => {
const plugin = appdevDesignModePlugin({
enabled: true,
exclude: ['test'],
});
const code = 'function App() { return <div>Test</div>; }';
const result = await plugin.transform?.(code, 'test/App.tsx', {});
expect(result).toBe(code);
});
it('应该处理glob模式', async () => {
const plugin = appdevDesignModePlugin({
enabled: true,
include: ['src/**/*.{ts,tsx}'],
});
const code = 'function App() { return <div>Test</div>; }';
const result1 = await plugin.transform?.(code, 'src/App.tsx', {});
const result2 = await plugin.transform?.(code, 'src/components/Button.tsx', {});
expect(result1).toBeDefined();
expect(result2).toBeDefined();
});
});
describe('configureServer hook', () => {
it('应该在开发模式下配置服务器', () => {
const plugin = appdevDesignModePlugin({ enabled: true });
const mockServer = {
config: {
command: 'serve' as const,
root: '/test',
},
middlewares: {
use: vi.fn(),
},
} as any;
plugin.configureServer?.(mockServer);
expect(mockServer.middlewares.use).toHaveBeenCalled();
});
it('应该在禁用时不配置服务器', () => {
const plugin = appdevDesignModePlugin({ enabled: false });
const mockServer = {
config: {
command: 'serve' as const,
root: '/test',
},
middlewares: {
use: vi.fn(),
},
} as any;
plugin.configureServer?.(mockServer);
// 不应该调用use或者调用但立即返回
// 这里我们只检查不会抛出错误
expect(mockServer).toBeDefined();
});
});
describe('transformIndexHtml hook', () => {
it('应该在启用时注入客户端脚本', () => {
const plugin = appdevDesignModePlugin({ enabled: true });
const html = '<html><head></head><body></body></html>';
const result = plugin.transformIndexHtml?.(html, {
path: '/',
filename: 'index.html',
});
if (typeof result === 'object' && result !== null) {
expect(result.tags).toBeDefined();
expect(result.tags?.length).toBeGreaterThan(0);
expect(result.tags?.[0]?.tag).toBe('script');
}
});
it('应该在禁用时不注入脚本', () => {
const plugin = appdevDesignModePlugin({ enabled: false });
const html = '<html><head></head><body></body></html>';
const result = plugin.transformIndexHtml?.(html, {
path: '/',
filename: 'index.html',
});
expect(result).toBe(html);
});
});
describe('buildStart 和 buildEnd hooks', () => {
it('应该在verbose模式下输出日志', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const plugin = appdevDesignModePlugin({ verbose: true });
plugin.buildStart?.();
expect(consoleSpy).toHaveBeenCalledWith(
'[appdev-design-mode] Plugin started'
);
plugin.buildEnd?.();
expect(consoleSpy).toHaveBeenCalledWith(
'[appdev-design-mode] Plugin ended'
);
consoleSpy.mockRestore();
});
it('应该在非verbose模式下不输出日志', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const plugin = appdevDesignModePlugin({ verbose: false });
plugin.buildStart?.();
plugin.buildEnd?.();
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
});

View File

@@ -0,0 +1,211 @@
import { describe, it, expect } from 'vitest';
import {
isReactComponentName,
getJSXElementBaseName,
isStringLiteralAttribute,
extractStringAttributeValue,
createSourcePositionString,
parseSourcePositionString,
} from '../../packages/plugin/src/utils/babelHelpers';
import * as t from '@babel/types';
describe('babelHelpers', () => {
describe('isReactComponentName', () => {
it('应该识别大写开头的组件名', () => {
expect(isReactComponentName('App')).toBe(true);
expect(isReactComponentName('Button')).toBe(true);
expect(isReactComponentName('MyComponent')).toBe(true);
});
it('应该识别小写开头的非组件名', () => {
expect(isReactComponentName('app')).toBe(false);
expect(isReactComponentName('button')).toBe(false);
expect(isReactComponentName('myComponent')).toBe(false);
});
it('应该处理特殊字符', () => {
expect(isReactComponentName('_App')).toBe(false);
expect(isReactComponentName('$Component')).toBe(false);
expect(isReactComponentName('123Component')).toBe(false);
});
});
describe('getJSXElementBaseName', () => {
it('应该从JSXIdentifier获取名称', () => {
const identifier = t.jSXIdentifier('div');
const name = getJSXElementBaseName(identifier);
expect(name).toBe('div');
});
it('应该从JSXMemberExpression获取属性名', () => {
const memberExpr = t.jSXMemberExpression(
t.jSXIdentifier('React'),
t.jSXIdentifier('Component')
);
const name = getJSXElementBaseName(memberExpr);
expect(name).toBe('Component');
});
it('应该处理未知类型', () => {
const unknown = t.stringLiteral('test');
const name = getJSXElementBaseName(unknown as any);
expect(name).toBe('unknown');
});
});
describe('isStringLiteralAttribute', () => {
it('应该识别字符串字面量属性', () => {
const attr = t.jSXAttribute(
t.jSXIdentifier('className'),
t.stringLiteral('test')
);
const result = isStringLiteralAttribute(attr, 'className');
expect(result).toBe(true);
});
it('应该识别非字符串字面量属性', () => {
const attr = t.jSXAttribute(
t.jSXIdentifier('className'),
t.jSXExpressionContainer(t.identifier('classNameVar'))
);
const result = isStringLiteralAttribute(attr, 'className');
expect(result).toBe(false);
});
it('应该识别属性名不匹配的情况', () => {
const attr = t.jSXAttribute(
t.jSXIdentifier('id'),
t.stringLiteral('test')
);
const result = isStringLiteralAttribute(attr, 'className');
expect(result).toBe(false);
});
});
describe('extractStringAttributeValue', () => {
it('应该提取字符串属性值', () => {
const openingElement = t.jSXOpeningElement(
t.jSXIdentifier('div'),
[
t.jSXAttribute(
t.jSXIdentifier('className'),
t.stringLiteral('container')
),
]
);
const value = extractStringAttributeValue(openingElement, 'className');
expect(value).toBe('container');
});
it('应该返回null当属性不存在时', () => {
const openingElement = t.jSXOpeningElement(
t.jSXIdentifier('div'),
[]
);
const value = extractStringAttributeValue(openingElement, 'className');
expect(value).toBeNull();
});
it('应该返回null当属性不是字符串字面量时', () => {
const openingElement = t.jSXOpeningElement(
t.jSXIdentifier('div'),
[
t.jSXAttribute(
t.jSXIdentifier('className'),
t.jSXExpressionContainer(t.identifier('classNameVar'))
),
]
);
const value = extractStringAttributeValue(openingElement, 'className');
expect(value).toBeNull();
});
it('应该提取id属性值', () => {
const openingElement = t.jSXOpeningElement(
t.jSXIdentifier('div'),
[
t.jSXAttribute(
t.jSXIdentifier('id'),
t.stringLiteral('app')
),
]
);
const value = extractStringAttributeValue(openingElement, 'id');
expect(value).toBe('app');
});
});
describe('createSourcePositionString', () => {
it('应该创建正确的位置字符串', () => {
const position = createSourcePositionString('src/App.tsx', 10, 5);
expect(position).toBe('src/App.tsx:10:5');
});
it('应该处理不同的文件名', () => {
const position = createSourcePositionString('components/Button.tsx', 20, 15);
expect(position).toBe('components/Button.tsx:20:15');
});
it('应该处理行号和列号为0的情况', () => {
const position = createSourcePositionString('test.tsx', 0, 0);
expect(position).toBe('test.tsx:0:0');
});
});
describe('parseSourcePositionString', () => {
it('应该解析正确的位置字符串', () => {
const result = parseSourcePositionString('src/App.tsx:10:5');
expect(result).not.toBeNull();
expect(result?.fileName).toBe('src/App.tsx');
expect(result?.lineNumber).toBe(10);
expect(result?.columnNumber).toBe(5);
});
it('应该处理不同的文件名', () => {
const result = parseSourcePositionString('components/Button.tsx:20:15');
expect(result).not.toBeNull();
expect(result?.fileName).toBe('components/Button.tsx');
expect(result?.lineNumber).toBe(20);
expect(result?.columnNumber).toBe(15);
});
it('应该返回null当格式不正确时', () => {
expect(parseSourcePositionString('invalid')).toBeNull();
expect(parseSourcePositionString('src/App.tsx:10')).toBeNull();
expect(parseSourcePositionString('src/App.tsx:10:5:extra')).toBeNull();
});
it('应该返回null当行号或列号不是数字时', () => {
expect(parseSourcePositionString('src/App.tsx:abc:5')).toBeNull();
expect(parseSourcePositionString('src/App.tsx:10:def')).toBeNull();
expect(parseSourcePositionString('src/App.tsx:abc:def')).toBeNull();
});
it('应该处理行号和列号为0的情况', () => {
const result = parseSourcePositionString('test.tsx:0:0');
expect(result).not.toBeNull();
expect(result?.lineNumber).toBe(0);
expect(result?.columnNumber).toBe(0);
});
it('应该处理负数行号或列号', () => {
// 虽然负数在实际情况中不应该出现,但函数应该能处理
const result = parseSourcePositionString('test.tsx:-1:-1');
expect(result).not.toBeNull();
expect(result?.lineNumber).toBe(-1);
expect(result?.columnNumber).toBe(-1);
});
});
});