diff --git a/frontend/e2e/auth-shelf.spec.ts b/frontend/e2e/auth-shelf.spec.ts new file mode 100644 index 0000000..6064b24 --- /dev/null +++ b/frontend/e2e/auth-shelf.spec.ts @@ -0,0 +1,68 @@ +import { expect, test, type APIRequestContext } from "@playwright/test"; +import { adminApi, creds, ensureSampleBook } from "./helpers/api"; +import { scanA11y } from "./helpers/axe"; + +let admin: APIRequestContext; +let bookId: number; +let title: string; + +test.beforeAll(async () => { + admin = await adminApi(); + bookId = await ensureSampleBook(admin, "e2e"); + // 标题以 API 实况为准:后端 titleOf 可能改写文件名(下划线→空格等),书架卡按真实标题找。 + title = ((await (await admin.get(`/api/books/${bookId}`)).json()) as { title: string }).title; +}); + +test.afterAll(async () => { + await admin?.dispose(); +}); + +test("登录 → 书架 → CBZ 阅读翻页 → 书签 → 退出", async ({ page }) => { + const { user, pass } = creds(); + + // 登录页 + axe + await page.goto("/login"); + await scanA11y(page, "login"); + await page.fill("#login-user", user); + await page.fill("#login-pass", pass); + await page.getByRole("button", { name: "登录", exact: true }).click(); + await expect(page).toHaveURL(/\/$/); + + // 书架 + axe;样本卡可见并点进阅读器。 + // 卡片用「结构 + 文本」定位:可访问名匹配会同时命中卡片的《…》操作菜单钮(含书名 aria-label)。 + const card = page.locator(".group > button", { hasText: title }); + await expect(card).toBeVisible(); + await scanA11y(page, "shelf"); + await card.click(); + await expect(page).toHaveURL(new RegExp(`/book/${bookId}$`)); + + // 阅读器(chrome 默认展开)+ axe + const reader = page.getByLabel("漫画阅读器,点按左右两侧翻屏,点按中间显示工具栏"); + await expect(reader).toBeVisible(); + await expect(page.getByText("1/3")).toBeVisible(); + await scanA11y(page, "reader-chrome"); + + // 右缘点击翻屏 → 页码前进。 + // 单次 scrollBy 一屏未必把视口中心推过第 1 页底(页高 > 一屏),故按实况重试点击直到 2/3。 + const box = await reader.boundingBox(); + if (!box) throw new Error("阅读器容器无 boundingBox"); + await expect(async () => { + await page.mouse.click(box.x + box.width * 0.9, box.y + box.height / 2); + await expect(page.getByText("2/3")).toBeVisible(); + }).toPass({ timeout: 20_000 }); + + // 导航抽屉(本 fixture 无内嵌目录,抽屉直接落在书签面板)→ 加书签 → toast + await page.getByRole("button", { name: "导航", exact: true }).click(); + const nav = page.locator("#reader-nav"); + await expect(nav).toBeVisible(); + await nav.getByRole("button", { name: "加书签" }).click(); + await expect(page.getByText("已加书签")).toBeVisible(); + await page.getByRole("button", { name: "关闭导航" }).click(); + + // 回书架 → 退出登录 + await page.getByRole("link", { name: "书架" }).click(); + await expect(page).toHaveURL(/\/$/); + await page.getByRole("button", { name: new RegExp(`账户 ${user}`) }).click(); + await page.getByText("退出登录").click(); + await expect(page).toHaveURL(/\/login/); +}); diff --git a/frontend/e2e/helpers/axe.ts b/frontend/e2e/helpers/axe.ts index 78c369f..eda2d52 100644 --- a/frontend/e2e/helpers/axe.ts +++ b/frontend/e2e/helpers/axe.ts @@ -12,16 +12,34 @@ interface Waiver { * 仅允许「属于将被 ② 阅读器改版重写的旧 chrome」的问题进这里,reason 必须写明归属。 * 例:{ page: "reader-chrome", rule: "color-contrast", reason: "旧 rd-* chrome 配色,② 迁移 shadcn token 后移除本豁免" } */ -const WAIVERS: Waiver[] = []; +const WAIVERS: Waiver[] = [ + { + page: "reader-chrome", + rule: "color-contrast", + reason: + "旧 chrome:Reader.tsx 头栏(书架返回钮/书名/库名/进度)用 stone-* 配色,对比不足;② 阅读器改版迁移 shadcn token 后移除本豁免", + }, +]; export async function scanA11y(page: Page, name: string): Promise { + // 等 CSS 动画/过渡收尾(如 fx-rise 入场淡入)再扫:动画途中采样会因不透明度混色造成 contrast 误报;2s 上限兜底长动画 + await page.evaluate(() => + Promise.race([ + Promise.allSettled(document.getAnimations().map((a) => a.finished)), + new Promise((r) => setTimeout(r, 2000)), + ]), + ); const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze(); if (results.violations.length > 0) { console.log( `[axe:${name}]`, JSON.stringify( - results.violations.map((v) => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })), + results.violations.map((v) => ({ + id: v.id, + impact: v.impact, + nodes: v.nodes.map((n) => n.target.join(" ")), + })), null, 2, ), diff --git a/frontend/src/index.css b/frontend/src/index.css index c2a6d41..a4f3965 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -16,7 +16,7 @@ --secondary: oklch(0.95 0.003 80); --secondary-foreground: oklch(0.32 0.01 70); --muted: oklch(0.95 0.003 80); - --muted-foreground: oklch(0.55 0.008 70); + --muted-foreground: oklch(0.52 0.008 70); --accent: oklch(0.68 0.16 46); --accent-strong: oklch(0.55 0.17 46); --accent-foreground: oklch(0.985 0.002 80); diff --git a/frontend/src/readers/CbzReader.tsx b/frontend/src/readers/CbzReader.tsx index 34d701b..155111b 100644 --- a/frontend/src/readers/CbzReader.tsx +++ b/frontend/src/readers/CbzReader.tsx @@ -312,6 +312,8 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome >
{