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 }); // 页模式 + RTL 冒烟:开设置面板 → 单页 → 右缘翻页 → 右开本(RTL) 方向翻转 → 回长卷。 await page.getByRole("button", { name: "更多阅读设置" }).click(); await page.getByRole("button", { name: "单页" }).click(); await expect(page.getByText(/^1\/3 · /)).toBeVisible(); // 模式切换保持当前页:此时在第 2 页 // 回到第 1 页:左缘点击 const box2 = await reader.boundingBox(); if (!box2) throw new Error("阅读器容器无 boundingBox"); await page.mouse.click(box2.x + box2.width * 0.1, box2.y + box2.height / 2); await expect(page.getByText(/^1\/3 · /)).toBeVisible(); // 右缘点击 → 第 2 页 await page.mouse.click(box2.x + box2.width * 0.9, box2.y + box2.height / 2); await expect(page.getByText(/^2\/3 · /)).toBeVisible(); // RTL:右缘点击变后退 await page.getByRole("button", { name: "右开本(RTL)" }).click(); await page.mouse.click(box2.x + box2.width * 0.9, box2.y + box2.height / 2); await expect(page.getByText(/^1\/3 · /)).toBeVisible(); // 回长卷 await page.getByRole("button", { name: "长卷", exact: true }).click(); await expect(page.getByText("1/3")).toBeVisible(); // 导航抽屉(本 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/); });