Files
qiming/qiming-dev-inject/test/integration/cli.test.js
2026-06-01 12:53:19 +08:00

314 lines
9.5 KiB
JavaScript

import { test, describe } from 'node:test';
import assert from 'node:assert';
import { execSync } from 'child_process';
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
describe('CLI 功能集成测试', () => {
const testDir = join(__dirname, 'fixtures');
const cliPath = join(__dirname, '..', '..', 'bin', 'index.js');
const projectRoot = join(__dirname, '..', '..');
test.before(() => {
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
});
test.after(() => {
// 清理测试文件
const filesToClean = [
'vite.config.js',
'next.config.js',
'app/layout.tsx',
'pages/_document.tsx',
'test.html',
'public/index.html'
];
filesToClean.forEach(file => {
const filePath = join(testDir, file);
if (existsSync(filePath)) {
rmSync(filePath);
}
});
const dirsToClean = ['app', 'pages', 'public'];
dirsToClean.forEach(dir => {
const dirPath = join(testDir, dir);
if (existsSync(dirPath)) {
rmSync(dirPath, { recursive: true });
}
});
});
function runCli(args, options = {}) {
try {
const result = execSync(`node ${cliPath} ${args}`, {
encoding: 'utf8',
cwd: options.cwd || projectRoot,
stdio: ['ignore', 'pipe', 'pipe']
});
return { success: true, output: result };
} catch (error) {
return {
success: false,
output: error.stdout || error.message,
code: error.status
};
}
}
describe('基本 CLI 命令', () => {
test('应该显示帮助信息', () => {
const result = runCli('--help');
assert.strictEqual(result.success, true);
assert.match(result.output, /dev-inject.*开发环境脚本注入工具/);
assert.match(result.output, /--framework.*使用框架感知注入模式/);
assert.match(result.output, /--remote.*脚本地址/);
});
test('应该显示版本信息', () => {
const result = runCli('--version');
assert.strictEqual(result.success, true);
assert.match(result.output, /dev-inject v\d+\.\d+\.\d+/);
});
test('应该处理无效参数', () => {
const result = runCli('--invalid-option');
// 应该仍然成功(忽略未知选项)
assert.strictEqual(result.success, true);
});
test('应该在缺少参数时显示错误', () => {
const result = runCli('install');
assert.strictEqual(result.success, false);
assert.match(result.output, /install 命令需要 --remote 参数/);
});
});
describe('框架感知命令', () => {
test('应该检测到 Vite 项目', () => {
// 创建 Vite 项目
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = { plugins: [] }');
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /检测到项目类型: vite/);
assert.match(result.output, /DRY RUN/);
});
test('应该检测到 Next.js App Router', () => {
// 创建 Next.js App Router
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
mkdirSync(join(testDir, 'app'), { recursive: true });
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /检测到项目类型: next-app/);
});
test('应该检测到 Next.js Pages Router', () => {
// 创建 Next.js Pages Router
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
mkdirSync(join(testDir, 'pages'), { recursive: true });
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /检测到项目类型: next-pages/);
});
test('应该检测到 Create React App', () => {
// Create React App
mkdirSync(join(testDir, 'public'), { recursive: true });
writeFileSync(join(testDir, 'public', 'index.html'), '<html></html>');
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /检测到项目类型: create-react-app/);
});
test('应该检测到 HTML 项目', () => {
writeFileSync(join(testDir, 'index.html'), '<html></html>');
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /检测到项目类型: html-static/);
});
});
describe('传统注入模式', () => {
test('应该注入到 HTML 文件', () => {
// 创建 HTML 文件
const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`;
writeFileSync(join(testDir, 'test.html'), htmlContent);
const result = runCli('install --remote=/scripts/test.js --file=./test.html --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /DRY RUN.*将注入脚本到.*test\.html/);
});
test('应该查找多个 HTML 文件', () => {
// 创建多个 HTML 文件
writeFileSync(join(testDir, 'index.html'), '<html></html>');
writeFileSync(join(testDir, 'test.html'), '<html></html>');
const result = runCli('install --remote=/scripts/test.js --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /找到 \d+ 个 HTML 文件/);
});
});
describe('卸载功能', () => {
test('应该支持传统卸载', () => {
writeFileSync(join(testDir, 'test.html'), '<html></html>');
const result = runCli('uninstall --file=./test.html --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /卸载脚本/);
});
test('应该支持框架卸载', () => {
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = { plugins: [] }');
const result = runCli('uninstall --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
// 框架卸载的输出可能不同
});
});
describe('详细输出模式', () => {
test('应该显示详细的注入信息', () => {
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = { plugins: [] }');
const result = runCli('install --remote=/scripts/test.js --framework --dry-run --verbose', {
cwd: testDir
});
assert.strictEqual(result.success, true);
assert.match(result.output, /脚本类型:/);
assert.match(result.output, /脚本标签:/);
});
});
describe('错误处理', () => {
test('应该处理不存在的文件', () => {
const result = runCli('install --remote=/scripts/test.js --file=./nonexistent.html', {
cwd: testDir
});
assert.strictEqual(result.success, false);
assert.match(result.output, /未找到 HTML 文件/);
});
test('应该处理权限问题', () => {
// 创建只读目录(在某些系统上可能不支持)
try {
mkdirSync(join(testDir, 'readonly'), { recursive: true, mode: 0o444 });
const result = runCli('install --remote=/scripts/test.js --file=./readonly/test.html', {
cwd: testDir
});
// 应该有某种错误处理
assert.ok(true);
} catch (error) {
// 忽略权限相关的错误
}
});
});
describe('参数验证', () => {
test('应该验证远程 URL 格式', () => {
const result = runCli('install --remote=invalid-url --framework --dry-run', {
cwd: testDir
});
// 应该仍然接受,验证在实际注入时进行
assert.strictEqual(result.success, true);
});
test('应该处理空参数', () => {
const result = runCli('install --remote= --framework --dry-run', {
cwd: testDir
});
assert.strictEqual(result.success, true);
});
test('应该处理长参数', () => {
const longRemote = 'https://example.com/' + 'a'.repeat(100) + '/script.js';
const result = runCli(`install --remote=${longRemote} --framework --dry-run`, {
cwd: testDir
});
assert.strictEqual(result.success, true);
});
});
describe('性能测试', () => {
test('应该在合理时间内完成', () => {
// 创建一些文件
for (let i = 0; i < 10; i++) {
writeFileSync(join(testDir, `test${i}.html`), '<html></html>');
}
const startTime = Date.now();
const result = runCli('install --remote=/scripts/test.js --dry-run', {
cwd: testDir
});
const endTime = Date.now();
assert.strictEqual(result.success, true);
assert(endTime - startTime < 5000, 'CLI 命令应该在 5 秒内完成');
});
});
});