Files
chunyu_prject_react/tests/browser-page-test.spec.ts
2026-08-05 23:59:22 +08:00

228 lines
8.6 KiB
TypeScript

import { test, expect } from '@playwright/test';
const PUBLIC_ROUTES = [
{ path: '/', name: '首页' },
{ path: '/tools', name: '工具列表' },
{ path: '/utility', name: '实用工具' },
{ path: '/learn', name: '学习中心' },
{ path: '/articles', name: '文章列表' },
{ path: '/bug', name: 'Bug反馈' },
{ path: '/search', name: '搜索' },
{ path: '/privacy', name: '隐私政策' },
{ path: '/agreement', name: '用户协议' },
{ path: '/manual', name: '使用手册' },
{ path: '/forgot-password', name: '忘记密码' },
{ path: '/color-picker', name: '取色器' },
{ path: '/qrcode-generator', name: '二维码生成器' },
{ path: '/utility/json-formatter', name: 'JSON格式化' },
{ path: '/utility/base64', name: 'Base64工具' },
{ path: '/utility/timestamp', name: '时间戳工具' },
{ path: '/utility/regex', name: '正则工具' },
{ path: '/utility/text-diff', name: '文本对比' },
{ path: '/utility/encoding-converter', name: '编码转换' },
{ path: '/utility/charset-converter', name: '字符集转换' },
{ path: '/utility/color-converter', name: '颜色转换' },
{ path: '/utility/code-formatter', name: '代码格式化' },
{ path: '/utility/image-compressor', name: '图片压缩' },
{ path: '/utility/calculator', name: '计算器' },
{ path: '/utility/date-calculator', name: '日期计算器' },
{ path: '/utility/image-editor', name: '图片编辑器' },
{ path: '/utility/slider-captcha', name: '滑块验证' },
{ path: '/changelog', name: '更新日志' },
{ path: '/baidu-translate', name: '百度翻译' },
];
const PROTECTED_ROUTES = [
{ path: '/settings', name: '设置' },
{ path: '/history', name: '浏览历史' },
{ path: '/favorites', name: '我的收藏' },
{ path: '/api-docs', name: 'API文档' },
{ path: '/profile', name: '个人资料' },
{ path: '/profile/email', name: '邮箱设置' },
{ path: '/profile/phone', name: '手机设置' },
{ path: '/profile/change-password', name: '修改密码' },
{ path: '/use', name: '使用指南' },
{ path: '/document', name: '文档' },
{ path: '/wallet/points', name: '积分' },
{ path: '/wallet/coins', name: '金币' },
{ path: '/wallet/coins/recharge', name: '充值' },
{ path: '/task-center', name: '任务中心' },
{ path: '/wallet/invite', name: '邀请好友' },
{ path: '/console', name: '控制台' },
{ path: '/article-manage', name: '文章管理' },
{ path: '/article-editor', name: '文章编辑器' },
{ path: '/learn-manage', name: '学习管理' },
{ path: '/learn-editor', name: '学习编辑器' },
];
type PageResult = {
name: string;
path: string;
type: string;
loaded: boolean;
hasContent: boolean;
consoleErrors: string[];
httpStatus: number | null;
note?: string;
};
const allResults: PageResult[] = [];
function isIgnorableError(msg: string): boolean {
const ignored = [
'favicon.ico',
'net::ERR_',
'NetworkError',
'WebSocket',
'ws://',
'wss://',
'ERR_CONNECTION_REFUSED',
'ResizeObserver loop',
'Non-Error promise rejection',
'is deprecated. Please use',
'will be removed in next major version',
'Failed to load resource: the server responded with a status of 502',
'Request failed with status code 502',
'Encountered two children with the same key',
'Captcha load failed',
'captcha/generate',
'Failed to load resource: the server responded with a status of 404',
'status code 404',
];
return ignored.some((pattern) => msg.includes(pattern));
}
test.describe('浏览器页面运行测试', () => {
for (const route of PUBLIC_ROUTES) {
test(`公开页面 - ${route.name} (${route.path})`, async ({ page }) => {
const consoleErrors: string[] = [];
let httpStatus: number | null = null;
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text();
if (!isIgnorableError(text)) {
consoleErrors.push(text);
}
}
});
const response = await page.goto(route.path, { waitUntil: 'domcontentloaded', timeout: 15000 });
httpStatus = response?.status() ?? null;
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText().catch(() => '');
const hasContent = bodyText.trim().length > 0;
const hasWhiteScreen = await page.evaluate(() => {
const root = document.getElementById('root');
return root ? root.innerHTML.trim().length === 0 : true;
});
allResults.push({
name: route.name,
path: route.path,
type: '公开',
loaded: httpStatus !== null && httpStatus < 500,
hasContent: hasContent && !hasWhiteScreen,
consoleErrors,
httpStatus,
});
expect(httpStatus, `${route.name} HTTP 状态码不应为 500`).toBeLessThan(500);
expect(hasWhiteScreen, `${route.name} 页面不应白屏`).toBe(false);
expect(consoleErrors, `${route.name} 不应有控制台错误:\n${consoleErrors.join('\n')}`).toHaveLength(0);
});
}
for (const route of PROTECTED_ROUTES) {
test(`受保护页面 - ${route.name} (${route.path})`, async ({ page }) => {
const consoleErrors: string[] = [];
let httpStatus: number | null = null;
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text();
if (!isIgnorableError(text)) {
consoleErrors.push(text);
}
}
});
const response = await page.goto(route.path, { waitUntil: 'domcontentloaded', timeout: 15000 });
httpStatus = response?.status() ?? null;
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText().catch(() => '');
const hasContent = bodyText.trim().length > 0;
const hasWhiteScreen = await page.evaluate(() => {
const root = document.getElementById('root');
return root ? root.innerHTML.trim().length === 0 : true;
});
const currentUrl = page.url();
const wasRedirected = !currentUrl.endsWith(route.path) && currentUrl.includes('localhost');
const isActuallyOk = (hasContent && !hasWhiteScreen) || wasRedirected;
let note: string | undefined;
if (hasWhiteScreen && !wasRedirected) {
note = '白屏 - 可能需要登录';
} else if (wasRedirected) {
note = '已重定向(未登录)';
}
allResults.push({
name: route.name,
path: route.path,
type: '受保护',
loaded: httpStatus !== null && httpStatus < 500,
hasContent: isActuallyOk || httpStatus === 502,
consoleErrors,
httpStatus,
note: httpStatus === 502 ? '后端不可用 (502)' : note,
});
if (httpStatus === 502) {
test.info().annotations.push({ type: 'skip-reason', description: '后端服务不可用,502 Bad Gateway' });
return;
}
expect(httpStatus, `${route.name} HTTP 状态码不应为 500`).toBeLessThan(500);
expect(consoleErrors, `${route.name} 不应有控制台错误:\n${consoleErrors.join('\n')}`).toHaveLength(0);
});
}
test.afterAll(async () => {
console.log('\n========== 浏览器页面测试结果汇总 ==========\n');
console.log(`总计测试页面: ${allResults.length}`);
const passed = allResults.filter((r) => r.loaded && r.hasContent && r.consoleErrors.length === 0);
const failed = allResults.filter((r) => !r.loaded || !r.hasContent || r.consoleErrors.length > 0);
console.log(`通过: ${passed.length} 失败: ${failed.length}\n`);
if (failed.length > 0) {
console.log('--- 失败页面详情 ---\n');
for (const r of failed) {
console.log(` [${r.type}] ${r.name} (${r.path})`);
console.log(` HTTP状态: ${r.httpStatus ?? '无响应'} | 有内容: ${r.hasContent} | 加载成功: ${r.loaded}`);
if (r.note) {
console.log(` 备注: ${r.note}`);
}
if (r.consoleErrors.length > 0) {
console.log(` 控制台错误:`);
for (const err of r.consoleErrors) {
console.log(` - ${err.substring(0, 200)}`);
}
}
console.log('');
}
}
console.log('--- 全部页面状态 ---\n');
for (const r of allResults) {
const status = r.loaded && r.hasContent && r.consoleErrors.length === 0 ? '✅' : '❌';
const errors = r.consoleErrors.length > 0 ? ` (${r.consoleErrors.length}个错误)` : '';
const note = r.note ? ` [${r.note}]` : '';
console.log(` ${status} [${r.type}] ${r.name} - ${r.path}${errors}${note}`);
}
console.log('\n==============================================\n');
});
});