From ef57b194f4bf9477b1bddd7b5bd787750dac14ad Mon Sep 17 00:00:00 2001 From: XingfenD Date: Wed, 16 Sep 2026 19:27:25 +0800 Subject: [PATCH] =?UTF-8?q?chore(frontend):=20tighten=20eslint/axe=20gates?= =?UTF-8?q?,=20remove=20reader=20waivers=20(B5=20done,=20spec=20=E2=91=A1?= =?UTF-8?q?=20complete)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/e2e/auth-shelf.spec.ts | 4 +- frontend/e2e/helpers/api.ts | 12 ++++++ frontend/e2e/helpers/axe.ts | 14 ++----- frontend/e2e/text-search.spec.ts | 15 ++++++- frontend/eslint.config.js | 19 --------- frontend/src/components/format-badge.tsx | 15 ++++--- frontend/src/components/reader-settings.tsx | 46 ++++++++++++++++----- frontend/src/components/ui/tabs.tsx | 7 +--- frontend/src/readers/CbzReader.tsx | 11 +++-- frontend/src/readers/EpubReader.tsx | 3 ++ frontend/src/readers/PdfReader.tsx | 2 + frontend/src/readers/TextReader.tsx | 10 ++++- 12 files changed, 99 insertions(+), 59 deletions(-) diff --git a/frontend/e2e/auth-shelf.spec.ts b/frontend/e2e/auth-shelf.spec.ts index 915efa5..e4207db 100644 --- a/frontend/e2e/auth-shelf.spec.ts +++ b/frontend/e2e/auth-shelf.spec.ts @@ -1,5 +1,5 @@ import { expect, test, type APIRequestContext } from "@playwright/test"; -import { adminApi, creds, ensureSampleBook } from "./helpers/api"; +import { adminApiRetry, creds, ensureSampleBook } from "./helpers/api"; import { scanA11y } from "./helpers/axe"; let admin: APIRequestContext; @@ -7,7 +7,7 @@ let bookId: number; let title: string; test.beforeAll(async () => { - admin = await adminApi(); + admin = await adminApiRetry(); bookId = await ensureSampleBook(admin, "e2e"); // 标题以 API 实况为准:后端 titleOf 可能改写文件名(下划线→空格等),书架卡按真实标题找。 title = ((await (await admin.get(`/api/books/${bookId}`)).json()) as { title: string }).title; diff --git a/frontend/e2e/helpers/api.ts b/frontend/e2e/helpers/api.ts index b2ceb30..b9c9b97 100644 --- a/frontend/e2e/helpers/api.ts +++ b/frontend/e2e/helpers/api.ts @@ -31,6 +31,18 @@ export async function adminApi(): Promise { return request.newContext({ baseURL: BASE, extraHTTPHeaders: { Authorization: `Bearer ${token}` } }); } +/** adminApi 登录带 429 退避重试:三 spec 并发 worker 的登录总数可能顶到后端 1min/5 次限流门,等窗口即可;仅消耗等待预算,不改用例语义 */ +export async function adminApiRetry(): Promise { + for (let i = 0; ; i++) { + try { + return await adminApi(); + } catch (e) { + if (i > 20 || !(e instanceof Error) || !e.message.includes("HTTP 429")) throw e; + await new Promise((r) => setTimeout(r, 3000)); + } + } +} + export async function ensureLibrary(ctx: APIRequestContext, name: string): Promise { const libs = (await (await ctx.get("/api/libraries")).json()) as { id: number; name: string }[]; const hit = libs.find((l) => l.name === name); diff --git a/frontend/e2e/helpers/axe.ts b/frontend/e2e/helpers/axe.ts index eda2d52..cc4d7b6 100644 --- a/frontend/e2e/helpers/axe.ts +++ b/frontend/e2e/helpers/axe.ts @@ -8,18 +8,10 @@ interface Waiver { } /** - * critical/serious 违规的逐条豁免清单(spec S3): - * 仅允许「属于将被 ② 阅读器改版重写的旧 chrome」的问题进这里,reason 必须写明归属。 - * 例:{ page: "reader-chrome", rule: "color-contrast", reason: "旧 rd-* chrome 配色,② 迁移 shadcn token 后移除本豁免" } + * critical/serious 违规的逐条豁免清单(④ 基建): + * ② 阅读器改版后旧 chrome 重写完毕,本期清空;新增豁免必须写明 reason 并控制在一次性条件。 */ -const WAIVERS: Waiver[] = [ - { - page: "reader-chrome", - rule: "color-contrast", - reason: - "旧 chrome:Reader.tsx 头栏(书架返回钮/书名/库名/进度)用 stone-* 配色,对比不足;② 阅读器改版迁移 shadcn token 后移除本豁免", - }, -]; +const WAIVERS: Waiver[] = []; export async function scanA11y(page: Page, name: string): Promise { // 等 CSS 动画/过渡收尾(如 fx-rise 入场淡入)再扫:动画途中采样会因不透明度混色造成 contrast 误报;2s 上限兜底长动画 diff --git a/frontend/e2e/text-search.spec.ts b/frontend/e2e/text-search.spec.ts index da7eea4..2f18103 100644 --- a/frontend/e2e/text-search.spec.ts +++ b/frontend/e2e/text-search.spec.ts @@ -1,12 +1,12 @@ import { expect, test, type APIRequestContext } from "@playwright/test"; -import { adminApi, creds, ensureBook, FIXTURE_TXT } from "./helpers/api"; +import { adminApiRetry, creds, ensureBook, FIXTURE_TXT } from "./helpers/api"; import { scanA11y } from "./helpers/axe"; let admin: APIRequestContext; let bookId: number; test.beforeAll(async () => { - admin = await adminApi(); + admin = await adminApiRetry(); // 文件名避免与 CBZ 样本书同名:fixture 用 e2e-sample-txt.txt(title=e2e-sample-txt) bookId = await ensureBook(admin, "e2e", "e2e-sample-txt.txt", FIXTURE_TXT); }); @@ -46,6 +46,17 @@ test("登录 → txt 阅读 → 全书搜索 → 跳转", async ({ page }) => { await scanA11y(page, "reader-chrome"); + // 设置面板冒烟:字号/行距即时生效 + 主题卡联动阅读面(data-rd 受控) + const pre = page.locator(".rd-surface pre"); + await expect(pre).toHaveCSS("font-size", "20px"); + await page.getByRole("button", { name: "更多阅读设置" }).click(); + await page.getByRole("button", { name: "加大字号" }).click(); + await expect(pre).toHaveCSS("font-size", "22px"); + await page.getByRole("button", { name: "1.8", exact: true }).click(); + await expect(pre).toHaveAttribute("style", /line-height: 1\.8/); + await page.getByRole("button", { name: "夜色主题" }).click(); + await expect(page.locator(".rd-surface").first()).toHaveAttribute("data-rd", "night"); + // 退出 await page.getByRole("link", { name: "书架" }).click(); await expect(page).toHaveURL(/\/$/); diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index f9e45dc..7489adc 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -21,25 +21,6 @@ export default tseslint.config( ], }, }, - { - // 旧阅读器 chrome 的点击区/交互元素将被 ② 阅读器改版迁入 radix 原语, - // 届时收严为 error;本期对存量文件降 warn(spec S1 决策) - files: [ - "src/readers/**/*.tsx", - "src/components/Bookmarks.tsx", - "src/components/reader-nav.tsx", - "src/components/rd-slider.tsx", - "src/pages/Reader.tsx", - ], - rules: { - "jsx-a11y/click-events-have-key-events": "warn", - "jsx-a11y/no-static-element-interactions": "warn", - "jsx-a11y/no-noninteractive-element-interactions": "warn", - // 键盘焦点容器:CBZ/PDF 阅读器根 div 用 tabIndex={0} 接方向键翻页, - // 与上面三条同属 ② 改版范围(radix 原语自带焦点管理),本期随块降 warn - "jsx-a11y/no-noninteractive-tabindex": "warn", - }, - }, { files: ["e2e/**/*.ts"], // e2e 是 Playwright 脚本,测试工具链的动态类型用法(如 locator/fixture 包装)允许 any diff --git a/frontend/src/components/format-badge.tsx b/frontend/src/components/format-badge.tsx index a114016..4d6baa9 100644 --- a/frontend/src/components/format-badge.tsx +++ b/frontend/src/components/format-badge.tsx @@ -2,17 +2,20 @@ import { Badge } from "@/components/ui/badge"; import type { Format } from "@/api/types"; const CLASSES: Record = { - cbz: "bg-orange-500/15 text-orange-600 ring-orange-500/25 dark:text-orange-300", - pdf: "bg-rose-500/15 text-rose-600 ring-rose-500/25 dark:text-rose-300", - epub: "bg-sky-500/15 text-sky-600 ring-sky-500/25 dark:text-sky-300", - txt: "bg-emerald-500/15 text-emerald-600 ring-emerald-500/25 dark:text-emerald-300", - md: "bg-teal-500/15 text-teal-600 ring-teal-600/25 dark:text-teal-300", + cbz: "border-transparent bg-orange-500/15 text-orange-900 ring-0 dark:text-orange-300", + pdf: "border-transparent bg-rose-500/15 text-rose-900 ring-0 dark:text-rose-300", + epub: "border-transparent bg-sky-500/15 text-sky-900 ring-0 dark:text-sky-300", + txt: "border-transparent bg-emerald-500/15 text-emerald-900 ring-0 dark:text-emerald-300", + md: "border-transparent bg-teal-500/15 text-teal-900 ring-0 dark:text-teal-300", }; /** 格式徽章:按类型着色,色彩之外始终伴随文字,不依赖颜色单独传义(承接旧 ui.ts formatBadge)。 */ export function FormatBadge({ fmt, className }: { fmt: Format; className?: string }) { return ( - + {fmt} ); diff --git a/frontend/src/components/reader-settings.tsx b/frontend/src/components/reader-settings.tsx index 6741f05..7717bc4 100644 --- a/frontend/src/components/reader-settings.tsx +++ b/frontend/src/components/reader-settings.tsx @@ -3,7 +3,15 @@ import { useState, type ReactNode } from "react"; import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; import { useGlobalDark } from "@/components/theme"; -import { RD_THEMES, RD_THEME_LABEL, FONT_SIZES, resolveRdTheme, useReaderPrefs, type RdTheme } from "@/lib/readerPrefs"; +import { + RD_THEMES, + RD_THEME_LABEL, + FONT_SIZES, + resolveRdTheme, + useReaderPrefs, + type RdTheme, + type ReaderPrefs, +} from "@/lib/readerPrefs"; export interface ReaderSettingsProps { /** 进度滑条:value/max 页或千分比由调用方定,onChange 收整数;EPUB 无全局分数故可省 */ @@ -16,6 +24,11 @@ export interface ReaderSettingsProps { showFont?: boolean; /** 字号调节走哪一档(text=bumpText / epub=bumpEpub);未传时不渲染 A−/A+ */ onFont?: (d: number) => void; + /** 当前字号档位(用于 A−/A+ 边界禁用);不传按文本框位 */ + fontIdx?: number; + /** 受控主题:传入时用调用方状态,保证阅读面 data-rd 与设置栏同步(不传则组件内自持) */ + themeMode?: ReaderPrefs["themeMode"]; + onThemeMode?: (m: ReaderPrefs["themeMode"]) => void; /** 「更多设置」展开区内容(按格式给:CBZ 翻页/RTL/连读/预读;Text 行距/边距;EPUB 字号/行距/边距) */ extra?: ReactNode; } @@ -24,10 +37,23 @@ export interface ReaderSettingsProps { const SWATCH: Record = { paper: "#faf8f4", sepia: "#f0e6d2", night: "#171512" }; /** 统一阅读设置栏:常驻行 = 进度滑条 + 位置 + 左区 + 主题卡(含跟随全局)+ A−/A+;「更多设置」展开区按格式给。 */ -export function ReaderSettings({ slider, position, left, showFont = false, onFont, extra }: ReaderSettingsProps) { - const pr = useReaderPrefs(); +export function ReaderSettings({ + slider, + position, + left, + showFont = false, + onFont, + fontIdx, + themeMode, + onThemeMode, + extra, +}: ReaderSettingsProps) { + const internal = useReaderPrefs(); + // 主题受控:调用方给状态时用调用方(阅读面 data-rd 联动);否则退回组件内自持(独立使用/测试) + const mode = themeMode ?? internal.prefs.themeMode; + const setMode = onThemeMode ?? internal.setThemeMode; const globalDark = useGlobalDark(); - const rd = resolveRdTheme(pr.prefs.themeMode, globalDark); + const rd = resolveRdTheme(mode, globalDark); const [open, setOpen] = useState(false); return ( @@ -47,8 +73,8 @@ export function ReaderSettings({ slider, position, left, showFont = false, onFon @@ -76,7 +102,7 @@ export function ReaderSettings({ slider, position, left, showFont = false, onFon variant="ghost" size="icon-xs" aria-label="减小字号" - disabled={pr.prefs.text.sizeIdx === 0} + disabled={(fontIdx ?? internal.prefs.text.sizeIdx) === 0} onClick={() => onFont(-1)} > A− @@ -85,7 +111,7 @@ export function ReaderSettings({ slider, position, left, showFont = false, onFon variant="ghost" size="icon-xs" aria-label="加大字号" - disabled={pr.prefs.text.sizeIdx === FONT_SIZES.length - 1} + disabled={(fontIdx ?? internal.prefs.text.sizeIdx) === FONT_SIZES.length - 1} onClick={() => onFont(1)} > A+ diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx index 0e47d01..49f8e63 100644 --- a/frontend/src/components/ui/tabs.tsx +++ b/frontend/src/components/ui/tabs.tsx @@ -10,10 +10,7 @@ function TabsList({ className, ...props }: React.ComponentProps ); @@ -24,7 +21,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps ) : ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- 手势区(两侧翻屏/中间收放 chrome);键盘翻页由根容器统一提供
{count < 1 || winViews < 1 ? null : showEndCard ? ( diff --git a/frontend/src/readers/EpubReader.tsx b/frontend/src/readers/EpubReader.tsx index 18de5c3..d8c2faa 100644 --- a/frontend/src/readers/EpubReader.tsx +++ b/frontend/src/readers/EpubReader.tsx @@ -144,6 +144,8 @@ export default function EpubReader({ book, initialLocator, initialPercent }: Omi )}