const { test, expect } = require('@playwright/test'); const bcrypt = require('bcryptjs'); const { cleanDatabase, disconnect, prisma } = require('./fixtures/database'); test.describe('导航与可访问性', () => { test.beforeAll(async () => { await cleanDatabase(); }); test.afterAll(async () => { await cleanDatabase(); await disconnect(); }); test('首页显示两张卡片', async ({ page }) => { await page.goto('/'); await expect(page.locator('#landing')).toBeVisible(); // 两张卡片:open-characters 和 open-distill const cards = page.locator('[data-action="open-characters"], [data-action="open-distill"]'); await expect(cards).toHaveCount(2, { timeout: 5000 }); }); test('底部 tabBar 三个 tab 可见', async ({ page }) => { await page.goto('/'); await expect(page.locator('.tab-bar')).toBeVisible(); await expect(page.locator('[data-tab-action="tab-home"]')).toBeVisible(); await expect(page.locator('[data-tab-action="tab-distill"]')).toBeVisible(); await expect(page.locator('[data-tab-action="tab-mine"]')).toBeVisible(); }); test('tabBar 切换视图', async ({ page }) => { await page.goto('/'); // 点击蒸馏前任 tab await page.locator('[data-tab-action="tab-distill"]').click(); await expect(page.locator('#distill')).toBeVisible(); // 点击首页 tab await page.locator('[data-tab-action="tab-home"]').click(); await expect(page.locator('#landing')).toBeVisible(); // 点击我的 tab(未登录应跳转到 auth) await page.locator('[data-tab-action="tab-mine"]').click(); await expect(page.locator('#auth')).toBeVisible(); }); test('首页卡片点击进入对应视图', async ({ page }) => { await page.goto('/'); // 点击蒸馏前任卡片 await page.locator('[data-action="open-distill"]').first().click(); await expect(page.locator('#distill')).toBeVisible(); // 返回首页 await page.locator('[data-tab-action="tab-home"]').click(); await expect(page.locator('#landing')).toBeVisible(); // 点击「我的 XXX」卡片(未登录进入 auth) await page.locator('[data-action="open-characters"]').first().click(); await expect(page.locator('#auth')).toBeVisible(); }); test('关于页面可访问', async ({ page }) => { await page.goto('/'); await page.locator('[data-action="open-about"]').first().click(); await expect(page.locator('#about')).toBeVisible(); }); test('创作者入驻页面可访问', async ({ page }) => { await page.goto('/'); await page.locator('[data-action="open-onboarding"]').first().click(); await expect(page.locator('#onboarding')).toBeVisible(); }); test('FAQ 折叠展开', async ({ page }) => { await page.goto('/'); await page.locator('[data-action="open-about"]').first().click(); await expect(page.locator('#about')).toBeVisible(); // 点击第一个 FAQ 按钮(.faq-q,非 .faq-item__question) const faqButton = page.locator('.faq-q').first(); await faqButton.click(); // 验证 aria-expanded 变为 true await expect(faqButton).toHaveAttribute('aria-expanded', 'true'); // 再次点击折叠 await faqButton.click(); await expect(faqButton).toHaveAttribute('aria-expanded', 'false'); }); test('返回按钮基于历史记录导航', async ({ page }) => { await page.goto('/'); // 导航路径: landing → about → 返回 await page.locator('[data-action="open-about"]').first().click(); await expect(page.locator('#about')).toBeVisible(); // about 视图使用 data-action="back" await page.locator('#about [data-action="back"]').click(); await expect(page.locator('#landing')).toBeVisible(); }); test('跳过链接(a11y)存在且可用', async ({ page }) => { await page.goto('/'); const skipLink = page.locator('.skip-link'); // 跳过链接应存在 await expect(skipLink).toHaveCount(1); }); test('tabBar 有正确的 ARIA 角色', async ({ page }) => { await page.goto('/'); const tabbar = page.locator('.tab-bar'); await expect(tabbar).toHaveAttribute('role', 'tablist'); // tab-bar 内的按钮应有 role="tab" const tabs = page.locator('.tab-bar [data-tab-action]'); const count = await tabs.count(); expect(count).toBe(3); for (let i = 0; i < count; i++) { await expect(tabs.nth(i)).toHaveAttribute('role', 'tab'); } }); test('视图切换时 aria-live 播报', async ({ page }) => { await page.goto('/'); // #sr-announce 是 aria-live 区域,role="status" aria-live="polite" const liveRegion = page.locator('#sr-announce'); await expect(liveRegion).toHaveAttribute('aria-live', 'polite'); await expect(liveRegion).toHaveAttribute('role', 'status'); // 切换视图后 live region 应有内容 await page.locator('[data-action="open-about"]').first().click(); await expect(liveRegion).not.toHaveText(''); const liveText = await liveRegion.textContent(); expect(liveText.length).toBeGreaterThan(0); }); test('角色卡片支持键盘操作', async ({ page }) => { // 创建非创作者用户 + 角色 const bcrypt = require('bcryptjs'); const user = await prisma.user.create({ data: { account: 'e2e_keyboard_user', password: bcrypt.hashSync('Test123456', 10), isCreator: false, libraryName: '键盘测试库', }, }); await prisma.role.create({ data: { creatorId: user.id, displayName: '键盘测试角色', gender: 'female', personality: '测试', background: '测试', speechStyle: '测试', greeting: '测试', desc: '键盘测试', price: 9.9, status: 'running', reviewStatus: 'synced', }, }); // 登录非创作者(登录后自动进入角色库) await page.goto('/'); await page.locator('[data-action="open-characters"]').first().click(); await expect(page.locator('#auth')).toBeVisible(); await page.fill('[data-form="login"] [name="account"]', 'e2e_keyboard_user'); await page.fill('[data-form="login"] [name="password"]', 'Test123456'); await page.locator('[data-form="login"] button[type="submit"]').click(); await expect(page.locator('#role-library')).toBeVisible({ timeout: 5000 }); // 聚焦角色卡片(role-card 有 tabindex="0") const roleCard = page.locator('#role-list .role-card').first(); await roleCard.focus(); // 按 Enter 应进入详情 await page.keyboard.press('Enter'); await expect(page.locator('#role-detail')).toBeVisible({ timeout: 5000 }); // 清理 await prisma.role.deleteMany(); await prisma.user.deleteMany(); }); test('表单 label 与 input 关联', async ({ page }) => { await page.goto('/'); await page.locator('[data-action="open-characters"]').first().click(); await expect(page.locator('#auth')).toBeVisible(); // 检查登录表单的 input 存在 const accountInput = page.locator('[data-form="login"] [name="account"]'); await expect(accountInput).toBeVisible(); // 检查是否有对应的 label(包裹式 label 包含账号相关文案) const labelText = await page.locator('label').filter({ hasText: /账号|用户名|Account/i }).count(); expect(labelText).toBeGreaterThan(0); }); test('focus-visible 样式存在', async ({ page }) => { await page.goto('/'); // Tab 到第一个可聚焦元素 await page.keyboard.press('Tab'); // 聚焦元素应有可见的焦点样式(:focus-visible 使用 outline) const focused = await page.evaluate(() => { const el = document.activeElement; if (!el) return false; const styles = window.getComputedStyle(el); return styles.outlineStyle !== 'none' || styles.boxShadow !== 'none'; }); expect(focused).toBe(true); }); });