// 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()', 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) })