提交qiming-dev-inject

This commit is contained in:
Codex
2026-06-01 12:53:19 +08:00
parent 96af3b324b
commit 9e9486b7c2
76 changed files with 6397 additions and 0 deletions

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

View 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 秒内完成');
});
});
});

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
<html></html>

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

View 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"
}
}

View 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();

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

View File

@@ -0,0 +1 @@
export default function Layout() {}

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
export default function Document() {}

View File

@@ -0,0 +1 @@
<html></html>

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

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