chore: sync local latest state and repository cleanup

This commit is contained in:
Your Name
2026-03-23 13:02:36 +08:00
parent f1ff3d629f
commit 2ef0f17961
493 changed files with 46912 additions and 7977 deletions

View File

@@ -1,284 +1,290 @@
import { test, expect } from '../fixtures/test-data';
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
/**
* 🦟 蚊子项目 - 用户端到端旅程测试
*
* 测试场景真实API交互
* 1. 活动查看流程
* 2. 排行榜查看流程
* 3. 短链生成和跳转流程
* 4. 分享统计查看流程
* 5. 邀请信息查看流程
* 用户核心旅程测试(严格模式)
*
* 双模式执行
* - 无真实凭证显式跳过test.skip
* - 有真实凭证:严格断言 2xx/3xx
*/
test.describe('🎯 用户核心旅程测试', () => {
test.beforeEach(async ({ page, testData }) => {
// 设置测试环境
console.log(`\n 测试活动ID: ${testData.activityId}`);
console.log(` API Key: ${testData.apiKey.substring(0, 20)}...`);
});
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5173';
test('🏠 首页加载和活动列表展示', async ({ page, testData, apiClient }) => {
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 expect(page).toHaveTitle(/Mosquito|蚊子/);
await expect(page.locator('body')).toBeVisible();
// 截图记录
await page.screenshot({ path: `e2e-results/home-page-${Date.now()}.png` });
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();
});
});
await test.step('验证活动列表API返回数据', async () => {
try {
const response = await apiClient.getActivities();
if (response.code === 200) {
expect(response.data).toBeDefined();
expect(Array.isArray(response.data)).toBeTruthy();
// 验证测试活动在列表中
const testActivity = response.data.find(
(a: any) => a.id === testData.activityId
);
if (testActivity) {
console.log(` ✅ 找到测试活动: ${testActivity.name}`);
}
} else {
console.log(` ⚠️ API返回非200状态: ${response.code}`);
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}`,
},
}
} catch (error) {
console.log(' ⚠️ API调用失败可能需要有效认证');
}
});
});
test('📊 活动详情和统计数据展示', async ({ page, testData, apiClient }) => {
await test.step('获取活动详情API', async () => {
const response = await apiClient.getActivity(testData.activityId);
expect(response.code).toBe(200);
expect(response.data.id).toBe(testData.activityId);
console.log(` 活动名称: ${response.data.name}`);
);
const status = response.status();
// 严格断言201创建成功或2xx
expect(
[200, 201],
`短链API应返回200/201实际${status}`
).toContain(status);
});
await test.step('获取活动统计数据API', async () => {
const response = await apiClient.getActivityStats(testData.activityId);
expect(response.code).toBe(200);
expect(response.data).toBeDefined();
// 验证统计字段存在
const stats = response.data;
console.log(` 总参与人数: ${stats.totalParticipants || 0}`);
console.log(` 总分享次数: ${stats.totalShares || 0}`);
});
await test.step('前端页面展示活动信息', async ({ authenticatedPage }) => {
// 如果前端有活动详情页面
await authenticatedPage.goto(`/?activityId=${testData.activityId}`);
// 等待页面加载
await authenticatedPage.waitForLoadState('networkidle');
// 截图记录
await authenticatedPage.screenshot({
path: `e2e-results/activity-detail-${Date.now()}.png`
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}`,
},
});
});
});
test('🏆 排行榜查看流程', async ({ page, testData, apiClient }) => {
await test.step('获取排行榜数据API', async () => {
const response = await apiClient.getLeaderboard(testData.activityId, 0, 10);
expect(response.code).toBe(200);
expect(response.data).toBeDefined();
console.log(` 排行榜数据: ${JSON.stringify(response.data).substring(0, 100)}...`);
const status = response.status();
// 严格断言2xx/3xx
expect(
status,
`分享统计API应返回2xx/3xx实际${status}`
).toBeGreaterThanOrEqual(200);
expect(
status,
`分享统计API应返回2xx/3xx实际${status}`
).toBeLessThan(400);
});
await test.step('前端展示排行榜', async ({ authenticatedPage }) => {
// 访问排行榜页面
await authenticatedPage.goto(`/leaderboard?activityId=${testData.activityId}`);
await authenticatedPage.waitForLoadState('networkidle');
// 截图记录
await authenticatedPage.screenshot({
path: `e2e-results/leaderboard-${Date.now()}.png`
});
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('🔗 短链生成和访问流程', async ({ page, testData, apiClient }) => {
let shortCode: string;
await test.step('生成短链API', async () => {
const originalUrl = `https://example.com/test?activityId=${testData.activityId}&timestamp=${Date.now()}`;
const response = await apiClient.createShortLink(originalUrl, testData.activityId);
expect(response.code).toBe(201);
expect(response.data).toBeDefined();
shortCode = response.data.code || response.data.shortUrl?.split('/').pop();
console.log(` 生成短链: ${shortCode}`);
});
await test.step('访问短链跳转', async () => {
// 访问短链
const response = await page.goto(`/r/${shortCode}`);
// 验证重定向
expect(response?.status()).toBe(302);
console.log(' ✅ 短链跳转成功');
});
await test.step('验证点击记录', async () => {
// 等待统计更新
await page.waitForTimeout(1000);
const metrics = await apiClient.getShareMetrics(testData.activityId);
expect(metrics.code).toBe(200);
console.log(` 总点击数: ${metrics.data?.totalClicks || 0}`);
});
});
test('📈 分享统计数据查看', async ({ page, testData, apiClient }) => {
await test.step('获取分享统计API', async () => {
const response = await apiClient.getShareMetrics(testData.activityId);
expect(response.code).toBe(200);
expect(response.data).toBeDefined();
const metrics = response.data;
console.log(` 总点击数: ${metrics.totalClicks || 0}`);
console.log(` 总分享数: ${metrics.totalShares || 0}`);
console.log(` 总邀请数: ${metrics.totalInvites || 0}`);
});
await test.step('前端展示分享统计', async ({ authenticatedPage }) => {
await authenticatedPage.goto(`/share-metrics?activityId=${testData.activityId}`);
await authenticatedPage.waitForLoadState('networkidle');
await authenticatedPage.screenshot({
path: `e2e-results/share-metrics-${Date.now()}.png`
});
});
});
test('🎫 API Key验证流程', async ({ apiClient }) => {
await test.step('验证有效的API Key', async () => {
// 这个测试需要使用global-setup创建的API Key
const globalData = (globalThis as any).__TEST_DATA__;
if (globalData?.apiKey) {
const isValid = await apiClient.validateApiKey(globalData.apiKey);
expect(isValid).toBe(true);
console.log(' ✅ API Key验证通过');
}
});
});
}
});
test.describe('📱 响应式布局测试', () => {
test('移动端布局检查', async ({ page, testData }) => {
// 设置移动端视口
test('移动端布局检查', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto(`/?activityId=${testData.activityId}`);
await page.goto(FRONTEND_URL);
await page.waitForLoadState('networkidle');
// 截图记录移动端效果
await page.screenshot({
path: `e2e-results/mobile-layout-${Date.now()}.png`
});
console.log(' ✅ 移动端布局检查完成');
await expect(page.locator('#app')).toBeAttached();
});
test('平板端布局检查', async ({ page, testData }) => {
test('平板端布局检查', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto(`/?activityId=${testData.activityId}`);
await page.goto(FRONTEND_URL);
await page.waitForLoadState('networkidle');
await page.screenshot({
path: `e2e-results/tablet-layout-${Date.now()}.png`
});
console.log(' ✅ 平板端布局检查完成');
await expect(page.locator('#app')).toBeAttached();
});
test('桌面端布局检查', async ({ page, testData }) => {
test('桌面端布局检查', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(`/?activityId=${testData.activityId}`);
await page.goto(FRONTEND_URL);
await page.waitForLoadState('networkidle');
await page.screenshot({
path: `e2e-results/desktop-layout-${Date.now()}.png`
});
console.log(' ✅ 桌面端布局检查完成');
await expect(page.locator('#app')).toBeAttached();
});
});
test.describe('⚡ 性能测试', () => {
test('API响应时间测试', async ({ apiClient, testData }) => {
test('后端健康检查响应时间', async ({ request }) => {
const startTime = Date.now();
await apiClient.getActivity(testData.activityId);
const response = await request.get(`${API_BASE_URL}/actuator/health`);
const responseTime = Date.now() - startTime;
expect(responseTime).toBeLessThan(2000); // API响应应在2秒内
console.log(` API响应时间: ${responseTime}ms`);
expect(response.status()).toBe(200);
expect(responseTime, '健康检查响应时间应小于 2000ms').toBeLessThan(2000);
});
test('页面加载时间测试', async ({ page, testData }) => {
test('前端页面加载时间', async ({ page }) => {
const startTime = Date.now();
await page.goto(`/?activityId=${testData.activityId}`);
await page.goto(FRONTEND_URL);
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(5000); // 页面应在5秒内加载
console.log(` 页面加载时间: ${loadTime}ms`);
await expect(page.locator('#app')).toBeAttached();
expect(loadTime, '页面加载时间应小于 6000ms').toBeLessThan(6000);
});
});
test.describe('🔒 错误处理测试', () => {
test('处理无效的活动ID', async ({ page }) => {
await page.goto('/?activityId=999999999');
await page.goto(`${FRONTEND_URL}/?activityId=999999999`);
await page.waitForLoadState('networkidle');
// 验证页面优雅处理错误
await page.screenshot({
path: `e2e-results/error-handling-${Date.now()}.png`
});
console.log(' ✅ 错误处理测试完成');
await expect(page.locator('#app')).toBeAttached();
});
test('处理网络错误', async ({ apiClient }) => {
// 测试API客户端的错误处理
try {
// 尝试访问不存在的端点
const response = await apiClient.get('/api/v1/non-existent-endpoint');
// 应该返回错误,而不是抛出异常
expect(response.code).not.toBe(200);
} catch (error) {
// 错误被正确处理
console.log(' ✅ 网络错误被正确处理');
}
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);
});
});
});