290 lines
9.4 KiB
TypeScript
290 lines
9.4 KiB
TypeScript
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);
|
||
});
|
||
}); |