diff --git a/web/index.html b/web/index.html index 87c2838..1107717 100644 --- a/web/index.html +++ b/web/index.html @@ -4,6 +4,8 @@ + + openteam · LLM 中转网关 diff --git a/web/scripts/wig-verify.cjs b/web/scripts/wig-verify.cjs new file mode 100644 index 0000000..2332c34 --- /dev/null +++ b/web/scripts/wig-verify.cjs @@ -0,0 +1,98 @@ +// 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) }) diff --git a/web/src/components/ui/Button.vue b/web/src/components/ui/Button.vue index 373d059..7234da2 100644 --- a/web/src/components/ui/Button.vue +++ b/web/src/components/ui/Button.vue @@ -6,16 +6,22 @@ withDefaults( type?: 'button' | 'submit' loading?: boolean disabled?: boolean + ariaLabel?: string }>(), { variant: 'primary', size: 'md', type: 'button', loading: false, disabled: false }, ) + +defineEmits<{ (e: 'click', ev: MouseEvent): void }>() diff --git a/web/src/components/ui/Input.vue b/web/src/components/ui/Input.vue index 1eefeb6..9b8499f 100644 --- a/web/src/components/ui/Input.vue +++ b/web/src/components/ui/Input.vue @@ -1,5 +1,9 @@ diff --git a/web/src/components/ui/Modal.vue b/web/src/components/ui/Modal.vue index 72e4a2b..fae132d 100644 --- a/web/src/components/ui/Modal.vue +++ b/web/src/components/ui/Modal.vue @@ -1,23 +1,88 @@