提交qiming-dev-inject
This commit is contained in:
388
qiming-dev-inject/test/e2e/e2e.test.js
Normal file
388
qiming-dev-inject/test/e2e/e2e.test.js
Normal file
@@ -0,0 +1,388 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { readFileSync } 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('端到端测试', () => {
|
||||
const testDir = join(__dirname, 'fixtures');
|
||||
const cliPath = join(__dirname, '..', '..', 'bin', 'index.js');
|
||||
const projectRoot = join(__dirname, '..', '..');
|
||||
|
||||
// 创建测试项目的基本结构
|
||||
const testProjects = {
|
||||
vite: {
|
||||
name: 'test-vite-project',
|
||||
files: {
|
||||
'package.json': JSON.stringify({
|
||||
"name": "test-vite-project",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": { "dev": "vite" },
|
||||
"dependencies": { "react": "^18.2.0" }
|
||||
}, null, 2),
|
||||
'vite.config.js': `import { defineConfig } from 'vite'
|
||||
export default defineConfig({
|
||||
plugins: []
|
||||
})`,
|
||||
'index.html': `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Vite Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>`,
|
||||
'src/main.js': `import './app.js'`,
|
||||
'src/app.js': `console.log('Vite app loaded');
|
||||
document.getElementById('app').innerHTML = '<h1>Vite App</h1>';`
|
||||
}
|
||||
},
|
||||
'next-app': {
|
||||
name: 'test-next-app',
|
||||
files: {
|
||||
'package.json': JSON.stringify({
|
||||
"name": "test-next-app",
|
||||
"version": "1.0.0",
|
||||
"scripts": { "dev": "next dev" }
|
||||
}, null, 2),
|
||||
'next.config.js': 'module.exports = {}',
|
||||
'app/layout.tsx': `import './globals.css'
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)}`,
|
||||
'app/page.tsx': `export default function Home() {
|
||||
return <h1>Next.js App</h1>;
|
||||
}`,
|
||||
'app/globals.css': 'body { font-family: Arial, sans-serif; }'
|
||||
}
|
||||
},
|
||||
'html-static': {
|
||||
name: 'test-html-project',
|
||||
files: {
|
||||
'index.html': `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Static HTML</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Static HTML Project</h1>
|
||||
<p>This is a static HTML project.</p>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function createProject(projectName, files) {
|
||||
const projectPath = join(testDir, projectName);
|
||||
mkdirSync(projectPath, { recursive: true });
|
||||
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
const fullPath = join(projectPath, filePath);
|
||||
const dir = dirname(fullPath);
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
writeFileSync(fullPath, content);
|
||||
}
|
||||
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
function cleanupProject(projectPath) {
|
||||
if (existsSync(projectPath)) {
|
||||
rmSync(projectPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function runCliInProject(projectPath, args, options = {}) {
|
||||
try {
|
||||
const result = execSync(`node ${cliPath} ${args}`, {
|
||||
encoding: 'utf8',
|
||||
cwd: projectPath,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: options.timeout || 10000
|
||||
});
|
||||
return { success: true, output: result };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: error.stdout || error.message,
|
||||
code: error.status,
|
||||
signal: error.signal
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test.before(() => {
|
||||
if (!existsSync(testDir)) {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
// 清理所有测试项目
|
||||
Object.keys(testProjects).forEach(projectName => {
|
||||
const projectPath = join(testDir, testProjects[projectName].name);
|
||||
cleanupProject(projectPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vite 项目完整流程', () => {
|
||||
test('应该完成 Vite 项目的完整注入流程', () => {
|
||||
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||
|
||||
try {
|
||||
// 1. 检测项目类型
|
||||
const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
|
||||
assert.strictEqual(detectResult.success, true);
|
||||
assert.match(detectResult.output, /检测到项目类型: vite/);
|
||||
|
||||
// 2. 执行实际注入
|
||||
const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
|
||||
assert.strictEqual(injectResult.success, true);
|
||||
assert.match(injectResult.output, /框架注入成功/);
|
||||
|
||||
// 3. 验证配置文件被修改
|
||||
const viteConfigPath = join(projectPath, 'vite.config.js');
|
||||
assert(existsSync(viteConfigPath));
|
||||
const configContent = readFileSync(viteConfigPath, 'utf8');
|
||||
assert.match(configContent, /dev-inject-plugin/);
|
||||
assert.match(configContent, /scripts\/monitor\.js/);
|
||||
|
||||
// 4. 验证原始配置保留
|
||||
assert.match(configContent, /defineConfig/);
|
||||
|
||||
// 5. 执行卸载
|
||||
const uninstallResult = runCliInProject(projectPath, 'uninstall --framework');
|
||||
assert.strictEqual(uninstallResult.success, true);
|
||||
|
||||
// 6. 验证插件被移除
|
||||
const cleanedContent = readFileSync(viteConfigPath, 'utf8');
|
||||
assert(!cleanedContent.includes('dev-inject-plugin'));
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该处理不同的脚本类型', () => {
|
||||
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||
|
||||
try {
|
||||
// 测试 HTTP URL
|
||||
const httpResult = runCliInProject(projectPath, 'install --remote=https://cdn.example.com/monitor.js --framework --dry-run');
|
||||
assert.strictEqual(httpResult.success, true);
|
||||
assert.match(httpResult.output, /检测到项目类型: vite/);
|
||||
|
||||
// 测试本地路径
|
||||
const localResult = runCliInProject(projectPath, 'install --remote=/assets/dev-monitor.js --framework --dry-run');
|
||||
assert.strictEqual(localResult.success, true);
|
||||
assert.match(localResult.output, /检测到项目类型: vite/);
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next.js 项目完整流程', () => {
|
||||
test('应该完成 Next.js App Router 的完整注入流程', () => {
|
||||
const projectPath = createProject(testProjects['next-app'].name, testProjects['next-app'].files);
|
||||
|
||||
try {
|
||||
// 1. 检测项目类型
|
||||
const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
|
||||
assert.strictEqual(detectResult.success, true);
|
||||
assert.match(detectResult.output, /检测到项目类型: next-app/);
|
||||
|
||||
// 2. 执行实际注入
|
||||
const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
|
||||
assert.strictEqual(injectResult.success, true);
|
||||
assert.match(injectResult.output, /框架注入成功/);
|
||||
|
||||
// 3. 验证 layout 文件被修改
|
||||
const layoutPath = join(projectPath, 'app/layout.tsx');
|
||||
assert(existsSync(layoutPath));
|
||||
const layoutContent = readFileSync(layoutPath, 'utf8');
|
||||
assert.match(layoutContent, /process\.env\.NODE_ENV === 'development'/);
|
||||
assert.match(layoutContent, /scripts\/monitor\.js/);
|
||||
|
||||
// 4. 验证原始内容保留
|
||||
assert.match(layoutContent, /RootLayout/);
|
||||
assert.match(layoutContent, /children/);
|
||||
|
||||
// 5. 执行卸载
|
||||
const uninstallResult = runCliInProject(projectPath, 'uninstall --framework');
|
||||
assert.strictEqual(uninstallResult.success, true);
|
||||
|
||||
// 6. 验证注入内容被移除
|
||||
const cleanedContent = readFileSync(layoutPath, 'utf8');
|
||||
assert(!cleanedContent.includes('process.env.NODE_ENV'));
|
||||
assert(!cleanedContent.includes('scripts/monitor.js'));
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('静态 HTML 项目完整流程', () => {
|
||||
test('应该完成静态 HTML 项目的完整注入流程', () => {
|
||||
const projectPath = createProject(testProjects['html-static'].name, testProjects['html-static'].files);
|
||||
|
||||
try {
|
||||
// 1. 检测项目类型
|
||||
const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
|
||||
assert.strictEqual(detectResult.success, true);
|
||||
assert.match(detectResult.output, /检测到项目类型: html-static/);
|
||||
|
||||
// 2. 执行实际注入
|
||||
const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
|
||||
assert.strictEqual(injectResult.success, true);
|
||||
assert.match(injectResult.output, /成功处理 \d+ 个文件/);
|
||||
|
||||
// 3. 验证 HTML 文件被修改
|
||||
const htmlPath = join(projectPath, 'index.html');
|
||||
assert(existsSync(htmlPath));
|
||||
const htmlContent = readFileSync(htmlPath, 'utf8');
|
||||
assert.match(htmlContent, /<script src="\/scripts\/monitor\.js"><\/script>/);
|
||||
assert.match(htmlContent, /<!-- injected by dev-inject -->/);
|
||||
|
||||
// 4. 验证原始内容保留
|
||||
assert.match(htmlContent, /Static HTML Project/);
|
||||
|
||||
// 5. 执行卸载
|
||||
const uninstallResult = runCliInProject(projectPath, 'uninstall');
|
||||
assert.strictEqual(uninstallResult.success, true);
|
||||
assert.match(uninstallResult.output, /成功清理 \d+ 个文件/);
|
||||
|
||||
// 6. 验证脚本被移除
|
||||
const cleanedContent = readFileSync(htmlPath, 'utf8');
|
||||
assert(!cleanedContent.includes('scripts/monitor.js'));
|
||||
assert(!cleanedContent.includes('injected by dev-inject'));
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('回退机制测试', () => {
|
||||
test('应该在不支持的项目中回退到传统模式', () => {
|
||||
// 创建一个不支持的空项目
|
||||
const projectPath = join(testDir, 'unsupported-project');
|
||||
mkdirSync(projectPath, { recursive: true });
|
||||
|
||||
try {
|
||||
const result = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run');
|
||||
|
||||
// 应该回退到传统模式并找到 HTML 文件
|
||||
assert.strictEqual(result.success, true);
|
||||
assert.match(result.output, /回退到传统 HTML 注入模式/);
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('并发测试', () => {
|
||||
test('应该处理多个并发命令', async () => {
|
||||
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||
|
||||
try {
|
||||
// 创建多个并发命令
|
||||
const commands = [
|
||||
runCliInProject(projectPath, 'install --remote=/scripts/monitor1.js --framework --dry-run'),
|
||||
runCliInProject(projectPath, 'install --remote=/scripts/monitor2.js --framework --dry-run'),
|
||||
runCliInProject(projectPath, 'install --remote=/scripts/monitor3.js --framework --dry-run')
|
||||
];
|
||||
|
||||
// 等待所有命令完成
|
||||
const results = await Promise.all(commands);
|
||||
|
||||
// 所有命令都应该成功
|
||||
results.forEach(result => {
|
||||
assert.strictEqual(result.success, true);
|
||||
});
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
test('应该在合理时间内完成大型项目操作', () => {
|
||||
// 创建一个大项目(很多文件)
|
||||
const projectPath = join(testDir, 'large-project');
|
||||
mkdirSync(projectPath, { recursive: true });
|
||||
|
||||
// 创建大量 HTML 文件
|
||||
for (let i = 0; i < 100; i++) {
|
||||
writeFileSync(join(projectPath, `file${i}.html`), `<html><head><title>File ${i}</title></head><body></body></html>`);
|
||||
}
|
||||
|
||||
// 创建 Vite 配置
|
||||
writeFileSync(join(projectPath, 'vite.config.js'), 'module.exports = { plugins: [] }');
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run', {
|
||||
timeout: 30000 // 30秒超时
|
||||
});
|
||||
const endTime = Date.now();
|
||||
|
||||
assert.strictEqual(result.success, true);
|
||||
assert(endTime - startTime < 10000, '大项目操作应该在 10 秒内完成');
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('真实场景模拟', () => {
|
||||
test('应该模拟真实的开发工作流', () => {
|
||||
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||
|
||||
try {
|
||||
// 1. 初始注入
|
||||
const initialInject = runCliInProject(projectPath, 'install --remote=/scripts/dev-monitor.js --framework');
|
||||
assert.strictEqual(initialInject.success, true);
|
||||
|
||||
// 2. 重新注入(应该检测到已存在)
|
||||
const reInject = runCliInProject(projectPath, 'install --remote=/scripts/dev-monitor.js --framework --dry-run');
|
||||
assert.strictEqual(reInject.success, true);
|
||||
|
||||
// 3. 更换脚本
|
||||
const switchScript = runCliInProject(projectPath, 'install --remote=/scripts/new-monitor.js --framework --dry-run');
|
||||
assert.strictEqual(switchScript.success, true);
|
||||
|
||||
// 4. 卸载
|
||||
const uninstall = runCliInProject(projectPath, 'uninstall --framework');
|
||||
assert.strictEqual(uninstall.success, true);
|
||||
|
||||
// 5. 清理后重新注入
|
||||
const cleanReinject = runCliInProject(projectPath, 'install --remote=/scripts/dev-monitor.js --framework --dry-run');
|
||||
assert.strictEqual(cleanReinject.success, true);
|
||||
|
||||
} finally {
|
||||
cleanupProject(projectPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
313
qiming-dev-inject/test/integration/cli.test.js
Normal file
313
qiming-dev-inject/test/integration/cli.test.js
Normal file
@@ -0,0 +1,313 @@
|
||||
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 秒内完成');
|
||||
});
|
||||
});
|
||||
});
|
||||
1
qiming-dev-inject/test/integration/fixtures/index.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test0.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test0.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test1.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test1.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test2.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test2.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test3.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test3.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test4.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test4.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test5.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test5.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test6.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test6.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test7.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test7.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test8.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test8.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/integration/fixtures/test9.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test9.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
409
qiming-dev-inject/test/integration/smartInject.test.js
Normal file
409
qiming-dev-inject/test/integration/smartInject.test.js
Normal file
@@ -0,0 +1,409 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// 导入要测试的模块
|
||||
import { smartInject } from '../../lib/framework-inject.js';
|
||||
|
||||
describe('框架注入集成测试', () => {
|
||||
const testDir = join(__dirname, 'fixtures');
|
||||
|
||||
test.before(() => {
|
||||
if (!existsSync(testDir)) {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
// 清理所有测试文件
|
||||
const filesToClean = [
|
||||
'vite.config.js',
|
||||
'vite.config.ts',
|
||||
'next.config.js',
|
||||
'app/layout.tsx',
|
||||
'app/layout.js',
|
||||
'pages/_document.tsx',
|
||||
'pages/_document.js',
|
||||
'test.html'
|
||||
];
|
||||
|
||||
filesToClean.forEach(file => {
|
||||
const filePath = join(testDir, file);
|
||||
if (existsSync(filePath)) {
|
||||
rmSync(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
const dirsToClean = ['app', 'pages'];
|
||||
dirsToClean.forEach(dir => {
|
||||
const dirPath = join(testDir, dir);
|
||||
if (existsSync(dirPath)) {
|
||||
rmSync(dirPath, { recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vite 项目注入', () => {
|
||||
test('应该成功注入到 Vite 配置文件', () => {
|
||||
// 创建 Vite 配置文件
|
||||
const viteConfig = `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
})`;
|
||||
writeFileSync(join(testDir, 'vite.config.js'), viteConfig);
|
||||
|
||||
// 执行注入
|
||||
const result = smartInject({
|
||||
remote: 'http://localhost:9000/dev-monitor.js',
|
||||
dryRun: true,
|
||||
verbose: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('应该检查已存在的插件', () => {
|
||||
// 创建已有插件的 Vite 配置
|
||||
const viteConfigWithPlugin = `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
// dev-inject-plugin
|
||||
{
|
||||
name: 'dev-inject-plugin',
|
||||
transformIndexHtml(html) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return html.replace('</head>', '<script src="/existing.js"></script></head>');
|
||||
}
|
||||
return html;
|
||||
}
|
||||
}
|
||||
],
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
})`;
|
||||
writeFileSync(join(testDir, 'vite.config.js'), viteConfigWithPlugin);
|
||||
|
||||
// 执行注入(应该检测到已存在)
|
||||
const result = smartInject({
|
||||
remote: 'http://localhost:9000/dev-monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('应该处理没有 plugins 数组的情况', () => {
|
||||
// 创建没有 plugins 的配置
|
||||
const simpleConfig = `export default {
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
}`;
|
||||
writeFileSync(join(testDir, 'vite.config.js'), simpleConfig);
|
||||
|
||||
const result = smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('应该处理 TypeScript 配置文件', () => {
|
||||
const tsConfig = `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})`;
|
||||
writeFileSync(join(testDir, 'vite.config.ts'), tsConfig);
|
||||
|
||||
const result = smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next.js App Router 注入', () => {
|
||||
test('应该成功注入到 app/layout.tsx', () => {
|
||||
// 创建 Next.js App Router 结构
|
||||
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||
|
||||
const layoutContent = `import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Next.js App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}`;
|
||||
writeFileSync(join(testDir, 'app', 'layout.tsx'), layoutContent);
|
||||
|
||||
const result = smartInject({
|
||||
remote: 'http://localhost:9000/dev-monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('应该处理 app/layout.js', () => {
|
||||
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||
|
||||
const jsLayout = `import './globals.css'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}`;
|
||||
writeFileSync(join(testDir, 'app', 'layout.js'), jsLayout);
|
||||
|
||||
const result = smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next.js Pages Router 注入', () => {
|
||||
test('应该成功注入到 pages/_document.tsx', () => {
|
||||
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||
|
||||
const documentContent = `import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}`;
|
||||
writeFileSync(join(testDir, 'pages', '_document.tsx'), documentContent);
|
||||
|
||||
const result = smartInject({
|
||||
remote: 'http://localhost:9000/dev-monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('应该处理 pages/_document.js', () => {
|
||||
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||
|
||||
const jsDocument = `const { Html, Head, Main, NextScript } = require('next/document')
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<title>My App</title>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}`;
|
||||
writeFileSync(join(testDir, 'pages', '_document.js'), jsDocument);
|
||||
|
||||
const result = smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('混合模式处理', () => {
|
||||
test('应该优先注入到 app/layout', () => {
|
||||
// 创建混合模式结构
|
||||
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||
|
||||
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() { return <div>App Layout</div>; }');
|
||||
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() { return <div>Document</div>; }');
|
||||
|
||||
const result = smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
test('应该处理不支持的框架类型', () => {
|
||||
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||
|
||||
try {
|
||||
smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: false
|
||||
}, testDir);
|
||||
assert.fail('应该抛出错误');
|
||||
} catch (error) {
|
||||
assert.match(error.message, /不支持的项目类型/);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该处理配置文件不存在的情况', () => {
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||
// 不创建 layout 文件
|
||||
|
||||
try {
|
||||
smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: false
|
||||
}, testDir);
|
||||
assert.fail('应该抛出错误');
|
||||
} catch (error) {
|
||||
assert.match(error.message, /未找到/);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该处理无效的配置文件', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.js'), 'invalid javascript content');
|
||||
|
||||
try {
|
||||
smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: false
|
||||
}, testDir);
|
||||
assert.fail('应该抛出错误');
|
||||
} catch (error) {
|
||||
assert.ok(error.message.length > 0); // 应该有某种错误信息
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('实际文件修改测试', () => {
|
||||
test('应该实际修改 Vite 配置文件', () => {
|
||||
const originalConfig = `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
})`;
|
||||
|
||||
writeFileSync(join(testDir, 'vite.config.js'), originalConfig);
|
||||
|
||||
// 执行实际注入
|
||||
const result = smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: false,
|
||||
verbose: false
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
|
||||
// 验证文件被修改
|
||||
const modifiedContent = readFileSync(join(testDir, 'vite.config.js'), 'utf8');
|
||||
assert.match(modifiedContent, /dev-inject-plugin/);
|
||||
assert.match(modifiedContent, /scripts\/monitor\.js/);
|
||||
|
||||
// 验证原始内容保留
|
||||
assert.match(modifiedContent, /defineConfig/);
|
||||
assert.match(modifiedContent, /react\(\)/);
|
||||
});
|
||||
|
||||
test('应该能够移除注入的内容', () => {
|
||||
// 先注入
|
||||
smartInject({
|
||||
remote: '/scripts/monitor.js',
|
||||
dryRun: false
|
||||
}, testDir);
|
||||
|
||||
// 然后移除
|
||||
const result = removeInjection({
|
||||
dryRun: false,
|
||||
verbose: false
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
|
||||
// 验证插件被移除
|
||||
const cleanedContent = readFileSync(join(testDir, 'vite.config.js'), 'utf8');
|
||||
assert(!cleanedContent.includes('dev-inject-plugin'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('不同脚本类型', () => {
|
||||
test('应该处理 HTTP URL', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.js'), 'export default { plugins: [] }');
|
||||
|
||||
const result = smartInject({
|
||||
remote: 'https://cdn.example.com/monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('应该处理本地路径', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.js'), 'export default { plugins: [] }');
|
||||
|
||||
const result = smartInject({
|
||||
remote: '/assets/dev-monitor.js',
|
||||
dryRun: true
|
||||
}, testDir);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
14
qiming-dev-inject/test/package.json
Normal file
14
qiming-dev-inject/test/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node --test",
|
||||
"test:unit": "node --test test/unit/*.test.js",
|
||||
"test:integration": "node --test test/integration/*.test.js",
|
||||
"test:e2e": "node --test test/e2e/*.test.js",
|
||||
"test:watch": "node --test --watch",
|
||||
"test:coverage": "node --test --experimental-test-coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node:test": "^1.0.0"
|
||||
}
|
||||
}
|
||||
180
qiming-dev-inject/test/test-runner.js
Normal file
180
qiming-dev-inject/test/test-runner.js
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
console.log('🧪 dev-inject 测试运行器\n');
|
||||
console.log('=' .repeat(50));
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const testDir = new URL('.', import.meta.url).pathname;
|
||||
|
||||
// 颜要在项目根目录运行
|
||||
const projectRoot = process.cwd();
|
||||
const cliPath = join(projectRoot, 'bin', 'index.js');
|
||||
const testPackagePath = join(projectRoot, 'test', 'package.json');
|
||||
|
||||
// 检查测试环境
|
||||
function checkTestEnvironment() {
|
||||
if (!existsSync(cliPath)) {
|
||||
console.error('❌ CLI 路径不存在:', cliPath);
|
||||
console.log('请确保在 dev-inject 项目根目录运行此脚本');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(testPackagePath)) {
|
||||
console.error('❌ 测试配置不存在:', testPackagePath);
|
||||
console.log('正在创建测试配置...');
|
||||
execSync('npm init -y', { cwd: join(projectRoot, 'test'), stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
// 运行指定类型的测试
|
||||
function runTests(testType) {
|
||||
console.log(`\n🚀 运行 ${testType.toUpperCase()} 测试...\n`);
|
||||
|
||||
try {
|
||||
const testCommand = `node --test test/${testType}/*.test.js`;
|
||||
execSync(testCommand, {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit',
|
||||
timeout: 60000 // 60秒超时
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`\n❌ ${testType.toUpperCase()} 测试失败`);
|
||||
if (error.signal === 'SIGTERM') {
|
||||
console.log(' 原因: 测试超时');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行特定测试
|
||||
function runSpecificTest(testPath) {
|
||||
console.log(`\n🎯 运行特定测试: ${testPath}\n`);
|
||||
|
||||
try {
|
||||
execSync(`node --test ${testPath}`, {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit',
|
||||
timeout: 30000
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`\n❌ 测试 ${testPath} 失败`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成测试报告
|
||||
function generateTestReport(results) {
|
||||
const totalTests = Object.keys(results).length;
|
||||
const passedTests = Object.values(results).filter(Boolean).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('📊 测试报告汇总');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`✅ 通过: ${passedTests}/${totalTests}`);
|
||||
console.log(`❌ 失败: ${failedTests}/${totalTests}`);
|
||||
console.log(`📈 成功率: ${Math.round((passedTests / totalTests) * 100)}%`);
|
||||
|
||||
if (passedTests === totalTests) {
|
||||
console.log('\n🎉 所有测试都通过了!dev-inject 功能正常!');
|
||||
} else {
|
||||
console.log('\n⚠️ 有部分测试失败,请检查相关功能。');
|
||||
}
|
||||
|
||||
console.log('\n🔍 测试覆盖范围:');
|
||||
console.log(' 📦 单元测试 - 核心函数和逻辑');
|
||||
console.log(' 🔧 集成测试 - 模块间协作');
|
||||
console.log(' 🌐 端到端测试 - 完整工作流');
|
||||
console.log(' 📱 框架支持 - Vite、Next.js、HTML');
|
||||
|
||||
return passedTests === totalTests;
|
||||
}
|
||||
|
||||
// 主函数
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// 检查测试环境
|
||||
checkTestEnvironment();
|
||||
|
||||
// 解析命令行参数
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
🧪 dev-inject 测试运行器
|
||||
|
||||
用法:
|
||||
node test/test-runner.js [选项] [测试类型/文件]
|
||||
|
||||
选项:
|
||||
--help, -h 显示帮助信息
|
||||
--unit 运行单元测试
|
||||
--integration 运行集成测试
|
||||
--e2e 运行端到端测试
|
||||
--all 运行所有测试
|
||||
--verbose 显示详细输出
|
||||
--watch 监听模式(需要 node --test 支持)
|
||||
|
||||
示例:
|
||||
node test/test-runner.js --unit # 只运行单元测试
|
||||
node test/test-runner.js --integration # 只运行集成测试
|
||||
node test/test-runner.js --e2e # 只运行端到端测试
|
||||
node test/test-runner.js --all # 运行所有测试
|
||||
node test/test-runner.js test/unit/parseArgs.test.js # 运行特定测试文件
|
||||
|
||||
测试类型:
|
||||
unit 单元测试(核心函数)
|
||||
integration 集成测试(模块协作)
|
||||
e2e 端到端测试(完整流程)
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const results = {};
|
||||
|
||||
if (args.includes('--all')) {
|
||||
// 运行所有测试
|
||||
console.log('🎯 运行所有测试套件...');
|
||||
results.unit = runTests('unit');
|
||||
results.integration = runTests('integration');
|
||||
results.e2e = runTests('e2e');
|
||||
} else if (args.includes('--unit')) {
|
||||
// 运行单元测试
|
||||
results.unit = runTests('unit');
|
||||
} else if (args.includes('--integration')) {
|
||||
// 运行集成测试
|
||||
results.integration = runTests('integration');
|
||||
} else if (args.includes('--e2e')) {
|
||||
// 运行端到端测试
|
||||
results.e2e = runTests('e2e');
|
||||
} else if (args.some(arg => arg.endsWith('.test.js'))) {
|
||||
// 运行特定测试文件
|
||||
const testFile = args.find(arg => arg.endsWith('.test.js'));
|
||||
const success = runSpecificTest(testFile);
|
||||
process.exit(success ? 0 : 1);
|
||||
} else {
|
||||
// 默认运行快速测试
|
||||
console.log('🚀 运行快速测试套件...');
|
||||
results.unit = runTests('unit');
|
||||
|
||||
if (results.unit) {
|
||||
console.log('\n✅ 单元测试通过,继续运行集成测试...');
|
||||
results.integration = runTests('integration');
|
||||
}
|
||||
|
||||
if (results.unit && results.integration) {
|
||||
console.log('\n✅ 基础测试通过,可以运行完整测试:');
|
||||
console.log(' node test/test-runner.js --all');
|
||||
}
|
||||
}
|
||||
|
||||
// 生成报告
|
||||
const allPassed = generateTestReport(results);
|
||||
process.exit(allPassed ? 0 : 1);
|
||||
}
|
||||
|
||||
main();
|
||||
160
qiming-dev-inject/test/unit/detectProjectType.test.js
Normal file
160
qiming-dev-inject/test/unit/detectProjectType.test.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
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);
|
||||
|
||||
// 导入要测试的模块
|
||||
import { detectProjectType } from '../../lib/framework-inject.js';
|
||||
|
||||
describe('框架检测功能', () => {
|
||||
const testDir = join(__dirname, 'fixtures');
|
||||
|
||||
// 测试前设置
|
||||
test.before(() => {
|
||||
// 确保测试目录存在
|
||||
if (!existsSync(testDir)) {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// 测试后清理
|
||||
test.after(() => {
|
||||
// 清理测试文件
|
||||
const filesToClean = [
|
||||
'vite.config.js',
|
||||
'vite.config.ts',
|
||||
'next.config.js',
|
||||
'app/layout.tsx',
|
||||
'pages/_document.tsx',
|
||||
'public/index.html',
|
||||
'index.html'
|
||||
];
|
||||
|
||||
filesToClean.forEach(file => {
|
||||
const filePath = join(testDir, file);
|
||||
if (existsSync(filePath)) {
|
||||
rmSync(filePath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vite 项目检测', () => {
|
||||
test('应该检测到 Vite 项目 (js)', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'vite');
|
||||
});
|
||||
|
||||
test('应该检测到 Vite 项目 (ts)', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.ts'), 'export default {};');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'vite');
|
||||
});
|
||||
|
||||
test('优先检测 Vite 而不是其他配置', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||
writeFileSync(join(testDir, 'public/index.html'), '<html></html>');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'vite');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next.js App Router 检测', () => {
|
||||
test('应该检测到 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 type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'next-app');
|
||||
});
|
||||
|
||||
test('应该检测到 Next.js App Router (js)', () => {
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'app', 'layout.js'), 'export default function Layout() {}');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'next-app');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next.js Pages Router 检测', () => {
|
||||
test('应该检测到 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 type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'next-pages');
|
||||
});
|
||||
|
||||
test('应该检测到 Next.js Pages Router (js)', () => {
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'pages', '_document.js'), 'export default function Document() {}');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'next-pages');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next.js 混合模式检测', () => {
|
||||
test('应该检测到 Next.js 混合模式', () => {
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
|
||||
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'next-hybrid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create React App 检测', () => {
|
||||
test('应该检测到 Create React App', () => {
|
||||
mkdirSync(join(testDir, 'public'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'public', 'index.html'), '<html></html>');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'create-react-app');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML 静态项目检测', () => {
|
||||
test('应该检测到 HTML 静态项目', () => {
|
||||
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'html-static');
|
||||
});
|
||||
});
|
||||
|
||||
describe('未知项目检测', () => {
|
||||
test('应该返回 unknown 对于空目录', () => {
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'unknown');
|
||||
});
|
||||
|
||||
test('应该返回 unknown 对于不存在的目录', () => {
|
||||
const type = detectProjectType('/non-existent-directory');
|
||||
assert.strictEqual(type, 'unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('优先级检测', () => {
|
||||
test('Next.js 优先于 HTML', () => {
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'next-app'); // 默认是 app 模式
|
||||
});
|
||||
|
||||
test('Vite 优先于 Next.js', () => {
|
||||
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
|
||||
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||
const type = detectProjectType(testDir);
|
||||
assert.strictEqual(type, 'vite');
|
||||
});
|
||||
});
|
||||
});
|
||||
1
qiming-dev-inject/test/unit/fixtures/app/layout.js
Normal file
1
qiming-dev-inject/test/unit/fixtures/app/layout.js
Normal file
@@ -0,0 +1 @@
|
||||
export default function Layout() {}
|
||||
1
qiming-dev-inject/test/unit/fixtures/index.html
Normal file
1
qiming-dev-inject/test/unit/fixtures/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
1
qiming-dev-inject/test/unit/fixtures/pages/_document.js
Normal file
1
qiming-dev-inject/test/unit/fixtures/pages/_document.js
Normal file
@@ -0,0 +1 @@
|
||||
export default function Document() {}
|
||||
1
qiming-dev-inject/test/unit/fixtures/test.html
Normal file
1
qiming-dev-inject/test/unit/fixtures/test.html
Normal file
@@ -0,0 +1 @@
|
||||
<html></html>
|
||||
161
qiming-dev-inject/test/unit/parseArgs.test.js
Normal file
161
qiming-dev-inject/test/unit/parseArgs.test.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
// 导入要测试的模块
|
||||
import { parseArgs } from '../../lib/args.js';
|
||||
|
||||
describe('CLI 参数解析', () => {
|
||||
describe('基本命令解析', () => {
|
||||
test('应该解析 install 命令', () => {
|
||||
const result = parseArgs(['install', '--remote', '/scripts/test.js']);
|
||||
assert.strictEqual(result.command, 'install');
|
||||
assert.strictEqual(result.options.remote, '/scripts/test.js');
|
||||
});
|
||||
|
||||
test('应该解析 uninstall 命令', () => {
|
||||
const result = parseArgs(['uninstall']);
|
||||
assert.strictEqual(result.command, 'uninstall');
|
||||
});
|
||||
|
||||
test('应该处理空参数', () => {
|
||||
const result = parseArgs([]);
|
||||
assert.strictEqual(result.command, null);
|
||||
assert.deepStrictEqual(result.options, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('--remote 参数解析', () => {
|
||||
test('应该解析 --remote=value 格式', () => {
|
||||
const result = parseArgs(['install', '--remote=http://localhost:9000/monitor.js']);
|
||||
assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
|
||||
});
|
||||
|
||||
test('应该解析 --remote value 格式', () => {
|
||||
const result = parseArgs(['install', '--remote', '/scripts/dev-monitor.js']);
|
||||
assert.strictEqual(result.options.remote, '/scripts/dev-monitor.js');
|
||||
});
|
||||
|
||||
test('应该处理多个 --remote 参数(使用最后一个)', () => {
|
||||
const result = parseArgs(['install', '--remote', '/first.js', '--remote', '/second.js']);
|
||||
assert.strictEqual(result.options.remote, '/second.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('--file 参数解析', () => {
|
||||
test('应该解析 --file=value 格式', () => {
|
||||
const result = parseArgs(['install', '--remote=/test.js', '--file=public/index.html']);
|
||||
assert.strictEqual(result.options.file, 'public/index.html');
|
||||
});
|
||||
|
||||
test('应该解析 --file value 格式', () => {
|
||||
const result = parseArgs(['install', '--remote=/test.js', '--file', './public/index.html']);
|
||||
assert.strictEqual(result.options.file, './public/index.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('--framework 参数解析', () => {
|
||||
test('应该解析 --framework 选项', () => {
|
||||
const result = parseArgs(['install', '--remote=/test.js', '--framework']);
|
||||
assert.strictEqual(result.options.framework, true);
|
||||
});
|
||||
|
||||
test('应该解析 -f 简写选项', () => {
|
||||
const result = parseArgs(['install', '--remote=/test.js', '-f']);
|
||||
assert.strictEqual(result.options.framework, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('--dry-run 参数解析', () => {
|
||||
test('应该解析 --dry-run 选项', () => {
|
||||
const result = parseArgs(['install', '--remote=/test.js', '--dry-run']);
|
||||
assert.strictEqual(result.options.dryRun, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('--verbose 参数解析', () => {
|
||||
test('应该解析 --verbose 选项', () => {
|
||||
const result = parseArgs(['install', '--remote=/test.js', '--verbose']);
|
||||
assert.strictEqual(result.options.verbose, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('复合选项解析', () => {
|
||||
test('应该解析多个选项', () => {
|
||||
const result = parseArgs([
|
||||
'install',
|
||||
'--remote', 'http://localhost:9000/monitor.js',
|
||||
'--framework',
|
||||
'--dry-run',
|
||||
'--verbose',
|
||||
'--file', './test.html'
|
||||
]);
|
||||
|
||||
assert.strictEqual(result.command, 'install');
|
||||
assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
|
||||
assert.strictEqual(result.options.framework, true);
|
||||
assert.strictEqual(result.options.dryRun, true);
|
||||
assert.strictEqual(result.options.verbose, true);
|
||||
assert.strictEqual(result.options.file, './test.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
test('install 命令缺少 --remote 参数应该抛出错误', () => {
|
||||
assert.throws(() => {
|
||||
parseArgs(['install']);
|
||||
}, /install 命令需要 --remote 参数/);
|
||||
});
|
||||
|
||||
test('uninstall 命令不需要 --remote 参数', () => {
|
||||
const result = parseArgs(['uninstall']);
|
||||
assert.strictEqual(result.command, 'uninstall');
|
||||
assert.deepStrictEqual(result.options, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('无效参数处理', () => {
|
||||
test('应该忽略未知参数', () => {
|
||||
// 使用 mock console.warn 来捕获警告
|
||||
const originalWarn = console.warn;
|
||||
let warnCalled = false;
|
||||
console.warn = () => { warnCalled = true; };
|
||||
|
||||
const result = parseArgs(['install', '--remote=/test.js', '--unknown-option']);
|
||||
|
||||
console.warn = originalWarn;
|
||||
|
||||
// 应该仍然正确解析已知参数
|
||||
assert.strictEqual(result.options.remote, '/test.js');
|
||||
// 应该标记未知参数被忽略(通过 console.warn)
|
||||
assert.ok(warnCalled); // 如果有警告输出则测试通过
|
||||
});
|
||||
});
|
||||
|
||||
describe('参数顺序无关性', () => {
|
||||
test('应该正确处理不同顺序的参数', () => {
|
||||
const result1 = parseArgs(['install', '--remote=/test.js', '--framework', '--dry-run']);
|
||||
const result2 = parseArgs(['install', '--dry-run', '--framework', '--remote=/test.js']);
|
||||
|
||||
assert.deepStrictEqual(result1.options, result2.options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
test('应该处理空的 --remote 值', () => {
|
||||
const result = parseArgs(['install', '--remote', '']);
|
||||
assert.strictEqual(result.options.remote, '');
|
||||
});
|
||||
|
||||
test('应该处理包含特殊字符的路径', () => {
|
||||
const complexPath = '/path with spaces/file.js';
|
||||
const result = parseArgs(['install', '--remote', complexPath]);
|
||||
assert.strictEqual(result.options.remote, complexPath);
|
||||
});
|
||||
|
||||
test('应该处理长参数值', () => {
|
||||
const longValue = 'a'.repeat(1000);
|
||||
const result = parseArgs(['install', '--remote', longValue]);
|
||||
assert.strictEqual(result.options.remote, longValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
304
qiming-dev-inject/test/unit/utils.test.js
Normal file
304
qiming-dev-inject/test/unit/utils.test.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
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);
|
||||
|
||||
// 导入要测试的模块
|
||||
import {
|
||||
parseRemoteType,
|
||||
generateScriptTag,
|
||||
lookupFiles,
|
||||
injectScriptToHtml,
|
||||
removeInjectedScripts
|
||||
} from '../../lib/utils.js';
|
||||
|
||||
describe('工具函数', () => {
|
||||
const testDir = join(__dirname, 'fixtures');
|
||||
|
||||
test.before(() => {
|
||||
if (!existsSync(testDir)) {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
const filesToClean = [
|
||||
'test1.html',
|
||||
'test2.html',
|
||||
'subdir/test3.html',
|
||||
'subdir/test4.html'
|
||||
];
|
||||
|
||||
filesToClean.forEach(file => {
|
||||
const filePath = join(testDir, file);
|
||||
if (existsSync(filePath)) {
|
||||
rmSync(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
const subDir = join(testDir, 'subdir');
|
||||
if (existsSync(subDir)) {
|
||||
rmSync(subDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('parseRemoteType', () => {
|
||||
test('应该识别 HTTP URL', () => {
|
||||
assert.strictEqual(parseRemoteType('http://localhost:9000/monitor.js'), 'url');
|
||||
assert.strictEqual(parseRemoteType('http://example.com/script.js'), 'url');
|
||||
});
|
||||
|
||||
test('应该识别 HTTPS URL', () => {
|
||||
assert.strictEqual(parseRemoteType('https://cdn.example.com/monitor.js'), 'url');
|
||||
assert.strictEqual(parseRemoteType('https://localhost:3000/dev-monitor.js'), 'url');
|
||||
});
|
||||
|
||||
test('应该识别绝对路径', () => {
|
||||
assert.strictEqual(parseRemoteType('/scripts/monitor.js'), 'absolute-path');
|
||||
assert.strictEqual(parseRemoteType('/assets/dev-monitor.js'), 'absolute-path');
|
||||
assert.strictEqual(parseRemoteType('/path/to/script.js'), 'absolute-path');
|
||||
});
|
||||
|
||||
test('应该处理相对路径', () => {
|
||||
try {
|
||||
parseRemoteType('./script.js');
|
||||
assert.fail('应该抛出错误');
|
||||
} catch (error) {
|
||||
assert.match(error.message, /不支持的远程路径格式/);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该处理空字符串', () => {
|
||||
try {
|
||||
parseRemoteType('');
|
||||
assert.fail('应该抛出错误');
|
||||
} catch (error) {
|
||||
assert.match(error.message, /不支持的远程路径格式/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateScriptTag', () => {
|
||||
test('应该生成 URL 类型的脚本标签', () => {
|
||||
const tag = generateScriptTag('http://localhost:9000/monitor.js', 'url');
|
||||
assert.strictEqual(tag, '<script src="http://localhost:9000/monitor.js"></script>');
|
||||
});
|
||||
|
||||
test('应该生成绝对路径类型的脚本标签', () => {
|
||||
const tag = generateScriptTag('/scripts/monitor.js', 'absolute-path');
|
||||
assert.strictEqual(tag, '<script src="/scripts/monitor.js"></script>');
|
||||
});
|
||||
|
||||
test('应该处理不同的路径格式', () => {
|
||||
const testCases = [
|
||||
'https://cdn.example.com/monitor.js',
|
||||
'http://localhost:3000/dev-monitor.js',
|
||||
'/assets/scripts/dev-monitor.js',
|
||||
'/scripts/monitor.js'
|
||||
];
|
||||
|
||||
testCases.forEach(path => {
|
||||
const type = parseRemoteType(path);
|
||||
const tag = generateScriptTag(path, type);
|
||||
assert.match(tag, /^<script src="[^"]*"><\/script>$/);
|
||||
assert(tag.includes(path));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupFiles', () => {
|
||||
test('应该查找指定目录中的 HTML 文件', () => {
|
||||
// 创建测试文件
|
||||
writeFileSync(join(testDir, 'test1.html'), '<html></html>');
|
||||
writeFileSync(join(testDir, 'test2.html'), '<html></html>');
|
||||
mkdirSync(join(testDir, 'subdir'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'subdir', 'test3.html'), '<html></html>');
|
||||
|
||||
const files = lookupFiles(testDir);
|
||||
|
||||
// 应该找到所有 HTML 文件
|
||||
assert(files.length >= 3);
|
||||
assert(files.some(f => f.includes('test1.html')));
|
||||
assert(files.some(f => f.includes('test2.html')));
|
||||
assert(files.some(f => f.includes('test3.html')));
|
||||
});
|
||||
|
||||
test('应该优先返回 index.html', () => {
|
||||
// 创建测试文件
|
||||
writeFileSync(join(testDir, 'test.html'), '<html></html>');
|
||||
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||
|
||||
const files = lookupFiles(testDir);
|
||||
|
||||
// 应该优先返回 index.html
|
||||
assert.strictEqual(files.length, 1);
|
||||
assert(files[0].includes('index.html'));
|
||||
});
|
||||
|
||||
test('应该处理空目录', () => {
|
||||
const files = lookupFiles(testDir);
|
||||
assert.deepStrictEqual(files, []);
|
||||
});
|
||||
|
||||
test('应该处理不存在的目录', () => {
|
||||
const files = lookupFiles('/non-existent-directory');
|
||||
assert.deepStrictEqual(files, []);
|
||||
});
|
||||
|
||||
test('应该跳过 node_modules 目录', () => {
|
||||
// 创建测试文件和 node_modules 目录
|
||||
writeFileSync(join(testDir, 'test.html'), '<html></html>');
|
||||
mkdirSync(join(testDir, 'node_modules'), { recursive: true });
|
||||
writeFileSync(join(testDir, 'node_modules', 'index.html'), '<html></html>');
|
||||
|
||||
const files = lookupFiles(testDir);
|
||||
|
||||
// 不应该包含 node_modules 中的文件
|
||||
assert(files.length === 1);
|
||||
assert(files[0].includes('test.html'));
|
||||
assert(!files.some(f => f.includes('node_modules')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectScriptToHtml', () => {
|
||||
test('应该注入脚本到 HTML 的 </head> 标签之前', () => {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const scriptTag = '<script src="/scripts/monitor.js"></script>';
|
||||
const result = injectScriptToHtml(html, scriptTag);
|
||||
|
||||
assert.match(result, /<script src="\/scripts\/monitor\.js"><\/script>\s*<\/head>/);
|
||||
assert.match(result, /dev-inject/);
|
||||
});
|
||||
|
||||
test('应该添加 dev-inject 标识注释', () => {
|
||||
const html = `<html><head></head><body></body></html>`;
|
||||
const scriptTag = '<script src="/test.js"></script>';
|
||||
const result = injectScriptToHtml(html, scriptTag);
|
||||
|
||||
assert.match(result, /<!-- injected by dev-inject -->/);
|
||||
});
|
||||
|
||||
test('应该处理没有 </head> 标签的情况', () => {
|
||||
const html = `<html><head><title>Test</title></head><body>content</body></html>`;
|
||||
const scriptTag = '<script src="/test.js"></script>';
|
||||
const result = injectScriptToHtml(html, scriptTag);
|
||||
|
||||
assert.match(result, /<script src="\/test\.js"><\/script>/);
|
||||
});
|
||||
|
||||
test('应该处理没有 <head> 标签的情况', () => {
|
||||
const html = `<html><body>content</body></html>`;
|
||||
const scriptTag = '<script src="/test.js"></script>';
|
||||
const result = injectScriptToHtml(html, scriptTag);
|
||||
|
||||
assert.match(result, /<script src="\/test\.js"><\/script>/);
|
||||
});
|
||||
|
||||
test('应该处理空 HTML', () => {
|
||||
const html = '';
|
||||
const scriptTag = '<script src="/test.js"></script>';
|
||||
const result = injectScriptToHtml(html, scriptTag);
|
||||
|
||||
assert.match(result, /<script src="\/test\.js"><\/script>/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeInjectedScripts', () => {
|
||||
test('应该移除注入的脚本和注释', () => {
|
||||
const htmlWithInjection = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test</title>
|
||||
<script src="/scripts/monitor.js"></script> <!-- injected by dev-inject -->
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const result = removeInjectedScripts(htmlWithInjection);
|
||||
|
||||
assert.match(result, /<head>\s*<meta charset="UTF-8">\s*<title>Test<\/title>\s*<\/head>/);
|
||||
assert(!result.includes('dev-monitor.js'));
|
||||
assert(!result.includes('injected by dev-inject'));
|
||||
});
|
||||
|
||||
test('应该处理多个注入的脚本', () => {
|
||||
const htmlWithMultiple = `<html><head>
|
||||
<script src="/script1.js"></script> <!-- injected by dev-inject -->
|
||||
<title>Test</title>
|
||||
<script src="/script2.js"></script> <!-- injected by dev-inject -->
|
||||
</head></html>`;
|
||||
|
||||
const result = removeInjectedScripts(htmlWithMultiple);
|
||||
|
||||
assert(!result.includes('script1.js'));
|
||||
assert(!result.includes('script2.js'));
|
||||
assert(!result.includes('injected by dev-inject'));
|
||||
});
|
||||
|
||||
test('应该保留非注入的脚本', () => {
|
||||
const htmlWithMixed = `<html><head>
|
||||
<script src="/normal-script.js"></script>
|
||||
<script src="/injected-script.js"></script> <!-- injected by dev-inject -->
|
||||
<title>Test</title>
|
||||
</head></html>`;
|
||||
|
||||
const result = removeInjectedScripts(htmlWithMixed);
|
||||
|
||||
assert(result.includes('normal-script.js'));
|
||||
assert(!result.includes('injected-script.js'));
|
||||
assert(!result.includes('injected by dev-inject'));
|
||||
});
|
||||
|
||||
test('应该处理没有注入脚本的 HTML', () => {
|
||||
const normalHtml = `<html><head><title>Test</title></head><body>content</body></html>`;
|
||||
const result = removeInjectedScripts(normalHtml);
|
||||
|
||||
assert.strictEqual(result, normalHtml);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况和错误处理', () => {
|
||||
test('应该处理特殊字符', () => {
|
||||
const html = '<html><head><title>Test & "Special" Characters</title></head></html>';
|
||||
const scriptTag = '<script src="/path/with spaces/script.js"></script>';
|
||||
const result = injectScriptToHtml(html, scriptTag);
|
||||
|
||||
assert(result.includes('script.js'));
|
||||
});
|
||||
|
||||
test('应该处理非常长的 HTML', () => {
|
||||
const longHtml = '<html><head>' + '<meta charset="UTF-8">'.repeat(1000) + '</head></html>';
|
||||
const scriptTag = '<script src="/test.js"></script>';
|
||||
const result = injectScriptToHtml(longHtml, scriptTag);
|
||||
|
||||
assert(result.length > longHtml.length);
|
||||
assert(result.includes('test.js'));
|
||||
});
|
||||
|
||||
test('应该处理格式不规范的 HTML', () => {
|
||||
const malformedHtml = '<html><HEAD><title>Test</Title></HEAD><BODY>content</BODY></HTML>';
|
||||
const scriptTag = '<script src="/test.js"></script>';
|
||||
const result = injectScriptToHtml(malformedHtml, scriptTag);
|
||||
|
||||
assert(result.includes('test.js'));
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user