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

290 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});