Files
openteam/web/scripts/wig-verify.cjs
T
Sakurasan b25e9ec8a7 前端: 按 Web Interface Guidelines 全面重做
组件:
- Button: transition-all→明确属性列表, touch-action, aria-busy
- Input: name/spellcheck/inputmode/aria-invalid/aria-describedby, focus-visible ring,
  label 关联, 暴露 focus() 供错误定位, required 标记
- Modal: Escape 关闭 + focus trap + 焦点归还, overscroll-behavior: contain,
  aria-labelledby, body 滚动锁, touch-action
- 新增 Toast(aria-live polite) + pinia toast store

页面:
- ConsoleLayout: skip link, <main> 语义标签, aside/nav aria-label, aria-current,
  装饰 SVG aria-hidden, Intl 金额, translate=no 品牌/代码
- Dashboard: Intl.NumberFormat, 骨架屏 loading, aria-live 错误
- Keys: 字段级错误+焦点定位, toast 反馈(复制/吊销), name 属性
- Usage: select 加 label, 分页/筛选同步 URL(可深链), Intl 日期/金额, 表格 caption
- Login/Register: 字段级校验+错误定位, spellcheck=false, inputmode, autocomplete
- Landing: router-link 替换 href, aria-hidden, text-balance, scroll-mt
- index.html: theme-color + color-scheme

验证: playwright 22 项断言全过, WCAG AA 对比度 9 组全过, 零控制台错误
2026-08-15 13:32:18 +08:00

99 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// WIG 合规验证:语义标签 / aria / focus / modal 交互
const { chromium } = require('playwright')
async function main() {
const browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
const errors = []
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message))
page.on('console', (m) => { if (m.type() === 'error') errors.push('console: ' + m.text().slice(0, 150)) })
const BASE = 'http://127.0.0.1:8088'
let pass = 0, fail = 0
const check = (name, ok) => { console.log(`${ok ? '✓' : '✗'} ${name}`); ok ? pass++ : fail++ }
// ---- Landing ----
await page.goto(BASE + '/', { waitUntil: 'networkidle' })
check('landing: h1 存在', await page.locator('h1').count() === 1)
check('landing: main 语义标签', await page.locator('main#landing-main').count() === 1)
check('landing: 导航用 router-link(<a>)', await page.locator('header nav a').count() >= 2)
check('landing: theme-color', await page.locator('meta[name="theme-color"]').count() === 1)
// ---- 注册(带字段级校验)----
const uname = 'wig' + Date.now().toString().slice(-6)
await page.goto(BASE + '/register', { waitUntil: 'networkidle' })
// 空提交 → 字段级错误
await page.click('button[type="submit"]')
await page.waitForTimeout(300)
check('register: 字段级错误显示', await page.locator('[role="alert"]').count() >= 1)
// 填错邮箱
await page.fill('input[name="username"]', uname)
await page.fill('input[name="email"]', 'not-an-email')
await page.fill('input[name="password"]', 'password123')
await page.fill('input[name="confirm"]', 'password123')
await page.click('button[type="submit"]')
await page.waitForTimeout(300)
check('register: 邮箱格式错误', await page.locator('text=邮箱格式不正确').count() === 1)
// 修正并提交
await page.fill('input[name="email"]', uname + '@test.com')
await page.click('button[type="submit"]')
await page.waitForURL('**/console/dashboard', { timeout: 10000 })
await page.waitForTimeout(1500)
// ---- Console ----
check('console: skip link', await page.locator('a[href="#main-content"]').count() === 1)
check('console: main 语义标签', await page.locator('main#main-content').count() === 1)
check('console: aside aria-label', await page.locator('aside[aria-label]').count() === 1)
check('console: 侧边栏卡片', await page.locator('section[aria-label="用量指标"] .rounded-lg').count() === 4)
check('console: 骨架屏已消失(loaded)', await page.locator('.animate-pulse').count() === 0)
// ---- Keys ----
await page.goto(BASE + '/console/keys', { waitUntil: 'networkidle' })
await page.waitForTimeout(500)
check('keys: 表格 caption', await page.locator('table caption.sr-only').count() === 1)
check('keys: 吊销按钮', await page.locator('button:has-text("吊销")').count() >= 0)
// 创建密钥 modal:Escape 关闭
await page.click('button:has-text("新建密钥")')
await page.waitForTimeout(300)
check('modal: dialog 打开', await page.locator('[role="dialog"]').count() === 1)
await page.keyboard.press('Escape')
await page.waitForTimeout(300)
check('modal: Escape 关闭', await page.locator('[role="dialog"]').count() === 0)
// 重新打开并创建
await page.click('button:has-text("新建密钥")')
await page.waitForTimeout(300)
await page.fill('input[name="key-name"]', 'wig-test')
await page.click('button:has-text("创建密钥")')
await page.waitForTimeout(600)
check('modal: 一次性明文展示', await page.locator('code.text-mint-300').count() === 1)
check('modal: 复制按钮 + toast', (await page.click('button:has-text("复制")'), true))
await page.waitForTimeout(300)
check('toast: aria-live 容器', await page.locator('[aria-live="polite"]').count() >= 1)
await page.click('button:has-text("完成")')
// ---- Usage ----
await page.goto(BASE + '/console/usage?model=gpt-4o-mini', { waitUntil: 'networkidle' })
await page.waitForTimeout(800)
check('usage: select 有 label', await page.locator('label:has(select)').count() === 1)
check('usage: URL 反映筛选状态', page.url().includes('model='))
check('usage: 分页 aria 标签', await page.locator('nav[aria-label="分页"]').count() === 1)
// ---- Tab 焦点可见性 ----
await page.goto(BASE + '/console/dashboard', { waitUntil: 'networkidle' })
await page.keyboard.press('Tab')
await page.waitForTimeout(200)
const focusedRing = await page.evaluate(() => {
const el = document.activeElement
if (!el) return 'none'
const s = getComputedStyle(el)
return s.outlineStyle === 'auto' || s.outlineWidth !== '0px' ? 'outline' : (s.boxShadow !== 'none' ? 'ring' : 'none')
})
check(`focus: Tab 首元素可见焦点 (${focusedRing})`, focusedRing !== 'none')
console.log(`\n结果: ${pass} 通过, ${fail} 失败`)
if (errors.length) console.log('控制台错误:', errors.slice(0, 5))
await browser.close()
process.exit(fail > 0 ? 1 : 0)
}
main().catch((e) => { console.error(e); process.exit(1) })