Files
book-comic-library/frontend/test/chapters.test.ts
T

71 lines
3.4 KiB
TypeScript

import { expect, it } from "vitest";
import { chapterText, seekChapter, splitChapters } from "../src/lib/chapters";
const pad = (n: number) => "字".repeat(n);
it("识别中文/英文章节标题,首章前的引子归入「开篇」", () => {
const t = `${pad(300)}\n简介正文\n\n第一章 初入江湖\n${pad(500)}\n\n第二章 风起云涌\n${pad(500)}\n\nChapter 3 the end\n${pad(100)}`;
const chs = splitChapters(t);
expect(chs.map((c) => c.title)).toEqual(["开篇", "第一章 初入江湖", "第二章 风起云涌", "Chapter 3 the end"]);
expect(chs[1].start).toBe(t.indexOf("第一章"));
expect(chapterText(t, chs, 1)).toMatch(/^第一章 初入江湖/);
expect(chapterText(t, chs, chs.length - 1).trim().length).toBeLessThanOrEqual(200 + 15);
});
it("密集目录行(间隔 < MIN_GAP)被折叠,正文里的真章节仍可导航", () => {
const toc = ["第一章 aaa", "第二章 bbb", "第三章 ccc"].map((s) => s + "\n").join("");
const t = toc + "\n" + pad(500) + "\n第三章 ccc 正式开讲\n" + pad(500) + "\n第四章 ddd\n" + pad(500);
const chs = splitChapters(t);
expect(chs.map((c) => c.title)).toEqual(["第一章 aaa", "第三章 ccc 正式开讲", "第四章 ddd"]);
});
it("无章节标记的长文按段落边界切伪章节,覆盖全文不重叠", () => {
const para = pad(2500) + "\n\n";
const t = para.repeat(40); // 10 万字
const chs = splitChapters(t);
expect(chs.length).toBeGreaterThanOrEqual(5);
expect(chs[0].title).toBe("第 1 节");
expect(chs[0].start).toBe(0);
for (let i = 0; i < chs.length; i++) expect(chs[i].start).toBeLessThan(chs[i + 1]?.start ?? t.length);
if (chs.length > 1) expect(t[chs[1].start - 1]).toBe("\n"); // 落点在换行后,不切断一行
const total = chs.reduce((n, c, i) => n + (chs[i + 1]?.start ?? t.length) - c.start, 0);
expect(total).toBe(t.length);
});
it("短文无标记 → 单节,chapterText 即全文", () => {
const t = "hello\nworld";
const chs = splitChapters(t);
expect(chs).toEqual([{ title: "第 1 节", start: 0 }]);
expect(chapterText(t, chs, 0)).toBe(t);
});
it("卷不感知:只按章切,卷题行留在正文里", () => {
const t =
"第一卷 天云世界\n第一章 铜镜\n" +
pad(500) +
"\n第二章 修炼\n" +
pad(500) +
"\n\n\n第二卷 仙界\n第三章 飞升\n" +
pad(300);
const chs = splitChapters(t);
expect(chs.map((c) => c.title)).toEqual(["第一章 铜镜", "第二章 修炼", "第三章 飞升"]);
expect(chs[0].start).toBe(t.indexOf("第一章"));
expect(chs[2].start).toBe(t.indexOf("第三章"));
expect(chapterText(t, chs, 1)).toContain("第二卷 仙界"); // 卷题在第二章结尾,不进第三章开头
});
it("seekChapter:全局进度分数 → (章序号, 章内分数),端点与越界安全", () => {
expect(seekChapter(69, 0)).toEqual({ idx: 0, intra: 0 });
expect(seekChapter(69, 0.5)).toEqual({ idx: 34, intra: 0.5 }); // 69*0.5=34.5
expect(seekChapter(69, 1).idx).toBe(68); // 顶到 0.9999 防越界
expect(seekChapter(69, 1).intra).toBeGreaterThan(0.99);
expect(seekChapter(69, -0.2)).toEqual({ idx: 0, intra: 0 });
expect(seekChapter(1, 0.3)).toEqual({ idx: 0, intra: 0.3 });
});
it("只有卷没有章的书:按伪章节切", () => {
const t = "第一卷 起\n" + pad(600) + "\n第二卷 承\n" + pad(600) + "\n第三卷 转\n" + pad(600);
const chs = splitChapters(t);
expect(chs).toEqual([{ title: "第 1 节", start: 0 }]);
});