import { describe, expect, it } from "vitest"; import { buildReviewChapterOptions, defaultReviewChapter, reviewChapterHref, reviewChapterOptions, } from "./chapterNav"; import type { ChapterListItem } from "@/lib/api/types"; import type { ChapterEntry } from "@/lib/workbench/chapter"; const item = ( chapter_no: number, over: Partial = {}, ): ChapterListItem => ({ chapter_no, has_draft: true, accepted: false, reviewed_at: null, ...over, }); describe("reviewChapterHref", () => { it("builds the review route for a chapter number", () => { expect(reviewChapterHref("proj-1", 3)).toBe( "/projects/proj-1/review?chapter=3", ); }); it("always targets the review page (not write)", () => { expect(reviewChapterHref("p", 1)).toContain("/review?chapter=1"); }); }); describe("reviewChapterOptions (legacy, outline-only)", () => { const chapters: ChapterEntry[] = [ { no: 1, title: "开篇" }, { no: 2 }, { no: 3, title: "转折" }, ]; it("merges outline chapters with the current chapter, sorted ascending", () => { const opts = reviewChapterOptions(chapters, 2); expect(opts.map((o) => o.no)).toEqual([1, 2, 3]); }); it("includes the current chapter even when it is outside the outline", () => { const opts = reviewChapterOptions(chapters, 9); expect(opts.map((o) => o.no)).toEqual([1, 2, 3, 9]); }); it("labels each option with chapter number and optional title (never disabled)", () => { const opts = reviewChapterOptions(chapters, 1); expect(opts[0]).toEqual({ no: 1, label: "第 1 章 · 开篇", disabled: false }); expect(opts[1]).toEqual({ no: 2, label: "第 2 章", disabled: false }); }); }); describe("buildReviewChapterOptions (real chapters + outline titles)", () => { const outline: ChapterEntry[] = [ { no: 1, title: "开篇" }, { no: 2, title: "转折" }, ]; it("enumerates real chapters, sorted, with outline titles + status suffix", () => { const list = [ item(1, { accepted: true }), item(2, { reviewed_at: "2026-01-01T00:00:00Z" }), ]; const opts = buildReviewChapterOptions(list, outline, 2); expect(opts).toEqual([ { no: 1, label: "第 1 章 · 开篇 · 已验收", disabled: false }, { no: 2, label: "第 2 章 · 转折 · 已审", disabled: false }, ]); }); it("covers chapters written but not in the outline, and shows outline structure", () => { const list = [item(1), item(5)]; // 第 5 章 写过但大纲没有;大纲有第 2 章但未写 const opts = buildReviewChapterOptions(list, outline, 1); expect(opts.map((o) => o.no)).toEqual([1, 2, 5]); expect(opts.find((o) => o.no === 5)?.label).toBe("第 5 章 · 草稿"); // 大纲有、未写的章出现在结构里但置灰(不可审) expect(opts.find((o) => o.no === 2)?.disabled).toBe(true); }); it("disables chapters with no draft, but never the current chapter", () => { const list = [item(1), item(2, { has_draft: false })]; const optsOnOther = buildReviewChapterOptions(list, outline, 1); expect(optsOnOther.find((o) => o.no === 2)?.disabled).toBe(true); // 当前就在第 2 章时不置灰(否则无法停留) const optsOnEmpty = buildReviewChapterOptions(list, outline, 2); expect(optsOnEmpty.find((o) => o.no === 2)?.disabled).toBe(false); }); it("always includes the current chapter even if absent from list and outline", () => { const opts = buildReviewChapterOptions([item(1)], outline, 7); expect(opts.map((o) => o.no)).toEqual([1, 2, 7]); expect(opts.find((o) => o.no === 7)).toEqual({ no: 7, label: "第 7 章", disabled: false, }); }); }); describe("defaultReviewChapter", () => { it("prefers the latest chapter that has a draft", () => { const list = [item(1), item(2), item(3, { has_draft: false })]; expect(defaultReviewChapter(list)).toBe(2); }); it("falls back to the max chapter number when none have drafts", () => { const list = [item(1, { has_draft: false }), item(4, { has_draft: false })]; expect(defaultReviewChapter(list)).toBe(4); }); it("defaults to chapter 1 for an empty list", () => { expect(defaultReviewChapter([])).toBe(1); }); });