import { test, expect } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; /** * 用户核心旅程测试(严格模式) * * 双模式执行: * - 无真实凭证:显式跳过(test.skip) * - 有真实凭证:严格断言 2xx/3xx */ const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080'; const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5176'; const DEFAULT_TEST_API_KEY = 'test-api-key-000000000000'; const DEFAULT_TEST_USER_TOKEN = 'test-e2e-token'; interface TestData { activityId: number; apiKey: string; userToken: string; userId: number; shortCode: string; baseUrl: string; apiBaseUrl: string; } function loadTestData(): TestData { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const testDataPath = path.join(__dirname, '..', '.e2e-test-data.json'); const defaultData: TestData = { activityId: 1, apiKey: DEFAULT_TEST_API_KEY, userToken: process.env.E2E_USER_TOKEN || DEFAULT_TEST_USER_TOKEN, userId: 10001, shortCode: 'test123', baseUrl: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5176', apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8080', }; try { if (fs.existsSync(testDataPath)) { const data = JSON.parse(fs.readFileSync(testDataPath, 'utf-8')); return { ...defaultData, ...data }; } } catch (error) { console.warn('无法加载测试数据,使用默认值'); } return defaultData; } function hasRealApiCredentials(data: TestData): boolean { return Boolean( data.apiKey && data.userToken && data.apiKey !== DEFAULT_TEST_API_KEY && data.userToken !== DEFAULT_TEST_USER_TOKEN ); } // 加载测试数据 const testData = loadTestData(); const useRealCredentials = hasRealApiCredentials(testData); const E2E_STRICT = process.env.E2E_STRICT === 'true'; test.describe('🎯 用户核心旅程测试', () => { // 首页不需要凭证,始终执行 test('🏠 首页加载(无需凭证)', async ({ page }) => { await test.step('访问首页', async () => { await page.goto('/'); await page.waitForLoadState('networkidle'); await expect(page.locator('#app')).toBeAttached(); }); }); if (!useRealCredentials) { // 严格模式下无真实凭证时必须失败,非严格模式才跳过 if (E2E_STRICT) { test('📊 活动列表API(需要真实凭证)', async () => { throw new Error('严格模式需要真实凭证(E2E_USER_TOKEN),但未提供有效凭证,测试失败'); }); } else { test.skip('📊 活动列表API(需要真实凭证)', async ({ request }) => { // 此测试需要真实凭证,无凭证时跳过 }); } } else { // 有真实凭证时严格断言 test('🏠 首页加载', async ({ page }) => { await test.step('访问首页', async () => { await page.goto('/'); await page.waitForLoadState('networkidle'); await expect(page.locator('#app')).toBeAttached(); }); }); test('📊 活动列表API - 严格断言', async ({ request }) => { const response = await request.get(`${API_BASE_URL}/api/v1/activities`, { headers: { 'X-API-Key': testData.apiKey, 'Authorization': `Bearer ${testData.userToken}`, }, }); const status = response.status(); // 严格断言:只接受 2xx/3xx expect( status, `活动列表API应返回2xx/3xx,实际${status}` ).toBeGreaterThanOrEqual(200); expect( status, `活动列表API应返回2xx/3xx,实际${status}` ).toBeLessThan(400); }); test('📊 活动详情API - 严格断言', async ({ request }) => { const response = await request.get(`${API_BASE_URL}/api/v1/activities/${testData.activityId}`, { headers: { 'X-API-Key': testData.apiKey, 'Authorization': `Bearer ${testData.userToken}`, }, }); const status = response.status(); // 严格断言:2xx/3xx expect( status, `活动详情API应返回2xx/3xx,实际${status}` ).toBeGreaterThanOrEqual(200); expect( status, `活动详情API应返回2xx/3xx,实际${status}` ).toBeLessThan(400); }); test('🏆 排行榜API - 严格断言', async ({ request }) => { const response = await request.get(`${API_BASE_URL}/api/v1/activities/${testData.activityId}/leaderboard`, { headers: { 'X-API-Key': testData.apiKey, 'Authorization': `Bearer ${testData.userToken}`, }, }); const status = response.status(); // 严格断言:2xx/3xx expect( status, `排行榜API应返回2xx/3xx,实际${status}` ).toBeGreaterThanOrEqual(200); expect( status, `排行榜API应返回2xx/3xx,实际${status}` ).toBeLessThan(400); }); test('🔗 短链API - 严格断言', async ({ request }) => { const response = await request.post( `${API_BASE_URL}/api/v1/internal/shorten`, { data: { originalUrl: 'https://example.com/test', activityId: testData.activityId, }, headers: { 'Content-Type': 'application/json', 'X-API-Key': testData.apiKey, 'Authorization': `Bearer ${testData.userToken}`, }, } ); const status = response.status(); // 严格断言:201创建成功或2xx expect( [200, 201], `短链API应返回200/201,实际${status}` ).toContain(status); }); test('📈 分享统计API - 严格断言', async ({ request }) => { const response = await request.get(`${API_BASE_URL}/api/v1/share/metrics?activityId=${testData.activityId}`, { headers: { 'X-API-Key': testData.apiKey, 'Authorization': `Bearer ${testData.userToken}`, }, }); const status = response.status(); // 严格断言:2xx/3xx expect( status, `分享统计API应返回2xx/3xx,实际${status}` ).toBeGreaterThanOrEqual(200); expect( status, `分享统计API应返回2xx/3xx,实际${status}` ).toBeLessThan(400); }); test('🎫 API Key验证端点 - 严格断言', async ({ request }) => { const response = await request.post( `${API_BASE_URL}/api/v1/keys/validate`, { data: { apiKey: testData.apiKey }, headers: { 'Content-Type': 'application/json', }, } ); const status = response.status(); // 严格断言:200成功 expect( status, `API Key验证应返回200,实际${status}` ).toBe(200); }); } }); test.describe('📱 响应式布局测试', () => { test('移动端布局检查', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto(FRONTEND_URL); await page.waitForLoadState('networkidle'); await expect(page.locator('#app')).toBeAttached(); }); test('平板端布局检查', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await page.goto(FRONTEND_URL); await page.waitForLoadState('networkidle'); await expect(page.locator('#app')).toBeAttached(); }); test('桌面端布局检查', async ({ page }) => { await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto(FRONTEND_URL); await page.waitForLoadState('networkidle'); await expect(page.locator('#app')).toBeAttached(); }); }); test.describe('⚡ 性能测试', () => { test('后端健康检查响应时间', async ({ request }) => { const startTime = Date.now(); const response = await request.get(`${API_BASE_URL}/actuator/health`); const responseTime = Date.now() - startTime; expect(response.status()).toBe(200); expect(responseTime, '健康检查响应时间应小于 2000ms').toBeLessThan(2000); }); test('前端页面加载时间', async ({ page }) => { const startTime = Date.now(); await page.goto(FRONTEND_URL); await page.waitForLoadState('networkidle'); const loadTime = Date.now() - startTime; await expect(page.locator('#app')).toBeAttached(); expect(loadTime, '页面加载时间应小于 6000ms').toBeLessThan(6000); }); }); test.describe('🔒 错误处理测试', () => { test('处理无效的活动ID', async ({ page }) => { await page.goto(`${FRONTEND_URL}/?activityId=999999999`); await page.waitForLoadState('networkidle'); await expect(page.locator('#app')).toBeAttached(); }); test('处理无效 API 端点 - 严格断言', async ({ request }) => { const response = await request.get(`${API_BASE_URL}/api/v1/non-existent-endpoint`, { headers: { 'X-API-Key': testData.apiKey, 'Authorization': `Bearer ${testData.userToken}`, }, }); const status = response.status(); // 无效端点应返回404(而不是500或2xx) // 但如果用了真实凭证且有权限,可能返回403(禁止访问不存在的资源) // 所以这里只排除服务器错误和成功响应 // 4xx 客户端错误是预期行为 expect( [400, 401, 403, 404, 499], `无效API端点应返回4xx客户端错误,实际${status}` ).toContain(status); }); });