diff --git a/tests/int/story-beats.int.spec.ts b/tests/int/story-beats.int.spec.ts index 27f7708..e1ac3d1 100644 --- a/tests/int/story-beats.int.spec.ts +++ b/tests/int/story-beats.int.spec.ts @@ -4,6 +4,8 @@ import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { StoryBeat, StoryBeatState, StoryVoiceover, User } from "@/payload-types"; import { markBeatRead, setBeatReaction } from "@/lib/story"; +import { sortBeats, chronicleDisplayOrder } from "@/lib/story/sortBeats"; +import { bookColumnOffset, bookPageCount } from "@/lib/story/bookLayout"; let payload: Payload; @@ -469,3 +471,115 @@ describe("Story beats", () => { }); }); }); + +describe("sortBeats (chapter ordering)", () => { + const baseBeat = { + status: "published" as const, + body: {} as StoryBeat["body"], + }; + + function beat(overrides: Partial & { id: number }): StoryBeat { + return { ...baseBeat, ...overrides } as unknown as StoryBeat; + } + + it("defaults to real creation date ascending, ignoring in-story timestamps", () => { + // Fictional in-story dates deliberately out of release order. + const prologue = beat({ + id: 1, + title: "Little Green Light", + createdAt: "2026-09-20T10:00:00Z", + occurredAt: new Date("2156-03-18T08:00:00Z").toISOString(), + }); + const followUp = beat({ + id: 2, + title: "What Makes Us Human", + createdAt: "2026-09-24T10:00:00Z", + occurredAt: new Date("2034-06-12T08:00:00Z").toISOString(), + }); + + const sorted = sortBeats([followUp, prologue]); + expect(sorted.map((beat) => beat.id)).toEqual([1, 2]); + }); + + it("puts explicitly ordered beats first, sorted by their order value", () => { + const old = beat({ id: 1, createdAt: "2026-01-01T00:00:00Z" }); + const newer = beat({ id: 2, createdAt: "2026-02-01T00:00:00Z", order: 1 }); + const pinnedLast = beat({ id: 3, createdAt: "2026-03-01T00:00:00Z", order: 2 }); + // order = 0 is a valid curation value and must still lead the list. + const pinnedFirst = beat({ id: 4, createdAt: "2026-04-01T00:00:00Z", order: 0 }); + + const sorted = sortBeats([old, pinnedFirst, newer, pinnedLast]); + expect(sorted.map((beat) => beat.id)).toEqual([4, 2, 3, 1]); + }); + + it("does not mutate the input array", () => { + const a = beat({ id: 1, createdAt: "2026-01-01T00:00:00Z" }); + const b = beat({ id: 2, createdAt: "2026-02-01T00:00:00Z" }); + const input = [b, a]; + + sortBeats(input); + expect(input.map((beat) => beat.id)).toEqual([2, 1]); + }); +}); + +describe("chronicleDisplayOrder (page display)", () => { + const baseBeat = { + status: "published" as const, + body: {} as StoryBeat["body"], + }; + + function beat(overrides: Partial & { id: number }): StoryBeat { + return { ...baseBeat, ...overrides } as unknown as StoryBeat; + } + + it("shows the newest chapter first by default, with chapter numbers still ascending", () => { + const ch1 = beat({ id: 1, createdAt: "2026-09-20T10:00:00Z" }); + const ch2 = beat({ id: 2, createdAt: "2026-09-21T10:00:00Z" }); + const ch3 = beat({ id: 3, createdAt: "2026-09-22T10:00:00Z" }); + + const { beats, chapterById } = chronicleDisplayOrder([ch1, ch2, ch3]); + + expect(beats.map((beat) => beat.id)).toEqual([3, 2, 1]); + expect(chapterById.get(1)).toBe(1); + expect(chapterById.get(2)).toBe(2); + expect(chapterById.get(3)).toBe(3); + }); + + it("switches to book mode when any beat is hand-ordered", () => { + const a = beat({ id: 1, createdAt: "2026-09-20T10:00:00Z" }); + const b = beat({ id: 2, createdAt: "2026-09-21T10:00:00Z" }); + const c = beat({ id: 3, createdAt: "2026-09-22T10:00:00Z", order: 1 }); + + const { beats, chapterById } = chronicleDisplayOrder([a, b, c]); + + expect(beats.map((beat) => beat.id)).toEqual([3, 1, 2]); + expect(chapterById.get(3)).toBe(1); + }); +}); + +describe("bookLayout (Book view paging)", () => { + it("counts one page when content fits the viewport", () => { + // One 2-up page: scrollWidth = column width = (1200 - 32) / 2 = 584. + expect(bookPageCount(584, 1200, 2)).toBe(1); + // scrollWidth == viewport width means both visible pages are exactly full: 2 pages. + expect(bookPageCount(1200, 1200, 2)).toBe(2); + }); + + it("counts pages from the measured multicol scrollWidth", () => { + // 3 pages at 2-up: scrollWidth = 3 * pitch - gap = 3 * 616 - 32 = 1816. + expect(bookPageCount(1816, 1200, 2)).toBe(3); + // A lone right page (2.5 pages of content) still counts as its own page. + expect(bookPageCount(1816 + 300, 1200, 2)).toBe(4); + }); + + it("pays the gap once per page in single-column mode", () => { + // 2 pages at 1-up, viewport 700: scrollWidth = 2 * 732 - 32 = 1432. + expect(bookPageCount(1432, 700, 1)).toBe(2); + }); + + it("computes the flip offset as pageIndex * pitch", () => { + expect(bookColumnOffset(0, 1200, 2)).toBe(0); + expect(bookColumnOffset(2, 1200, 2)).toBe(1232); + expect(bookColumnOffset(1, 700, 1)).toBe(732); + }); +});