Files
roboco/motion/compositions/release-recap/release-recap.test.js
T
6188cce58c [e0bc418f] Video: v0.19.0 - our newest release (#333)
* [e0bc418f] add v019-release HyperFrames composition with vertical/square cuts, theme, props, and smoke tests

* [e0bc418f] fix(v019-release): remove local artifacts, replace em dashes, add captions.json and regression tests

* [e0bc418f] chore: delete .venv symlink and tighten .gitignore for symlink leakage

* [e0bc418f] chore(.gitignore): remove duplicate .pnpm-store line

* [e0bc418f] fix(v019-release): clean internal em dashes and add package-lock regression test

* [e0bc418f] chore(v019-release): remove committed vitest cache artifact from node_modules

* [e0bc418f] fix(.gitignore): restore .pnpm-store/ next to its GH001 comment and remove stray duplicate

* [e0bc418f] docs(motion): document v019-release composition and captions convention

* [e0bc418f] feat(release-recap): rebuild v0.19.0 video as panel-demo three-release recap

CEO rejected the text-card v019-release composition and rescoped this
to a recap of three releases (0.18.0-0.20.0) shipped in six days,
built in the panel-demo register per the CEO's brief. Brings
motion/kit/ and motion/compositions/panel-demo/ into this branch
(byte-identical to master, which merged them after this branch
diverged and cannot be rebased onto directly), deletes the obsolete
motion/compositions/v019-release/ text-card composition, and adds
motion/compositions/release-recap/: the panel frame, one intake typing
reveal, three release cards (v0.18.0/v0.19.0/v0.20.0) cycling through
a single shared kanban slot with their own progress-to-completed pill
flip, a cursor that glides in once and click-pulses at each flip, a
closing toast, and the roboco.tech outro, both at 1080x1920 and
1080x1080. Includes props.js, captions.json (X 136/280 chars, TikTok
365/2200 chars, no em dash), and a vitest smoke test (11/11 passing
across all three compositions in the package). Also refreshes
motion/README.md with the current design-bar/panel-demo-kit sections
and a release-recap example replacing the deleted v019-release one.

* [e0bc418f] fix(release-recap): polish pass — accumulate the three releases, fix facts, robust typewriter

Cards now stack and accumulate instead of cycling one absolute slot (the
three-release story never landed visually); duration tightened 20s to
15s; per-release copy corrected (0.18.0 fleet doctrine + design bar,
0.19.0 video engine + sandboxes, 0.20.0 root-owned coverage + the kit)
along with the same fixes in captions.json; one cursor with one glide
and one click landing at the pill edge instead of parking on pill text
for 17s; the per-char typing reveal (which partially froze) replaced by
a single width-steps mono typewriter sized to the supplied text.

* [e0bc418f] feat(release-recap): identity redesign — real logo, real-UI sidebar, explainers, stat beat

CEO review round 2: no logo, no similarity with the actual UI, explains
nothing. Rebuilt: branded cold open (the real logo on an app-icon tile
with a ring sweep, wordmark, tagline), a labeled sidebar mirroring the
real panel (nav labels + active state + search + Live dot + CEO chip),
a one-line explainer under each release title, a kinetic stat beat
(3 releases / 6 days / 1 human — full-frame interstitial on the square
cut where no side zone exists), and a logo lockup outro; 18s. The logo
PNG is vendored into motion/public (offline constraint, same pattern
as the fonts).

* [e0bc418f] fix(release-recap): darkmode logo hero — crisp 1024px mark replaces the pixelated 64px tile

The cold open now uses logos/roboco-logo-darkmode.png (vendored into
motion/public), built for dark backgrounds with the wordmark included:
full-size mark with a soft glow and the ring sweep, no white tile, no
separate typed wordmark. The small app-icon tiles in the sidebar and
outro keep the 64px icon where tiny sizes favor it.

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-09 18:48:46 +02:00

123 lines
5.1 KiB
JavaScript

import { readFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// HTML-structure smoke test for the release-recap composition - mirrors
// panel-demo.test.js (this clip extends the same pk-* kit). We parse the
// HTML as text and assert structural invariants: dimensions, props.js
// wiring, the kit stylesheet link, timed clips, the three-release beats
// (card + chip + pill flip each), the cursor, the toast/outro, the offline
// constraint, and the captions.json schema/limits/copy regressions the CEO's
// revision called out on the prior text-card composition.
const here = dirname(fileURLToPath(import.meta.url));
const dir = here;
const read = (name) => readFileSync(join(dir, name), "utf8");
const orientations = [
{ file: "vertical.html", height: "1920" },
{ file: "square.html", height: "1080" },
];
const versions = ["v0.18.0", "v0.19.0", "v0.20.0"];
describe("release-recap composition", () => {
it("ships props.js (default preview values, sidecar overwrites at render)", () => {
expect(existsSync(join(dir, "props.js"))).toBe(true);
const src = read("props.js");
expect(src).toContain("window.__PROPS__");
expect(src).toContain("window.__ORIENTATION__");
expect(src).toContain("introText");
expect(src).toContain("toastTitle");
expect(src).toContain("toastBody");
expect(src).not.toMatch(/—/); // no em dash in on-screen copy
});
it.each(orientations)("$file parses with correct dimensions and wiring", ({ file, height }) => {
const html = read(file);
// HyperFrames render params.
expect(html).toContain('data-width="1080"');
expect(html).toContain(`data-height="${height}"`);
expect(html).toMatch(/data-duration="18"/);
expect(html).toMatch(/data-fps="30"/);
// props.js loaded before the kit helper and before any inline script
// reads window.__PROPS__ / window.PanelKit.
const propsIdx = html.indexOf('<script src="props.js"></script>');
const kitJsIdx = html.indexOf('<script src="../../kit/kit.js"></script>');
const inlineIdx = html.indexOf("window.__timelines");
expect(propsIdx).toBeGreaterThan(-1);
expect(kitJsIdx).toBeGreaterThan(propsIdx);
expect(inlineIdx).toBeGreaterThan(kitJsIdx);
// Kit stylesheet linked (no theme.css of its own - this is the demo
// register, not the text-card register).
expect(html).toContain('href="../../kit/kit.css"');
// At least one timed clip element.
expect(html).toMatch(/class="[^"]*clip[^"]*"/);
// Panel frame chrome - "the video must show the product moving".
expect(html).toContain("pk-frame");
expect(html).toContain("pk-frame__sidebar");
expect(html).toContain("pk-frame__topbar");
// Typing reveal into intake.
expect(html).toContain('id="typeIntro"');
expect(html).toContain("pk-caret");
expect(html).toContain("typeReveal");
// Three release beats: a card + version chip + a progress -> completed
// pill flip each ("status pills flipping", plural).
const cardCount = (html.match(/pk-card clip/g) || []).length;
const progressCount = (html.match(/pk-pill--progress/g) || []).length;
const completedCount = (html.match(/pk-pill--completed/g) || []).length;
expect(cardCount).toBe(3);
expect(progressCount).toBe(3);
expect(completedCount).toBe(3);
for (const version of versions) {
expect(html).toContain(version);
}
// Cursor glide + click.
expect(html).toContain("pk-cursor");
expect(html).toContain("pk-cursor__ring");
// Toast.
expect(html).toContain("pk-toast");
expect(html).toContain('id="toastTitle"');
expect(html).toContain('id="toastBody"');
// Outro headline moment.
expect(html).toContain("pk-outro");
expect(html).toContain("roboco.tech");
// No CDN / network fetch of any kind - the render is offline.
expect(html).not.toMatch(/<script[^>]+src="https?:\/\//);
expect(html).not.toMatch(/href="https?:\/\//);
// No em dash in on-screen copy (design-bar regression from the prior
// text-card composition's QA failure on this same task).
expect(html).not.toMatch(/—/);
});
it("ships captions.json within X and TikTok platform limits", () => {
expect(existsSync(join(dir, "captions.json"))).toBe(true);
const captions = JSON.parse(read("captions.json"));
expect(captions.composition_id).toBe("release-recap");
expect(captions.platforms.x.char_count).toBe(captions.platforms.x.caption.length);
expect(captions.platforms.x.char_count).toBeLessThanOrEqual(captions.platforms.x.limit);
expect(captions.platforms.x.within_limit).toBe(true);
expect(captions.platforms.tiktok.char_count).toBe(captions.platforms.tiktok.caption.length);
expect(captions.platforms.tiktok.char_count).toBeLessThanOrEqual(captions.platforms.tiktok.limit);
expect(captions.platforms.tiktok.within_limit).toBe(true);
// No em dash in either caption.
expect(captions.platforms.x.caption).not.toMatch(/—/);
expect(captions.platforms.tiktok.caption).not.toMatch(/—/);
});
});