215 lines
7.8 KiB
JavaScript
215 lines
7.8 KiB
JavaScript
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',
|
||
},
|
||
});
|
||
|
||
// 登录非创作者(登录后自动进入角色库)
|
||
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);
|
||
});
|
||
});
|