Files
wenzi/frontend/e2e/tests/user-journey.spec.ts

290 lines
9.4 KiB
TypeScript
Raw Normal View History

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:5173';
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:5173',
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);
});
});