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'); }); });