feat(ux): F1 注入面板可控版 — 📌置顶/✕排除/近N章步进器 + 已排除恢复
- injection.ts 加 override 纯函数(currentOverride/withPinToggled/withExcluded/withRestored/withRecentN/isPinned,钳 1..20)+ author_pin 徽标 - useInjection 加 saving 态 + togglePin/exclude/restore/setRecentN,PUT 后以服务端确定结果为准(不变量 #6);写失败可读文案、不改本地态(天然回滚) - ChapterAssistant:每实体置顶/排除按钮 + 「近 N 章」步进器 + 「已排除」恢复区 - 13 新 vitest(override 纯函数);前端门禁绿(lint/tsc/vitest 183/build) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { kindLabel, reasonLabel } from "./injection";
|
||||
import {
|
||||
currentOverride,
|
||||
isPinned,
|
||||
kindLabel,
|
||||
reasonLabel,
|
||||
withExcluded,
|
||||
withPinToggled,
|
||||
withRecentN,
|
||||
withRestored,
|
||||
type InjectionOverrideRequest,
|
||||
type InjectionResponse,
|
||||
} from "./injection";
|
||||
|
||||
const REF = { kind: "character", name: "林动" } as const;
|
||||
|
||||
const emptyOverride = (): InjectionOverrideRequest => ({
|
||||
pinned: [],
|
||||
excluded: [],
|
||||
recent_n: 5,
|
||||
});
|
||||
|
||||
describe("reasonLabel", () => {
|
||||
it("maps each known selection reason to a Chinese badge", () => {
|
||||
@@ -8,6 +27,7 @@ describe("reasonLabel", () => {
|
||||
expect(reasonLabel("main_character")).toBe("主角常驻");
|
||||
expect(reasonLabel("recent_digest")).toBe("近章出现");
|
||||
expect(reasonLabel("foreshadow_window")).toBe("伏笔窗口");
|
||||
expect(reasonLabel("author_pin")).toBe("作者置顶");
|
||||
});
|
||||
|
||||
it("falls back to the raw code for an unknown reason", () => {
|
||||
@@ -25,3 +45,59 @@ describe("kindLabel", () => {
|
||||
expect(kindLabel("mystery")).toBe("mystery");
|
||||
});
|
||||
});
|
||||
|
||||
describe("currentOverride", () => {
|
||||
it("extracts pinned/excluded/recent_n from a response", () => {
|
||||
const data = {
|
||||
project_id: "p",
|
||||
chapter_no: 1,
|
||||
selected: [],
|
||||
recent_n: 3,
|
||||
pinned: [REF],
|
||||
excluded: [{ kind: "world_entity", name: "玄铁" }],
|
||||
} as unknown as InjectionResponse;
|
||||
expect(currentOverride(data)).toEqual({
|
||||
pinned: [REF],
|
||||
excluded: [{ kind: "world_entity", name: "玄铁" }],
|
||||
recent_n: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("withPinToggled", () => {
|
||||
it("pins an unpinned ref and removes it from excluded", () => {
|
||||
const next = withPinToggled(withExcluded(emptyOverride(), REF), REF);
|
||||
expect(isPinned(next, REF)).toBe(true);
|
||||
expect(next.excluded).toEqual([]);
|
||||
});
|
||||
|
||||
it("unpins an already-pinned ref", () => {
|
||||
const pinned = withPinToggled(emptyOverride(), REF);
|
||||
const next = withPinToggled(pinned, REF);
|
||||
expect(isPinned(next, REF)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withExcluded / withRestored", () => {
|
||||
it("excludes a ref, removing it from pinned, then restores it", () => {
|
||||
const pinnedThenExcluded = withExcluded(withPinToggled(emptyOverride(), REF), REF);
|
||||
expect(isPinned(pinnedThenExcluded, REF)).toBe(false);
|
||||
expect(pinnedThenExcluded.excluded).toEqual([REF]);
|
||||
const restored = withRestored(pinnedThenExcluded, REF);
|
||||
expect(restored.excluded).toEqual([]);
|
||||
});
|
||||
|
||||
it("is idempotent when excluding twice", () => {
|
||||
const once = withExcluded(emptyOverride(), REF);
|
||||
const twice = withExcluded(once, REF);
|
||||
expect(twice.excluded).toEqual([REF]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withRecentN", () => {
|
||||
it("clamps below the minimum and above the maximum", () => {
|
||||
expect(withRecentN(emptyOverride(), 0).recent_n).toBe(1);
|
||||
expect(withRecentN(emptyOverride(), 999).recent_n).toBe(20);
|
||||
expect(withRecentN(emptyOverride(), 7).recent_n).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user