- 角色授权:权限矩阵抽屉与角色创建,按模块分组勾选 - 系统监控:总览、趋势、接口与事件页,15 秒自动刷新, 请求可中断、窗口切换暂停,详情抽屉支持导出 - T+ 同步:同步进度条组件与工作台状态拆分 - 工资:试用期面板、产品提取弹窗与无权限占位页 - 路由权限:按权限码控制菜单与页面可达性 影响范围:前端全部页面 验证:随本次后端联调本地人工核对,e2e 脚本入库待跑
126 lines
11 KiB
JavaScript
126 lines
11 KiB
JavaScript
/** 合成数据验证监控交互和故障状态,不登录真实账号、不写业务数据。 */
|
||
import assert from 'node:assert/strict'
|
||
import fs from 'node:fs'
|
||
import { createRequire } from 'node:module'
|
||
|
||
const require = createRequire(import.meta.url)
|
||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/Administrator/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
||
const base = process.env.MONITOR_UI_URL || 'http://127.0.0.1:5193'
|
||
const output = process.env.MONITOR_UI_OUTPUT || '.ai'
|
||
fs.mkdirSync(output, { recursive: true })
|
||
const browser = await chromium.launch({ headless: true, channel: 'msedge' })
|
||
const context = await browser.newContext({ viewport: { width: 1366, height: 1050 }, permissions: ['clipboard-read', 'clipboard-write'] })
|
||
const page = await context.newPage()
|
||
const errors = [], requests = [], checks = []
|
||
let failed = false, role = 'admin', empty = false
|
||
const at = '2026-11-02T10:00:00', id = 'a'.repeat(32)
|
||
const metric = { count: 48, errors_5xx: 3, errors_4xx: 2, error_rate: 6.25, p95_ms: 1200, max_ms: 2600, last_error_at: at }
|
||
const incident = { id: 1, instance_id: 'synthetic-instance', kind: 'HEALTH', code: 'WORKER_OFFLINE', component: 'tplus', level: 'warning', message: '采集进程心跳中断,请检查托管任务', request_id: null, method: null, route: null, status_code: null, duration_ms: null, batch_no: 'SYNTHETIC-BATCH', first_seen: at, last_seen: at, occurrences: 4, resolved_at: null }
|
||
const event = { ...incident, id: 2, kind: 'REQUEST_ERROR', code: 'HTTP_5XX', component: 'http', level: 'error', message: '接口处理异常,请按请求编号检索服务端日志。', request_id: id, method: 'GET', route: '/api/v1/base/products/{id}', status_code: 500, duration_ms: 2600, occurrences: 1, batch_no: null }
|
||
page.on('pageerror', error => errors.push(error.message))
|
||
await page.clock.install({ time: new Date('2026-11-02T10:00:00+08:00') })
|
||
await page.addInitScript(() => localStorage.setItem('ys_token', 'synthetic-monitor-browser-test'))
|
||
await page.route(url => url.pathname.startsWith('/api/'), async route => {
|
||
const request = route.request(), url = new URL(request.url()), path = url.pathname
|
||
requests.push({ path, query: Object.fromEntries(url.searchParams) })
|
||
assert.equal(request.method(), 'GET')
|
||
if (path.endsWith('/auth/me')) return route.fulfill({ json: { code: 200, message: 'success', data: { id: 1, username: 'synthetic', real_name: '监控页面验证', roles: [role], permissions: role === 'admin' ? ['system:monitor:view', 'tplus:view', 'dashboard:view'] : ['base:products:view'], is_payroll_maker: false, can_write_payroll: false, payroll_approval_enabled: false } } })
|
||
if (path.includes('/monitor/') && failed) return route.fulfill({ status: 503, json: { code: 503, message: '合成连接故障', data: null } })
|
||
let data = {}
|
||
if (path.endsWith('/monitor/overview')) data = {
|
||
status: empty ? 'unknown' : 'warning', environment: '合成演练', version: '0.1.0', instance_id: 'synthetic-instance', uptime_seconds: 12345,
|
||
collected_at: empty ? null : at, last_persisted_at: empty ? null : at, collection_status: empty ? 'unknown' : 'normal', collection_message: empty ? '等待首次采集' : '采集正常', dropped_events: 0,
|
||
resources: empty ? {} : { cpu_percent: 32.8, memory_percent: 48.2, disk_percent: 55.4, disk_free_gb: 124.5, process_cpu_percent: 2.3, process_memory_mb: 185, scope: '合成数据,仅用于页面验证' },
|
||
requests: empty ? { count: 0, error_rate: 0, p95_ms: null, max_ms: 0, errors_4xx: 0, errors_5xx: 0, last_error_at: null } : metric,
|
||
dependencies: empty ? [] : ['应用', 'MySQL', 'Redis', 'T+ 采集进程'].map(name => ({ name, status: name.startsWith('T+') ? 'warning' : 'normal', message: name.startsWith('T+') ? '心跳中断,请查看同步任务。' : '连接正常。', checked_at: at, duration_ms: 2 })),
|
||
alerts: empty ? [] : [incident], tasks: { status: 'warning', message: '存在采集异常,请查看相关任务。', enabled: true, worker_phase: 'STOPPED', heartbeat_at: at, last_success_at: at, next_retry_at: at, alerts: [{ code: 'WORKER_OFFLINE', message: incident.message, batch_no: incident.batch_no }] },
|
||
}
|
||
else if (path.endsWith('/monitor/trends')) data = { step_minutes: 1, points: empty ? [] : Array.from({ length: 60 }, (_, i) => ({ at: `2026-11-02T09:${String(i).padStart(2, '0')}:00`, sample_count: i === 25 ? 0 : 4, cpu_percent: i === 25 ? null : 25 + i % 13, memory_percent: i === 25 ? null : 42 + i % 8, disk_percent: 55, request_count: i === 25 ? null : i % 9 + 1, error_rate: i === 25 ? null : i % 17 === 0 ? 5 : 0, p95_ms: i === 25 ? null : 100 + i % 7 * 100 })) }
|
||
else if (path.endsWith('/monitor/interfaces')) data = { list: empty ? [] : [{ method: event.method, route: event.route, ...metric }], truncated: false }
|
||
else if (path.endsWith('/monitor/events')) data = { list: empty ? [] : [event], total: empty ? 0 : 1, pageNum: 1, pageSize: 20 }
|
||
else if (/\/monitor\/events\/\d+$/.test(path)) data = path.endsWith('/1') ? incident : event
|
||
else if (path.endsWith('/monitor/diagnostic')) return route.fulfill({ headers: { 'content-disposition': 'attachment; filename="monitor.json"' }, json: { generated_at: at, note: '脱敏诊断摘要' } })
|
||
else if (path.endsWith('/tplus/overview')) data = { is_configured: false, configuration_message: '合成任务定位验证', status_counts: {}, total_staged_records: 0, latest_audit: null }
|
||
else if (path.endsWith('/tplus/batches/SYNTHETIC-BATCH')) data = { batch_no: 'SYNTHETIC-BATCH', status: 'FAILED', task_type: 'READ', create_time: at, can_retry: false }
|
||
else data = { list: [], total: 0, pageNum: 1, pageSize: 20 }
|
||
return route.fulfill({ json: { code: 200, message: 'success', data } })
|
||
})
|
||
|
||
try {
|
||
await page.goto(`${base}/system/monitor`)
|
||
await page.getByRole('heading', { name: '系统监控', exact: true }).waitFor()
|
||
await page.getByText('合成演练', { exact: true }).waitFor()
|
||
await page.getByText(incident.message, { exact: true }).waitFor()
|
||
await page.clock.runFor(600)
|
||
for (const width of [1366, 1920, 768]) {
|
||
await page.setViewportSize({ width, height: 1050 })
|
||
await page.screenshot({ path: `${output}/monitor-ui-${width}.png`, fullPage: true })
|
||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, `页面横向溢出 ${width}`)
|
||
}
|
||
checks.push('1366/1920/768 布局、真实图表渲染与无横向溢出')
|
||
await page.setViewportSize({ width: 1366, height: 1050 })
|
||
await page.getByRole('tab', { name: '接口性能', exact: true }).click()
|
||
await page.getByRole('button', { name: '查看异常', exact: true }).click()
|
||
await page.getByRole('button', { name: '详情', exact: true }).click()
|
||
await page.getByRole('heading', { name: '诊断事件详情', exact: true }).waitFor()
|
||
await page.getByRole('button', { name: '复制请求编号', exact: true }).click()
|
||
assert.equal(await page.evaluate(() => navigator.clipboard.readText()), id)
|
||
await page.screenshot({ path: `${output}/monitor-event-detail.png`, fullPage: true })
|
||
await page.getByRole('button', { name: '关闭此对话框' }).click()
|
||
checks.push('接口排行联动异常列表、抽屉详情与请求编号复制')
|
||
await page.getByRole('button', { name: '诊断摘要', exact: true }).click()
|
||
await page.getByRole('tab', { name: '任务与同步', exact: true }).click()
|
||
await page.getByRole('button', { name: '查看相关批次', exact: true }).click()
|
||
await page.waitForURL('**/system/tplus?batch=SYNTHETIC-BATCH')
|
||
await page.getByRole('dialog').waitFor()
|
||
checks.push('诊断摘要下载、T+ 批次跳转与原详情打开')
|
||
await page.goto(`${base}/system/monitor`)
|
||
await page.getByText('合成演练', { exact: true }).waitFor()
|
||
await Promise.all([page.waitForResponse(response => response.url().includes('/monitor/trends') && response.url().includes('hours=24')),
|
||
page.getByText('24 小时', { exact: true }).click()])
|
||
checks.push('历史时间窗口筛选')
|
||
failed = true
|
||
await page.getByRole('button', { name: '刷新', exact: true }).click()
|
||
await page.getByText('监控连接失败,请检查服务或稍后刷新。', { exact: true }).waitFor()
|
||
assert.equal(await page.locator('.el-message--error').count(), 0, '轮询故障不应重复弹窗')
|
||
assert.ok(await page.getByText('合成演练', { exact: true }).isVisible(), '应保留上次成功状态')
|
||
checks.push('故障保留上次状态、明确未知且不弹窗轰炸')
|
||
failed = false; empty = true
|
||
await page.getByRole('button', { name: '刷新', exact: true }).click()
|
||
await page.getByText('当前状态尚不能确认', { exact: true }).waitFor()
|
||
assert.equal(await page.getByText('当前未发现活动异常', { exact: true }).count(), 0)
|
||
checks.push('初次无数据与正常状态明确区分')
|
||
const count = () => requests.filter(item => item.path.endsWith('/monitor/overview')).length
|
||
await page.locator('.toolbar .el-switch').click()
|
||
assert.equal(await page.getByRole('switch', { name: '自动刷新监控' }).isChecked(), false)
|
||
const disabledCount = count()
|
||
await page.clock.runFor(31000)
|
||
assert.equal(count(), disabledCount)
|
||
await page.locator('.toolbar .el-switch').click()
|
||
await page.evaluate(() => { Object.defineProperty(document, 'hidden', { configurable: true, get: () => true }); document.dispatchEvent(new Event('visibilitychange')) })
|
||
const hiddenCount = count()
|
||
await page.clock.runFor(46000)
|
||
assert.equal(count(), hiddenCount)
|
||
await page.evaluate(() => { Object.defineProperty(document, 'hidden', { configurable: true, get: () => false }); document.dispatchEvent(new Event('visibilitychange')) })
|
||
const resumed = page.waitForRequest(request => request.url().includes('/monitor/overview'))
|
||
await page.clock.runFor(16000); await resumed
|
||
checks.push('关闭自动刷新、隐藏页面暂停、恢复可见后继续采集视图')
|
||
await page.getByRole('menuitem', { name: 'T+ 数据同步', exact: true }).click()
|
||
const leftCount = count()
|
||
await page.clock.runFor(46000)
|
||
assert.equal(count(), leftCount)
|
||
checks.push('离开监控页停止轮询')
|
||
role = 'user'
|
||
await page.goto(`${base}/system/monitor`)
|
||
await page.waitForURL('**/base/products')
|
||
assert.equal(await page.getByRole('heading', { name: '系统监控', exact: true }).count(), 0)
|
||
checks.push('普通用户直接访问被拒绝,菜单不暴露监控入口')
|
||
assert.deepEqual(errors, [])
|
||
fs.writeFileSync(`${output}/monitor-browser-result.json`, JSON.stringify({ checks, errors, requests: requests.length }, null, 2))
|
||
console.log(JSON.stringify({ checks, errors, requests: requests.length }))
|
||
} catch (error) {
|
||
await page.screenshot({ path: `${output}/monitor-browser-failure.png`, fullPage: true })
|
||
fs.writeFileSync(`${output}/monitor-browser-failure.txt`, String(error) + '\n' + JSON.stringify({ errors, requests, url: page.url() }) + '\n' + await page.locator('body').innerText())
|
||
throw error
|
||
} finally { await browser.close() }
|