fix(e2e): 修复前端服务启动冲突问题

问题:
- Playwright的webServer配置会自动启动前端服务
- global-setup.ts也在启动前端服务
- 导致端口3002冲突

修复:
- 移除global-setup.ts中的前端服务启动逻辑
- 移除global-setup.ts中的前端服务停止逻辑
- 移除前端服务健康检查验证
- 让Playwright的webServer统一管理前端服务

优势:
- 避免端口冲突
- 简化测试环境设置
- 统一服务管理
This commit is contained in:
张翔
2026-04-07 11:24:50 +08:00
parent 92df794cc8
commit d65537529a
2 changed files with 77 additions and 138 deletions
+14
View File
@@ -0,0 +1,14 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class TestBCrypt {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String password = "admin123";
String hash = "$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy";
System.out.println("测试密码验证:");
System.out.println("密码: " + password);
System.out.println("哈希: " + hash);
System.out.println("验证结果: " + encoder.matches(password, hash));
}
}
+63 -138
View File
@@ -9,7 +9,6 @@ const __dirname = path.dirname(__filename);
let backendProcess: ChildProcess | null = null; let backendProcess: ChildProcess | null = null;
let gatewayProcess: ChildProcess | null = null; let gatewayProcess: ChildProcess | null = null;
let frontendProcess: ChildProcess | null = null;
let healthCheckInterval: NodeJS.Timeout | null = null; let healthCheckInterval: NodeJS.Timeout | null = null;
async function checkBackendHealth(): Promise<boolean> { async function checkBackendHealth(): Promise<boolean> {
@@ -57,12 +56,12 @@ function startHealthMonitoring() {
if (healthCheckInterval) { if (healthCheckInterval) {
clearInterval(healthCheckInterval); clearInterval(healthCheckInterval);
} }
healthCheckInterval = setInterval(async () => { healthCheckInterval = setInterval(async () => {
const backendHealthy = await checkBackendHealth(); const backendHealthy = await checkBackendHealth();
const gatewayHealthy = await checkGatewayHealth(); const gatewayHealthy = await checkGatewayHealth();
const frontendHealthy = await checkFrontendHealth(); const frontendHealthy = await checkFrontendHealth();
if (!backendHealthy) { if (!backendHealthy) {
console.error('⚠️ 后端服务健康检查失败!'); console.error('⚠️ 后端服务健康检查失败!');
} }
@@ -84,16 +83,16 @@ function stopHealthMonitoring() {
async function globalSetup(config: FullConfig) { async function globalSetup(config: FullConfig) {
console.log('🚀 开始全局测试环境设置...'); console.log('🚀 开始全局测试环境设置...');
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
process.env.PLAYWRIGHT_HEADLESS = 'false'; process.env.PLAYWRIGHT_HEADLESS = 'false';
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app'); const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar'); const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
let backendCommand: string; let backendCommand: string;
let backendArgs: string[]; let backendArgs: string[];
if (existsSync(jarFile)) { if (existsSync(jarFile)) {
console.log('📦 使用JAR文件启动后端服务...'); console.log('📦 使用JAR文件启动后端服务...');
console.log(` JAR文件: ${jarFile}`); console.log(` JAR文件: ${jarFile}`);
@@ -111,10 +110,10 @@ async function globalSetup(config: FullConfig) {
backendCommand = 'mvn'; backendCommand = 'mvn';
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev']; backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
} }
console.log(` 目录: ${backendDir}`); console.log(` 目录: ${backendDir}`);
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`); console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
backendProcess = spawn(backendCommand, backendArgs, { backendProcess = spawn(backendCommand, backendArgs, {
cwd: backendDir, cwd: backendDir,
stdio: 'pipe', stdio: 'pipe',
@@ -122,7 +121,7 @@ async function globalSetup(config: FullConfig) {
detached: false, detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' } env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
}); });
if (backendProcess.stdout) { if (backendProcess.stdout) {
backendProcess.stdout.on('data', (data) => { backendProcess.stdout.on('data', (data) => {
const output = data.toString(); const output = data.toString();
@@ -131,7 +130,7 @@ async function globalSetup(config: FullConfig) {
} }
}); });
} }
if (backendProcess.stderr) { if (backendProcess.stderr) {
backendProcess.stderr.on('data', (data) => { backendProcess.stderr.on('data', (data) => {
const output = data.toString(); const output = data.toString();
@@ -140,26 +139,26 @@ async function globalSetup(config: FullConfig) {
} }
}); });
} }
backendProcess.on('error', (error) => { backendProcess.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error); console.error('❌ 后端服务启动失败:', error);
}); });
backendProcess.on('exit', (code, signal) => { backendProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) { if (code !== 0 && code !== null) {
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`); console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
} }
}); });
console.log('⏳ 等待后端服务就绪...'); console.log('⏳ 等待后端服务就绪...');
await waitForBackendReady(); await waitForBackendReady();
const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway'); const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway');
const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar'); const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar');
let gatewayCommand: string; let gatewayCommand: string;
let gatewayArgs: string[]; let gatewayArgs: string[];
if (existsSync(gatewayJarFile)) { if (existsSync(gatewayJarFile)) {
console.log('🚪 使用JAR文件启动网关服务...'); console.log('🚪 使用JAR文件启动网关服务...');
console.log(` JAR文件: ${gatewayJarFile}`); console.log(` JAR文件: ${gatewayJarFile}`);
@@ -177,10 +176,10 @@ async function globalSetup(config: FullConfig) {
gatewayCommand = 'mvn'; gatewayCommand = 'mvn';
gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev']; gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
} }
console.log(` 目录: ${gatewayDir}`); console.log(` 目录: ${gatewayDir}`);
console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`); console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`);
gatewayProcess = spawn(gatewayCommand, gatewayArgs, { gatewayProcess = spawn(gatewayCommand, gatewayArgs, {
cwd: gatewayDir, cwd: gatewayDir,
stdio: 'pipe', stdio: 'pipe',
@@ -188,7 +187,7 @@ async function globalSetup(config: FullConfig) {
detached: false, detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' } env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' }
}); });
if (gatewayProcess.stdout) { if (gatewayProcess.stdout) {
gatewayProcess.stdout.on('data', (data) => { gatewayProcess.stdout.on('data', (data) => {
const output = data.toString(); const output = data.toString();
@@ -197,7 +196,7 @@ async function globalSetup(config: FullConfig) {
} }
}); });
} }
if (gatewayProcess.stderr) { if (gatewayProcess.stderr) {
gatewayProcess.stderr.on('data', (data) => { gatewayProcess.stderr.on('data', (data) => {
const output = data.toString(); const output = data.toString();
@@ -206,71 +205,28 @@ async function globalSetup(config: FullConfig) {
} }
}); });
} }
gatewayProcess.on('error', (error) => { gatewayProcess.on('error', (error) => {
console.error('❌ 网关服务启动失败:', error); console.error('❌ 网关服务启动失败:', error);
}); });
gatewayProcess.on('exit', (code, signal) => { gatewayProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) { if (code !== 0 && code !== null) {
console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`); console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`);
} }
}); });
console.log('⏳ 等待网关服务就绪...'); console.log('⏳ 等待网关服务就绪...');
await waitForGatewayReady(); await waitForGatewayReady();
const frontendDir = path.resolve(__dirname, '..');
console.log('🌐 启动前端服务...');
console.log(` 目录: ${frontendDir}`);
frontendProcess = spawn('pnpm', ['run', 'dev'], {
cwd: frontendDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, NODE_ENV: 'test' }
});
if (frontendProcess.stdout) {
frontendProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Local:') || output.includes('localhost:3002')) {
console.log('✅ 前端服务启动成功');
}
});
}
if (frontendProcess.stderr) {
frontendProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('error')) {
console.error('❌ 前端服务启动错误:', output);
}
});
}
frontendProcess.on('error', (error) => {
console.error('❌ 前端服务启动失败:', error);
});
frontendProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 前端服务异常退出,退出码: ${code}, 信号: ${signal}`);
}
});
console.log('⏳ 等待前端服务就绪...');
await waitForFrontendReady();
console.log('🔍 验证所有服务连通性...'); console.log('🔍 验证所有服务连通性...');
await verifyAllServices(); await verifyAllServices();
console.log('🧹 清理测试数据...'); console.log('🧹 清理测试数据...');
await cleanupTestData(); await cleanupTestData();
startHealthMonitoring(); startHealthMonitoring();
console.log('✅ 全局测试环境设置完成'); console.log('✅ 全局测试环境设置完成');
} }
@@ -281,21 +237,14 @@ async function verifyAllServices(): Promise<void> {
throw new Error('❌ 后端服务验证失败'); throw new Error('❌ 后端服务验证失败');
} }
console.log(' ✅ 后端服务正常'); console.log(' ✅ 后端服务正常');
console.log(' 验证网关服务...'); console.log(' 验证网关服务...');
const gatewayOk = await checkGatewayHealth(); const gatewayOk = await checkGatewayHealth();
if (!gatewayOk) { if (!gatewayOk) {
throw new Error('❌ 网关服务验证失败'); throw new Error('❌ 网关服务验证失败');
} }
console.log(' ✅ 网关服务正常'); console.log(' ✅ 网关服务正常');
console.log(' 验证前端服务...');
const frontendOk = await checkFrontendHealth();
if (!frontendOk) {
throw new Error('❌ 前端服务验证失败');
}
console.log(' ✅ 前端服务正常');
console.log(' 验证网关到后端的连通性...'); console.log(' 验证网关到后端的连通性...');
try { try {
const response = await fetch('http://localhost:8080/api/auth/login', { const response = await fetch('http://localhost:8080/api/auth/login', {
@@ -304,28 +253,28 @@ async function verifyAllServices(): Promise<void> {
body: JSON.stringify({ username: 'admin', password: 'admin123' }), body: JSON.stringify({ username: 'admin', password: 'admin123' }),
signal: AbortSignal.timeout(10000) as any signal: AbortSignal.timeout(10000) as any
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`网关到后端连通性验证失败,状态码: ${response.status}`); throw new Error(`网关到后端连通性验证失败,状态码: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
if (!data.token) { if (!data.token) {
throw new Error('网关到后端连通性验证失败,未返回token'); throw new Error('网关到后端连通性验证失败,未返回token');
} }
console.log(' ✅ 网关到后端连通性正常'); console.log(' ✅ 网关到后端连通性正常');
} catch (error) { } catch (error) {
throw new Error(`❌ 网关到后端连通性验证失败: ${error}`); throw new Error(`❌ 网关到后端连通性验证失败: ${error}`);
} }
console.log('✅ 所有服务验证通过'); console.log('✅ 所有服务验证通过');
} }
async function waitForBackendReady(): Promise<void> { async function waitForBackendReady(): Promise<void> {
const maxRetries = 90; const maxRetries = 90;
const retryInterval = 1000; const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
const response = await fetch('http://localhost:8084/actuator/health', { const response = await fetch('http://localhost:8084/actuator/health', {
@@ -335,7 +284,7 @@ async function waitForBackendReady(): Promise<void> {
const data = await response.json(); const data = await response.json();
if (data.status === 'UP') { if (data.status === 'UP') {
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`); console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
// 验证服务连通性:测试登录API // 验证服务连通性:测试登录API
try { try {
const loginTest = await fetch('http://localhost:8084/api/auth/login', { const loginTest = await fetch('http://localhost:8084/api/auth/login', {
@@ -344,7 +293,7 @@ async function waitForBackendReady(): Promise<void> {
body: JSON.stringify({ username: 'admin', password: 'admin123' }), body: JSON.stringify({ username: 'admin', password: 'admin123' }),
signal: AbortSignal.timeout(10000) as any signal: AbortSignal.timeout(10000) as any
}); });
if (loginTest.ok) { if (loginTest.ok) {
console.log('✅ 后端服务连通性验证通过(登录API可用)'); console.log('✅ 后端服务连通性验证通过(登录API可用)');
return; return;
@@ -359,19 +308,19 @@ async function waitForBackendReady(): Promise<void> {
} catch (error) { } catch (error) {
// 服务还未就绪,继续等待 // 服务还未就绪,继续等待
} }
if (i < maxRetries - 1) { if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryInterval)); await new Promise(resolve => setTimeout(resolve, retryInterval));
} }
} }
throw new Error('❌ 后端服务启动超时'); throw new Error('❌ 后端服务启动超时');
} }
async function waitForGatewayReady(): Promise<void> { async function waitForGatewayReady(): Promise<void> {
const maxRetries = 90; const maxRetries = 90;
const retryInterval = 1000; const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
const response = await fetch('http://localhost:8080/actuator/health', { const response = await fetch('http://localhost:8080/actuator/health', {
@@ -381,7 +330,7 @@ async function waitForGatewayReady(): Promise<void> {
const data = await response.json(); const data = await response.json();
if (data.status === 'UP') { if (data.status === 'UP') {
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`); console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
// 验证网关连通性:通过网关测试登录API // 验证网关连通性:通过网关测试登录API
try { try {
const loginTest = await fetch('http://localhost:8080/api/auth/login', { const loginTest = await fetch('http://localhost:8080/api/auth/login', {
@@ -390,7 +339,7 @@ async function waitForGatewayReady(): Promise<void> {
body: JSON.stringify({ username: 'admin', password: 'admin123' }), body: JSON.stringify({ username: 'admin', password: 'admin123' }),
signal: AbortSignal.timeout(10000) as any signal: AbortSignal.timeout(10000) as any
}); });
if (loginTest.ok) { if (loginTest.ok) {
console.log('✅ 网关服务连通性验证通过(登录API可用)'); console.log('✅ 网关服务连通性验证通过(登录API可用)');
return; return;
@@ -405,19 +354,19 @@ async function waitForGatewayReady(): Promise<void> {
} catch (error) { } catch (error) {
// 服务还未就绪,继续等待 // 服务还未就绪,继续等待
} }
if (i < maxRetries - 1) { if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryInterval)); await new Promise(resolve => setTimeout(resolve, retryInterval));
} }
} }
throw new Error('❌ 网关服务启动超时'); throw new Error('❌ 网关服务启动超时');
} }
async function waitForFrontendReady(): Promise<void> { async function waitForFrontendReady(): Promise<void> {
const maxRetries = 90; const maxRetries = 90;
const retryInterval = 1000; const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
const response = await fetch('http://localhost:3002', { const response = await fetch('http://localhost:3002', {
@@ -430,12 +379,12 @@ async function waitForFrontendReady(): Promise<void> {
} catch (error) { } catch (error) {
// 服务还未就绪,继续等待 // 服务还未就绪,继续等待
} }
if (i < maxRetries - 1) { if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryInterval)); await new Promise(resolve => setTimeout(resolve, retryInterval));
} }
} }
throw new Error('❌ 前端服务启动超时'); throw new Error('❌ 前端服务启动超时');
} }
@@ -452,25 +401,25 @@ async function cleanupTestData(): Promise<void> {
password: 'admin123' password: 'admin123'
}) })
}); });
if (!loginResponse.ok) { if (!loginResponse.ok) {
console.log('⚠️ 无法登录,跳过数据清理'); console.log('⚠️ 无法登录,跳过数据清理');
return; return;
} }
const loginData = await loginResponse.json(); const loginData = await loginResponse.json();
const token = loginData.token; const token = loginData.token;
// 获取所有用户 // 获取所有用户
const usersResponse = await fetch('http://localhost:8080/api/users', { const usersResponse = await fetch('http://localhost:8080/api/users', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
}); });
if (usersResponse.ok) { if (usersResponse.ok) {
const users = await usersResponse.json(); const users = await usersResponse.json();
// 删除测试创建的用户(保留ID 1-10的初始用户) // 删除测试创建的用户(保留ID 1-10的初始用户)
for (const user of users) { for (const user of users) {
if (user.id > 10) { if (user.id > 10) {
@@ -488,17 +437,17 @@ async function cleanupTestData(): Promise<void> {
} }
} }
} }
// 获取所有角色 // 获取所有角色
const rolesResponse = await fetch('http://localhost:8080/api/roles', { const rolesResponse = await fetch('http://localhost:8080/api/roles', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
}); });
if (rolesResponse.ok) { if (rolesResponse.ok) {
const roles = await rolesResponse.json(); const roles = await rolesResponse.json();
// 删除测试创建的角色(保留ID 1-4的初始角色) // 删除测试创建的角色(保留ID 1-4的初始角色)
for (const role of roles) { for (const role of roles) {
if (role.id > 4) { if (role.id > 4) {
@@ -516,7 +465,7 @@ async function cleanupTestData(): Promise<void> {
} }
} }
} }
console.log('✅ 测试数据清理完成'); console.log('✅ 测试数据清理完成');
} catch (error) { } catch (error) {
console.log('⚠️ 数据清理失败,继续执行测试'); console.log('⚠️ 数据清理失败,继续执行测试');
@@ -526,20 +475,20 @@ async function cleanupTestData(): Promise<void> {
async function globalTeardown() { async function globalTeardown() {
console.log('🧹 开始全局测试环境清理...'); console.log('🧹 开始全局测试环境清理...');
stopHealthMonitoring(); stopHealthMonitoring();
if (backendProcess) { if (backendProcess) {
console.log('🛑 停止后端服务...'); console.log('🛑 停止后端服务...');
backendProcess.kill('SIGTERM'); backendProcess.kill('SIGTERM');
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
if (backendProcess) { if (backendProcess) {
backendProcess.on('exit', () => { backendProcess.on('exit', () => {
console.log('✅ 后端服务已停止'); console.log('✅ 后端服务已停止');
resolve(); resolve();
}); });
setTimeout(() => { setTimeout(() => {
if (backendProcess) { if (backendProcess) {
backendProcess.kill('SIGKILL'); backendProcess.kill('SIGKILL');
@@ -552,18 +501,18 @@ async function globalTeardown() {
} }
}); });
} }
if (gatewayProcess) { if (gatewayProcess) {
console.log('🛑 停止网关服务...'); console.log('🛑 停止网关服务...');
gatewayProcess.kill('SIGTERM'); gatewayProcess.kill('SIGTERM');
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
if (gatewayProcess) { if (gatewayProcess) {
gatewayProcess.on('exit', () => { gatewayProcess.on('exit', () => {
console.log('✅ 网关服务已停止'); console.log('✅ 网关服务已停止');
resolve(); resolve();
}); });
setTimeout(() => { setTimeout(() => {
if (gatewayProcess) { if (gatewayProcess) {
gatewayProcess.kill('SIGKILL'); gatewayProcess.kill('SIGKILL');
@@ -576,31 +525,7 @@ async function globalTeardown() {
} }
}); });
} }
if (frontendProcess) {
console.log('🛑 停止前端服务...');
frontendProcess.kill('SIGTERM');
await new Promise<void>((resolve) => {
if (frontendProcess) {
frontendProcess.on('exit', () => {
console.log('✅ 前端服务已停止');
resolve();
});
setTimeout(() => {
if (frontendProcess) {
frontendProcess.kill('SIGKILL');
console.log('⚠️ 强制停止前端服务');
resolve();
}
}, 10000);
} else {
resolve();
}
});
}
console.log('✅ 全局测试环境清理完成'); console.log('✅ 全局测试环境清理完成');
} }