// Tests for the flips panel: its settings and validation (flips.mjs, through // validateChrome), the pairs against the timeline, the schedule, the cues and // the page (chrome-flips.mjs). // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; import { flipCues, flipSchedule, flipsOn, FLIPS_LIMITS, THEN_DIM, validateFlipEntries, validateFlips, yearsBetween } from "./flips.mjs"; import { deckGeometry, deckSchedule, railGap, validateChrome } from "./deck.mjs"; import { flipsHtml } from "./chrome-flips.mjs"; const PALETTE = { bg: "#12101a", fg: "#f4f1ea", muted: "#9a93ad", accent: "#e5534b", amber: "#ffc860" }; const PAIRS = [ { id: "vax", topic: "Vaccines", then: { entry: "a", when: "2019", words: "I trust doctors" }, now: { entry: "b", when: "2025", words: "Never again" } }, { id: "dw", topic: "The Daily Wire", then: { entry: "c", when: "May 2021", words: "Best place" }, now: { entry: "d", when: "Ep 300", words: "Worst place" } }, ]; const CHROME = { engine: "hyperframes", layout: "deck", deck: {}, flips: { pairs: PAIRS } }; const RENDER = { width: 1920, height: 1080, fps: 30, transition: 0.12, palette: PALETTE, chrome: CHROME }; const TIMELINE = ["a", "b", "c", "d"].map((id) => ({ id, type: "clip" })); test("validation: the pairs' shape, and not beside the thread rail", () => { assert.equal(flipsOn(RENDER), true); assert.deepEqual(validateFlips({ pairs: PAIRS }), []); assert.deepEqual(validateChrome(CHROME, RENDER), []); assert.match(validateFlips({ pairs: [] }).join(";"), /1 to 24 pairs/); const bad = validateFlips({ pairs: [ { id: "x", topic: "t", then: { entry: "a", when: "2019", words: "w".repeat(FLIPS_LIMITS.words + 1) }, now: { entry: "b", when: "x", words: "y" }, extra: 1 }, { id: "x", topic: "", then: null, now: { entry: "", when: "", words: "y" } }, ], }).join(";"); assert.match(bad, /pairs\[0\]\.extra is not a pair key/); assert.match(bad, /then\.words must be one line/); assert.match(bad, /x is listed twice/); assert.match(bad, /pairs\[1\]\.topic must be/); assert.match(bad, /pairs\[1\]\.then must be an object/); assert.match(bad, /now\.entry must name a timeline entry/); const both = { ...CHROME, threads: { list: [{ id: "t", label: "T" }] } }; assert.match(validateChrome(both, RENDER).join(";"), /share the left panel/); }); test("the pairs against the timeline: entries exist, THEN first, one pair each", () => { assert.deepEqual(validateFlipEntries({ render: RENDER, timeline: TIMELINE }), []); const errs = validateFlipEntries({ render: RENDER, timeline: [{ id: "b", type: "clip" }, { id: "a", type: "teaser" }] }).join(";"); assert.match(errs, /vax\)?: its THEN entry must come before its NOW entry/); assert.match(errs, /then\.entry a is a teaser/); assert.match(errs, /then\.entry c is not in the timeline/); const twice = { ...RENDER, chrome: { ...CHROME, flips: { pairs: [PAIRS[0], { ...PAIRS[1], then: { ...PAIRS[1].then, entry: "b" } }] } } }; assert.match(validateFlipEntries({ render: twice, timeline: TIMELINE }).join(";"), /b is already in pair vax/); }); test("the footage moves right for the panel, as for the rail", () => { const g = deckGeometry(RENDER).footage; assert.equal(g.x, 1920 - g.width - railGap(RENDER)); }); test("years between two whens, when both carry a year", () => { assert.equal(yearsBetween("2019", "2025"), 6); assert.equal(yearsBetween("May 2021", "October 2026"), 5); assert.equal(yearsBetween("Ep 12", "2025"), null); assert.equal(yearsBetween("2025", "2019"), null); }); const SEGS = [ { id: "a", start: 0, duration: 6 }, { id: "b", start: 5.88, duration: 5 }, { id: "c", start: 10.76, duration: 7 }, { id: "d", start: 17.64, duration: 4 }, ]; test("the schedule: each pair's spans, its years, its place", () => { const s = flipSchedule({ segments: SEGS, total: 21.64, render: RENDER, moves: [{ segment: "c", at: 11 }] }); assert.deepEqual(s.pairs.map((p) => [p.id, p.index, p.of, p.years]), [["vax", 1, 2, 6], ["dw", 2, 2, null]]); assert.deepEqual(s.pairs[0].then, { segment: "a", from: 0, to: 5.88, when: "2019", words: "I trust doctors" }); assert.deepEqual([s.pairs[1].now.from, s.pairs[1].now.to], [17.64, 21.64]); assert.deepEqual(s.asides, [{ from: 11, to: 17.64 }]); }); test("the cues: a pair rises with THEN, NOW slams and THEN dims, the count rolls to its year, the pair leaves", () => { const s = flipSchedule({ segments: SEGS, total: 21.64, render: RENDER }); const { init, cues } = flipCues(s); for (const c of cues) for (const k of Object.keys(c.to)) assert.notEqual(c.from[k], undefined, `${c.k} ${k} has a from`); assert.equal(cues.find((c) => c.k === "p0").at, 0); const slam = cues.find((c) => c.k === "n0"); assert.equal(slam.at, 5.88); assert.deepEqual(slam.to, { autoAlpha: 1, scale: 1, x: 0 }); assert.equal(cues.find((c) => c.k === "t0").to.opacity, THEN_DIM); const count = cues.find((c) => c.k === "gc0"); assert.ok(Math.abs(count.to.yPercent - -100 * (6 / 7)) < 1e-9); assert.equal(cues.find((c) => c.k === "gc1"), undefined, "no years, no count"); assert.deepEqual(init.gc0, { yPercent: 0 }); const leaves = cues.filter((c) => c.k === "p0" && c.to.autoAlpha === 0); assert.equal(leaves.length, 1); assert.ok(leaves[0].at + leaves[0].dur <= 10.76 + 1e-9, "gone by the next pair"); }); test("the page: one pair block per pair, both sides' words, an odometer of the years", () => { const entries = SEGS.map((s) => ({ id: s.id, type: "clip" })); const schedule = deckSchedule({ entries, durs: SEGS.map((s) => s.duration), D: 0.12, render: RENDER }); assert.ok(schedule.flips, "the deck's schedule carries the pairs"); const html = flipsHtml(schedule, RENDER, { fonts: { regular: "r.ttf", bold: "b.ttf" } }); assert.equal((html.match(/class="pair"/g) ?? []).length, 2); assert.match(html, /“I trust doctors”/); assert.match(html, /“Never again”/); assert.equal((html.match(/class="digit"/g) ?? []).length, 7); assert.match(html, /years later/); assert.match(html, /01 \/ 02/); assert.throws(() => flipsHtml({ ...schedule, flips: undefined }, RENDER), /no pairs/); });