// 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(/