- 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>
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
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", () => {
|
|
expect(reasonLabel("explicit_beat")).toBe("本章点名");
|
|
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", () => {
|
|
expect(reasonLabel("future_reason")).toBe("future_reason");
|
|
});
|
|
});
|
|
|
|
describe("kindLabel", () => {
|
|
it("maps entity kinds to Chinese", () => {
|
|
expect(kindLabel("character")).toBe("角色");
|
|
expect(kindLabel("world_entity")).toBe("设定");
|
|
});
|
|
|
|
it("falls back to the raw kind when unknown", () => {
|
|
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);
|
|
});
|
|
});
|