// Tests for the posts FEED (slice F1, `posts.layout: "feed"`): the core's // geometry and schedule for it (deck.mjs), the page that draws it // (chrome-feed.mjs) and the plan it runs, compose-chrome's feed region, the // build's framing record and its refusal, and the overlay of the feed's frames. // The popup and the deck without posts are held to what they always were. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { FEED_MOTION, feedCues, feedHtml, feedLayout, feedWho } from "./chrome-feed.mjs"; import { deckChoreography, deckGeometry, deckSchedule, estimateSchedule, feedGeometry, feedOn, postHolds, postSchedule, postsGeometry, postWindows, validateChrome, validatePosts, } from "./deck.mjs"; import { buildVideo, chromeOverlayChain, chromeRegions, deckFraming, deckFramingFilter, feedRegion, framedUnderDeck, framingProblems, postsRegions, segmentFraming, segmentJoins, } from "./build-video.mjs"; const PALETTE = { bg: "#12101a", fg: "#f4f1ea", muted: "#9a93ad", accent: "#a97bff", amber: "#ffc860" }; const CHROME = { engine: "hyperframes", layout: "deck" }; const BASE = { width: 1920, height: 1080, fps: 30, transition: 0.5, palette: PALETTE }; const RENDER = { ...BASE, chrome: { ...CHROME, deck: {} } }; const FEED = { ...BASE, chrome: { ...CHROME, deck: { posts: { layout: "feed" } } } }; const PROV = { siteOrigin: "https://example.test", channelSlug: "chan" }; const CLIPS = [ { id: "c1", type: "clip", video: "a", start: 0, end: 10, date: "2024-09-05" }, { id: "c2", type: "clip", video: "b", start: 0, end: 20, date: "2024-10-01" }, { id: "k1", type: "card", seconds: 4 }, { id: "c3", type: "clip", video: "c", start: 0, end: 6, date: "2025-12-08" }, ]; const DURS = [10, 20, 4, 6]; const POST = (id, date, extra = {}) => ({ id, platform: "bluesky", author: "Pirate Software", handle: "piratesoftware.live", date, text: `words of ${id}`, url: `https://bsky.app/profile/piratesoftware.live/post/${id}`, ...extra, }); // c2 carries a, b, c (October/November 2024); c3 carries z. const POSTS = [POST("a", "2024-10-19"), POST("b", "2024-11-27"), POST("c", "2024-12-01"), POST("z", "2026-01-22")]; const sched = (render = FEED, posts = POSTS, durs = DURS, D = 0.5) => deckSchedule({ entries: CLIPS, durs, D, render, provenance: PROV, posts }); // ---- the core --------------------------------------------------------------- test("feedGeometry: a column flush with the side and top down to the deck, the footage beside it", () => { const g = feedGeometry(FEED); assert.deepEqual(g.column, { x: 1320, y: 0, width: 600, height: 890 }); // 1320 × 890 left of the column; inset 24 all round; the frame's aspect; centred. assert.deepEqual(g.footage, { x: 24, y: 87, width: 1272, height: 716 }); assert.deepEqual(g.deck, deckGeometry(FEED).deck); // Nothing overlaps: footage, gap, column; footage above the deck. assert.equal(g.column.x - (g.footage.x + g.footage.width), 24); assert.ok(g.footage.y + g.footage.height <= g.deck.y); // top-left mirrors it; a narrower column grows the footage. const left = { ...BASE, chrome: { ...CHROME, deck: { posts: { layout: "feed", position: "top-left", width: 520 } } } }; const l = feedGeometry(left); assert.deepEqual(l.column, { x: 0, y: 0, width: 520, height: 890 }); assert.deepEqual(l.footage, { x: 520 + 24, y: 65, width: 1352, height: 760 }); // The posts region IS the column under the feed; the popup's is unchanged. assert.deepEqual(postsGeometry(FEED), g.column); assert.deepEqual(postsGeometry(RENDER), { x: 1920 - 24 - 600, y: 26, width: 600, height: 838 }); }); test("validation: layout is popup or feed; the feed is held to the footage left beside it", () => { assert.deepEqual(validateChrome(FEED.chrome, FEED), []); assert.match(validateChrome({ ...CHROME, deck: { posts: { layout: "sidebar" } } }, BASE)[0], /posts.layout must be "popup" or "feed"/); // 900 + 2·24 at 1920 leaves 972 px: allowed. At 1280×720 a 600 column leaves 632 < 640. assert.deepEqual(validateChrome({ ...CHROME, deck: { posts: { layout: "feed", width: 900 } } }, BASE), []); const small = { ...BASE, width: 1280, height: 720, chrome: { ...CHROME, deck: { posts: { layout: "feed" } } } }; assert.match(validateChrome(small.chrome, small).join(" "), /leaves the footage 632px wide beside the feed/); assert.match(validatePosts([POST("p", "2024-01-01")], CLIPS, small).join(" "), /beside the feed/); }); test("feedOn: the deck, the feed layout, shown, and a post to draw on a clip", () => { assert.equal(feedOn({ render: FEED, posts: POSTS, entries: CLIPS }), true); assert.equal(feedOn({ render: RENDER, posts: POSTS, entries: CLIPS }), false); assert.equal(feedOn({ render: FEED, posts: [], entries: CLIPS }), false); assert.equal(feedOn({ render: FEED, posts: [POST("h", "2024-01-01", { hide: true })], entries: CLIPS }), false); assert.equal(feedOn({ render: FEED, posts: POSTS, entries: [{ id: "k", type: "card" }] }), false); const off = { ...BASE, chrome: { ...CHROME, deck: { posts: { layout: "feed", show: false } } } }; assert.equal(feedOn({ render: off, posts: POSTS, entries: CLIPS }), false); assert.equal(feedOn({ render: { ...FEED, chrome: undefined }, posts: POSTS, entries: CLIPS }), false); }); test("the feed schedule: each post ticks in at its clip's start + D, a clip's next ones `seconds` apart", () => { const s = sched(); assert.equal(s.layout, "feed"); // No hold, no move: the segments are their own lengths. assert.deepEqual(s.segments.map((x) => x.duration), DURS); assert.equal(s.segments.some((x) => "hold" in x), false); assert.equal("moves" in s, false); assert.equal(postHolds({ posts: POSTS, entries: CLIPS, render: FEED }).size, 0); // c2 starts at 9.5: a at 10 (after the dissolve), b and c 4 s apart. const at = Object.fromEntries(s.posts.map((p) => [p.id, [p.segment, p.slot, p.of, p.in]])); assert.deepEqual(at, { a: ["c2", 0, 3, 10], b: ["c2", 1, 3, 14], c: ["c2", 2, 3, 18], z: ["c3", 0, 1, 33] }); // A feed post has `in`, not the popup's appear/out. for (const p of s.posts) assert.equal("appear" in p || "out" in p, false); // No windows: the feed is one composition. assert.deepEqual(postWindows(s), []); assert.deepEqual(postsRegions(FEED, "/o", s), []); assert.equal(segmentJoins(s), null); // A clip too short for k × seconds shares what it has before its leave. const short = sched(FEED, POSTS, [10, 8, 4, 6]); assert.deepEqual(short.posts.filter((p) => p.segment === "c2").map((p) => p.in), [10, 12.333, 14.667]); // A hard cut: D = 0, the post is in at the clip's first frame. const hard = sched(FEED, POSTS, DURS, 0); assert.deepEqual(hard.posts.map((p) => p.in), [10, 14, 18, 34]); // The core function, unrounded, says the same. const segs = s.segments.map((x) => ({ id: x.id, start: x.start, duration: x.duration })); assert.equal(postSchedule({ posts: POSTS, entries: CLIPS, segments: segs, D: 0.5, total: s.total, render: FEED })[0].in, 10); }); test("the popup and the deck without posts write the schedule they always did", () => { // A feed with nothing to draw is the deck alone, byte for byte. const plain = JSON.stringify(sched(RENDER, [])); assert.equal(JSON.stringify(sched(FEED, [])), plain); assert.equal(JSON.stringify(sched(FEED, [POST("h", "2024-10-19", { hide: true })])), plain); const noShow = { ...BASE, chrome: { ...CHROME, deck: { posts: { layout: "feed", show: false } } } }; assert.equal(JSON.stringify(sched(noShow, POSTS)), plain); // "popup", named, is the default: the same schedule, holds and moves and all. const named = { ...BASE, chrome: { ...CHROME, deck: { posts: { layout: "popup" } } } }; assert.equal(JSON.stringify(sched(named)), JSON.stringify(sched(RENDER))); const popup = sched(RENDER); assert.equal("layout" in popup, false); assert.ok(popup.moves.length > 0); assert.ok(popup.posts.every((p) => "appear" in p && "out" in p && !("in" in p))); assert.equal(estimateSchedule({ render: FEED, provenance: PROV, timeline: CLIPS, posts: POSTS }).layout, "feed"); }); // ---- the plan the page runs -------------------------------------------------- const P = (posts = sched().posts) => posts.map((p) => ({ id: p.id, in: p.in })); const byWhy = (cues, re) => cues.filter((c) => re.test(c.why)); test("feedCues: each card ticks in at its time, at the top, and the ones in move down by its height", () => { const plan = feedCues({ posts: P(), heights: [150, 200, 160, 180], column: 1000, ...FEED_MOTION }); const g = FEED_MOTION.gap; // Every card enters `lag` after its `in`, from the side, to x 0. P().forEach((p, j) => { const [e] = byWhy(plan.cues, new RegExp(`^enter ${p.id}$`)); assert.equal(e.k, `c${j}`); assert.equal(e.at, Math.round((p.in + FEED_MOTION.lag) * 1e4) / 1e4); assert.deepEqual(e.to, { autoAlpha: 1, x: 0 }); }); // b pushes a down by b's height; c pushes a and b by c's. assert.deepEqual(byWhy(plan.cues, /^push for b$/).map((c) => [c.k, c.at, c.to.y]), [["c0", 14, 200 + g]]); assert.deepEqual(byWhy(plan.cues, /^push for c$/).map((c) => [c.k, c.to.y]).sort(), [["c0", 200 + 160 + 2 * g], ["c1", 160 + g]]); // The highlight: the newest flares and settles; the one before it goes out. assert.deepEqual(byWhy(plan.cues, /^calm for b$/).map((c) => [c.k, c.to.opacity]), [["h0", 0]]); assert.deepEqual(byWhy(plan.cues, /^settle z$/).map((c) => [c.k, c.to.opacity]), [["h3", FEED_MOTION.newest]]); // No empty plate: before the first post the column is its header over its ground. assert.ok(!("empty" in plan.init)); assert.ok(!plan.cues.some((c) => c.k === "empty" || /^first /.test(c.why))); assert.equal(plan.cues.some((c) => /^n\d+$/.test(c.k)), false); assert.equal(Object.keys(plan.init).some((k) => /^n\d+$/.test(k)), false); assert.equal(plan.gone.every((x) => x === null), true); }); test("feedCues: when the column overflows the oldest fade out as they are pushed past it", () => { // A column of 400: a (150) and b (200) fit; c (160) pushes a to 376 + 150 > 400. const plan = feedCues({ posts: P(), heights: [150, 200, 160, 180], column: 400, ...FEED_MOTION }); const out = byWhy(plan.cues, /^out for /); assert.deepEqual(out.map((c) => [c.k, c.why, c.to.autoAlpha]), [["c0", "out for c", 0], ["c1", "out for z", 0]]); assert.deepEqual(plan.gone, [18, 33, null, null]); // A gone card is never moved again. assert.equal(plan.cues.some((c) => c.k === "c0" && c.at > 18), false); }); test("feedCues: every cue states its from, the last to on its element -- seek-safe", () => { const vis = deckChoreography({ ...sched(), segments: sched().segments.map((s) => ({ ...s, hideDeck: s.id === "k1" })) }, FEED).visibility; const plan = feedCues({ posts: P(), heights: [150, 200, 160, 180], column: 400, visibility: vis, slideX: 624, ...FEED_MOTION }); const state = Object.fromEntries(Object.entries(plan.init).map(([k, v]) => [k, { ...v }])); let last = -Infinity; const ends = {}; for (const c of plan.cues) { assert.ok(c.at >= last, "cues in time order"); last = c.at; for (const [q, v] of Object.entries(c.from)) assert.equal(v, state[c.k][q], `${c.k}.${q} from at ${c.at}`); assert.ok(c.at >= (ends[c.k] ?? -Infinity) - 1e-9, `${c.k} overlaps itself at ${c.at}`); ends[c.k] = c.at + c.dur; Object.assign(state[c.k], c.to); } // The column slides off with the deck over the card and back after it. const col = plan.cues.filter((c) => c.k === "col"); assert.deepEqual(col.map((c) => c.to.x), [624, 0]); assert.deepEqual(col.map((c) => c.at), vis.map((v) => v.at[0])); // Hidden from the first frame: that is the init, no cue. const first = feedCues({ posts: P(), heights: [1, 1, 1, 1], column: 400, startsHidden: true, slideX: 624, visibility: [{ i: 0, hide: true, at: [0, 0] }] }); assert.deepEqual(first.init.col, { x: 624 }); assert.equal(first.cues.some((c) => c.k === "col"), false); }); // ---- the page ------------------------------------------------------------------ const FONTS = { regular: "assets/DeckSans.ttf", bold: "assets/DeckSansBold.ttf" }; const HOSTILE = POST("evil", "2024-11-27T02:36:13.745Z", { platform: "x", handle: '">', author: "bold", text: "\nsecond line & 'quotes'\n\nnew paragraph", }); const dataOf = (html) => JSON.parse(/