Files
2026-09-16 19:27:25 +08:00

90 lines
4.1 KiB
TypeScript

import { expect, test, type APIRequestContext } from "@playwright/test";
import { adminApiRetry, creds, ensureSampleBook } from "./helpers/api";
import { scanA11y } from "./helpers/axe";
let admin: APIRequestContext;
let bookId: number;
let title: string;
test.beforeAll(async () => {
admin = await adminApiRetry();
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 expect(page.getByRole("region", { name: "阅读统计" })).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/);
});