2026-03-23 13:02:36 +08:00
|
|
|
|
import { test, expect, type Page } from '@playwright/test'
|
2026-03-02 13:31:54 +08:00
|
|
|
|
import fs from 'node:fs'
|
|
|
|
|
|
import path from 'node:path'
|
2026-03-23 13:02:36 +08:00
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
|
|
|
|
const __dirname = path.dirname(__filename)
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
|
|
|
|
|
const evidenceDir = process.env.E2E_EVIDENCE_DIR
|
|
|
|
|
|
? path.resolve(process.env.E2E_EVIDENCE_DIR)
|
|
|
|
|
|
: path.resolve(__dirname, '../../../evidence/run-unknown')
|
|
|
|
|
|
|
|
|
|
|
|
const ensureDir = (dir: string) => {
|
|
|
|
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const appendLog = (filePath: string, line: string) => {
|
|
|
|
|
|
ensureDir(path.dirname(filePath))
|
2026-03-23 13:02:36 +08:00
|
|
|
|
fs.appendFileSync(filePath, `${line}\n`, { encoding: 'utf8' })
|
2026-03-02 13:31:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const consoleLogPath = path.join(evidenceDir, 'e2e/console.log')
|
|
|
|
|
|
const networkLogPath = path.join(evidenceDir, 'e2e/network.log')
|
|
|
|
|
|
|
|
|
|
|
|
const logConsole = (type: string, text: string) => {
|
|
|
|
|
|
appendLog(consoleLogPath, `[${new Date().toISOString()}] ${type}: ${text}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const logNetwork = (line: string) => {
|
|
|
|
|
|
appendLog(networkLogPath, `[${new Date().toISOString()}] ${line}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
const attachUnauthorizedTracker = (page: Page) => {
|
|
|
|
|
|
const unauthorizedApiResponses: string[] = []
|
2026-03-02 13:31:54 +08:00
|
|
|
|
page.on('response', (res) => {
|
2026-03-23 13:02:36 +08:00
|
|
|
|
const status = res.status()
|
2026-03-02 13:31:54 +08:00
|
|
|
|
const url = res.url()
|
2026-03-23 13:02:36 +08:00
|
|
|
|
if (url.includes('/api/') && (status === 401 || status === 403)) {
|
|
|
|
|
|
unauthorizedApiResponses.push(`${status} ${res.request().method()} ${url}`)
|
2026-03-02 13:31:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-03-23 13:02:36 +08:00
|
|
|
|
return unauthorizedApiResponses
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 稳定性修复:统一等待应用真正可交互,替代固定 sleep。
|
|
|
|
|
|
const waitForAdminReady = async (page: Page) => {
|
|
|
|
|
|
await page.waitForLoadState('domcontentloaded')
|
|
|
|
|
|
await expect(page.locator('#app')).toBeAttached({ timeout: 15000 })
|
|
|
|
|
|
await expect(page.getByText('Mosquito Admin')).toBeVisible({ timeout: 15000 })
|
|
|
|
|
|
await expect(page.getByText('演示模式', { exact: true })).toBeVisible({ timeout: 15000 })
|
|
|
|
|
|
}
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
|
|
|
|
|
test.describe.serial('Admin E2E (real backend)', () => {
|
2026-03-23 13:02:36 +08:00
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
|
|
// 每个测试前清理localStorage,确保测试状态干净
|
|
|
|
|
|
// 但为了 demo 模式正常工作,需要预置演示用户信息
|
|
|
|
|
|
await page.addInitScript(() => {
|
|
|
|
|
|
localStorage.clear()
|
|
|
|
|
|
// 预置演示模式超级管理员用户信息(用于 demo 模式权限加载)
|
|
|
|
|
|
const demoUser = {
|
|
|
|
|
|
id: 'demo-super-admin',
|
|
|
|
|
|
name: '超级管理员',
|
|
|
|
|
|
email: 'demo@mosquito.com',
|
|
|
|
|
|
role: 'super_admin'
|
|
|
|
|
|
}
|
|
|
|
|
|
localStorage.setItem('mosquito_user', JSON.stringify(demoUser))
|
|
|
|
|
|
localStorage.setItem('mosquito_token', 'demo_token_' + Date.now())
|
|
|
|
|
|
localStorage.setItem('userRole', 'super_admin')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
page.on('console', (msg) => logConsole(msg.type(), msg.text()))
|
|
|
|
|
|
page.on('pageerror', (err) => logConsole('pageerror', err.message))
|
|
|
|
|
|
page.on('requestfailed', (req) => {
|
|
|
|
|
|
logNetwork(`requestfailed ${req.method()} ${req.url()} ${req.failure()?.errorText ?? ''}`)
|
|
|
|
|
|
})
|
|
|
|
|
|
page.on('response', (res) => {
|
|
|
|
|
|
const url = res.url()
|
|
|
|
|
|
if (url.includes('/api/')) {
|
|
|
|
|
|
logNetwork(`response ${res.status()} ${res.request().method()} ${url}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
test('dashboard renders correctly', async ({ page }) => {
|
|
|
|
|
|
const unauthorizedApiResponses = attachUnauthorizedTracker(page)
|
|
|
|
|
|
|
2026-03-02 13:31:54 +08:00
|
|
|
|
await page.goto('/')
|
2026-03-23 13:02:36 +08:00
|
|
|
|
await waitForAdminReady(page)
|
|
|
|
|
|
// 路由配置: / 重定向到 /dashboard
|
|
|
|
|
|
await expect(page).toHaveURL(/\/dashboard/)
|
|
|
|
|
|
|
|
|
|
|
|
// P0修复:管理台正常页面出现401/403时必须失败,避免“页面壳子存在即通过”。
|
|
|
|
|
|
expect(unauthorizedApiResponses, `dashboard出现未授权API响应: ${unauthorizedApiResponses.join(' | ')}`).toEqual([])
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
console.log('✅ Dashboard页面加载成功')
|
2026-03-02 13:31:54 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
test('users page loads', async ({ page }) => {
|
|
|
|
|
|
const unauthorizedApiResponses = attachUnauthorizedTracker(page)
|
|
|
|
|
|
|
2026-03-02 13:31:54 +08:00
|
|
|
|
await page.goto('/users')
|
2026-03-23 13:02:36 +08:00
|
|
|
|
await waitForAdminReady(page)
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
// 等待页面URL变为/users
|
|
|
|
|
|
await expect(page).toHaveURL(/\/users/, { timeout: 15000 })
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
// 检查页面没有重定向到403(说明有权限访问)
|
|
|
|
|
|
await expect(page).not.toHaveURL(/\/403/)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查页面是否显示用户相关内容(可能有多种方式渲染)
|
|
|
|
|
|
// 尝试查找"用户管理"标题(使用heading role更精确)
|
|
|
|
|
|
// 强断言:页面必须包含用户相关内容
|
|
|
|
|
|
await expect(
|
|
|
|
|
|
page.getByRole('heading', { name: '用户管理' }),
|
|
|
|
|
|
'用户管理页面应包含用户相关内容'
|
|
|
|
|
|
).toBeVisible({ timeout: 10000 })
|
|
|
|
|
|
|
|
|
|
|
|
// P0修复:用户管理页若出现401/403,必须显式失败。
|
|
|
|
|
|
expect(unauthorizedApiResponses, `users页出现未授权API响应: ${unauthorizedApiResponses.join(' | ')}`).toEqual([])
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
console.log('✅ 用户页面加载成功')
|
2026-03-02 13:31:54 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
test('forbidden page loads', async ({ page }) => {
|
2026-03-02 13:31:54 +08:00
|
|
|
|
await page.goto('/403')
|
2026-03-23 13:02:36 +08:00
|
|
|
|
await waitForAdminReady(page)
|
2026-03-02 13:31:54 +08:00
|
|
|
|
|
2026-03-23 13:02:36 +08:00
|
|
|
|
// 稳定性修复:校验 403 页关键文案,避免仅检查 #app 导致“假通过”。
|
|
|
|
|
|
await expect(page.getByText('403')).toBeVisible({ timeout: 15000 })
|
|
|
|
|
|
await expect(page).toHaveURL(/\/403/)
|
|
|
|
|
|
console.log('✅ 403页面加载成功')
|
2026-03-02 13:31:54 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|