228 lines
8.6 KiB
TypeScript
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');
|
|
});
|
|
});
|