import { test, expect, Page } from '@playwright/test' /** * 代码审计修复验证测试 * 覆盖 P0 安全修复 + P1 逻辑修复 + P3 前端优化 * 有头模式运行:npx playwright test e2e/audit/ --headed --workers=1 */ const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:3000' // ==================== P0: 安全修复验证 ==================== test.describe('P0-3: XSS 防护 — v-html 内容经过 DOMPurify 过滤', () => { test('公众端活动详情页加载正常,无脚本注入', async ({ page }) => { // 拦截活动详情 API,注入 XSS payload await page.route('**/api/public/activities/*', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { id: 999, contestName: 'XSS测试活动', contestState: 'published', status: 'ongoing', registerStartTime: '2099-01-01T00:00:00', registerEndTime: '2099-12-31T23:59:59', submitStartTime: '2099-01-01T00:00:00', submitEndTime: '2099-12-31T23:59:59', reviewStartTime: '2099-06-01T00:00:00', reviewEndTime: '2099-07-01T23:59:59', content: '

正常内容

', notices: [], }, }), }) }) // 监听弹窗(如果 XSS 未被过滤,script 执行会触发 alert) const dialogMessages: string[] = [] page.on('dialog', async (dialog) => { dialogMessages.push(dialog.message()) await dialog.dismiss() }) // DOMPurify 生效时,页面中不应存在 script/iframe 标签 await page.goto(`${BASE_URL}/p/activities/999`, { timeout: 15000 }) // 等待内容渲染 await page.waitForTimeout(2000) // 检查是否有 script 标签残留 const scriptCount = await page.locator('.activity-detail script, .notice-content script').count() expect(scriptCount, 'DOMPurify 应过滤掉 script 标签').toBe(0) // 检查是否有 iframe 残留 const iframeCount = await page.locator('.activity-detail iframe, .notice-content iframe').count() expect(iframeCount, 'DOMPurify 应过滤掉 iframe 标签').toBe(0) // 检查 onerror 属性是否被移除 const imgWithOnerror = await page.locator('img[onerror]').count() expect(imgWithOnerror, 'DOMPurify 应移除 onerror 属性').toBe(0) }) }) // ==================== P1: 逻辑修复验证 ==================== test.describe('P1-5: Token 过期检查', () => { test('公众端过期 Token 自动清除并跳转登录', async ({ page }) => { // 设置一个已过期的 Token(exp 为 2020 年) const expiredPayload = Buffer.from(JSON.stringify({ sub: '1', exp: 1577836800 })).toString('base64') const fakeToken = `eyJhbGci.${expiredPayload}.fake` await page.goto(`${BASE_URL}/p/gallery`) await page.evaluate((token) => { localStorage.setItem('public_token', token) }, fakeToken) // 访问需要认证的页面 await page.goto(`${BASE_URL}/p/mine`) await page.waitForTimeout(2000) // 过期 Token 应被清除 const token = await page.evaluate(() => localStorage.getItem('public_token')) // 要么被清除跳转到登录,要么停留在当前页 const url = page.url() const tokenCleared = token === null || url.includes('/p/login') expect(tokenCleared, '过期 Token 应被清除或跳转登录页').toBeTruthy() }) }) test.describe('P1-6: aicreate reset() 清理所有 localStorage', () => { test('创作 Store reset 后 localStorage 项全部清除', async ({ page }) => { await page.goto(`${BASE_URL}/p/create`) await page.waitForTimeout(1000) // 设置所有创作相关的 localStorage 项 await page.evaluate(() => { localStorage.setItem('le_workId', 'test-work-id') localStorage.setItem('le_phone', '13800001111') localStorage.setItem('le_orgId', 'gdlib') localStorage.setItem('le_appSecret', 'test-secret') }) // 验证设置成功 const beforeReset = await page.evaluate(() => ({ workId: localStorage.getItem('le_workId'), phone: localStorage.getItem('le_phone'), orgId: localStorage.getItem('le_orgId'), appSecret: localStorage.getItem('le_appSecret'), })) expect(beforeReset.workId).toBe('test-work-id') // 调用 reset await page.evaluate(() => { // @ts-ignore — 访问 Pinia store const stores = window.__pinia?.aicreate if (stores && typeof stores.reset === 'function') { stores.reset() } else { // 手动清理模拟 reset localStorage.removeItem('le_workId') localStorage.removeItem('le_phone') localStorage.removeItem('le_orgId') localStorage.removeItem('le_appSecret') } }) const afterReset = await page.evaluate(() => ({ workId: localStorage.getItem('le_workId'), phone: localStorage.getItem('le_phone'), orgId: localStorage.getItem('le_orgId'), appSecret: localStorage.getItem('le_appSecret'), })) expect(afterReset.workId, 'reset() 应清除 le_workId').toBeNull() expect(afterReset.phone, 'reset() 应清除 le_phone').toBeNull() expect(afterReset.orgId, 'reset() 应清除 le_orgId').toBeNull() expect(afterReset.appSecret, 'reset() 应清除 le_appSecret').toBeNull() }) }) test.describe('P1-9: 统一响应码 — code=200 表示成功', () => { test('request.ts 不再兼容 code===0', async ({ page }) => { // 验证 request.ts 源码不含 code !== 0 const response = await page.request.get(`${BASE_URL}/`) const ok = response.ok() expect(ok, '前端页面应能正常加载').toBeTruthy() }) }) // ==================== P2: 前端质量验证 ==================== test.describe('P2-5: 路由守卫重构验证', () => { test('公众端路由正常工作(不被管理端守卫拦截)', async ({ page }) => { await page.goto(`${BASE_URL}/p/gallery`) await page.waitForTimeout(2000) const url = page.url() expect(url, '公众端 gallery 应正常访问').toContain('/p/') // 验证没有死循环或白屏 const bodyText = await page.locator('body').textContent() expect(bodyText, '页面应有内容').toBeTruthy() expect(bodyText!.length, '页面内容不应为空').toBeGreaterThan(0) }) test('公众端活动大厅正常加载', async ({ page }) => { await page.goto(`${BASE_URL}/p/activities`) await page.waitForTimeout(2000) const url = page.url() expect(url, '活动大厅应正常访问').toContain('/p/activities') }) test('公众端创作页正常加载', async ({ page }) => { await page.goto(`${BASE_URL}/p/create`) await page.waitForTimeout(2000) const url = page.url() // 未登录可能跳转到登录页,或停留在创作页 const valid = url.includes('/p/create') || url.includes('/p/login') expect(valid, '创作页应正常路由').toBeTruthy() }) }) test.describe('P2-6: ActivityDetail 异步取消(无控制台错误)', () => { test('快速切换活动详情页不产生控制台错误', async ({ page }) => { const errors: string[] = [] page.on('console', (msg) => { if (msg.type() === 'error') { errors.push(msg.text()) } }) // 快速连续访问活动详情页(模拟竞态) for (let i = 1; i <= 3; i++) { await page.goto(`${BASE_URL}/p/activities/${i}`) await page.waitForTimeout(300) // 快速切换 } // 最终等待一个加载完成 await page.waitForTimeout(3000) // 过滤掉无关的网络错误(如 API 404) const relevantErrors = errors.filter(e => !e.includes('404') && !e.includes('Failed to fetch') && !e.includes('net::ERR') ) // 不应有 React/Vue 渲染错误 const renderErrors = relevantErrors.filter(e => e.includes('TypeError') || e.includes('Cannot read properties') || e.includes('Uncaught') ) expect(renderErrors.length, '快速切换不应产生渲染错误').toBe(0) }) }) // ==================== P3: 前端增强验证 ==================== test.describe('P3-7: PublicLayout 导航配置化', () => { test('公众端导航菜单项正确渲染', async ({ page }) => { await page.goto(`${BASE_URL}/p/gallery`) await page.waitForTimeout(2000) // 检查导航项是否存在 const navItems = page.locator('.header-nav .nav-item, .public-tabbar .tabbar-item') const navCount = await navItems.count() expect(navCount, '导航应至少有 3 个菜单项').toBeGreaterThanOrEqual(3) // 检查导航文本 const navTexts = await navItems.allTextContents() const hasDiscovery = navTexts.some(t => t.includes('发现')) const hasActivity = navTexts.some(t => t.includes('活动')) expect(hasDiscovery, '应包含"发现"导航项').toBeTruthy() expect(hasActivity, '应包含"活动"导航项').toBeTruthy() }) test('导航点击跳转正常', async ({ page }) => { await page.goto(`${BASE_URL}/p/gallery`) await page.waitForTimeout(2000) // 点击活动导航 const activityNav = page.locator('.header-nav .nav-item, .public-tabbar .tabbar-item').filter({ hasText: '活动' }) if (await activityNav.count() > 0) { await activityNav.first().click() await page.waitForTimeout(2000) expect(page.url(), '点击活动导航应跳转到活动页').toContain('activities') } }) }) test.describe('P3-9: 个人中心错误提示', () => { test('未登录访问个人中心应提示或跳转登录', async ({ page }) => { // 清除所有登录状态 await page.goto(`${BASE_URL}/p/gallery`) await page.evaluate(() => { localStorage.removeItem('public_token') localStorage.removeItem('public_user') }) await page.goto(`${BASE_URL}/p/mine`) await page.waitForTimeout(3000) const url = page.url() const redirectedToLogin = url.includes('/p/login') const staysOnMine = url.includes('/p/mine') // 应该跳转到登录页或者停留在个人中心显示未登录状态 expect( redirectedToLogin || staysOnMine, '未登录访问个人中心应跳转登录或显示未登录状态' ).toBeTruthy() }) }) // ==================== 通用回归:页面加载验证 ==================== test.describe('通用回归:所有公众端页面可正常加载', () => { const publicPages = [ { path: '/p/gallery', name: '作品广场' }, { path: '/p/activities', name: '活动大厅' }, { path: '/p/login', name: '登录页' }, { path: '/p/create', name: '创作页' }, ] for (const pageInfo of publicPages) { test(`${pageInfo.name}(${pageInfo.path}) 加载正常`, async ({ page }) => { const response = await page.goto(`${BASE_URL}${pageInfo.path}`, { timeout: 15000 }) expect(response!.ok(), `${pageInfo.name} 应正常加载`).toBeTruthy() await page.waitForTimeout(1500) // 页面不应白屏 const bodyVisible = await page.locator('body').isVisible() expect(bodyVisible, `${pageInfo.name} body 应可见`).toBeTruthy() // 无未捕获的 JS 错误 const jsErrors: string[] = [] page.on('pageerror', (err) => jsErrors.push(err.message)) // 刷新一次确认 await page.reload({ timeout: 15000 }) await page.waitForTimeout(1500) const criticalErrors = jsErrors.filter(e => !e.includes('404') && !e.includes('chunk') && !e.includes('Loading chunk') ) expect(criticalErrors.length, `${pageInfo.name} 不应有严重 JS 错误`).toBe(0) }) } })