Files
hyperframes/scripts/generate-catalog-previews.test.ts
Miguel Ángel fb4189ca72 fix(catalog): match preview length to authored compositions (#5533)
* fix(catalog): match preview length to authored compositions

* fix(catalog): derive undeclared preview duration from motion
2026-10-13 05:51:02 +00:00

150 lines
5.2 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { resolve, join } from "node:path";
import { tmpdir } from "node:os";
import { prepareProjectDir } from "./generate-catalog-previews.js";
import { after, describe, it } from "node:test";
import assert from "node:assert/strict";
const repoRoot = resolve(import.meta.dirname, "..");
const previewPath = resolve(repoRoot, "docs/images/catalog/blocks/thread-message-stack.png");
after(() => rmSync(previewPath, { force: true }));
describe("catalog preview raw block boundary", () => {
it("renders thread-message-stack through the exact Catalog Previews command", () => {
rmSync(previewPath, { force: true });
const result = spawnSync(
"bunx",
[
"tsx",
"scripts/generate-catalog-previews.ts",
"--only",
"thread-message-stack",
"--skip-video",
],
{ cwd: repoRoot, encoding: "utf8", timeout: 120_000 },
);
const output = `${result.stdout}${result.stderr}`;
assert.equal(result.status, 0, output);
assert.ok(!output.includes("✗ thread-message-stack"), output);
assert.ok(!output.includes("[Browser:ERROR]"), output);
assert.ok(!output.includes("timelines not registered"), output);
assert.ok(!output.includes("sub_timeline_readiness_timeout"), output);
assert.ok(output.includes("✓ thread-message-stack.png"), output);
assert.ok(existsSync(previewPath), previewPath);
});
});
async function preparedHtml(sourceDir: string, entryFile: string): Promise<string> {
const projectDir = await prepareProjectDir(
{ name: "duration-test", kind: "component", sourceDir, entryFile },
{ compile: false, hostedAssets: "cdn", uiFragment: true },
);
try {
return readFileSync(join(projectDir, "index.html"), "utf8");
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
}
describe("catalog wrapper duration", () => {
it("cleans its temporary project when reading the authored entry fails", async () => {
const name = `duration-error-${process.pid}`;
const sourceDir = mkdtempSync(join(tmpdir(), "catalog-duration-source-"));
const projects = () =>
readdirSync(tmpdir()).filter((entry) => entry.startsWith(`hf-catalog-${name}-`));
const before = projects();
try {
await assert.rejects(
prepareProjectDir(
{ name, kind: "component", sourceDir, entryFile: "missing.html" },
{ compile: false, hostedAssets: "cdn" },
),
{ code: "ENOENT" },
);
assert.deepEqual(projects(), before);
} finally {
rmSync(sourceDir, { recursive: true, force: true });
for (const project of projects())
if (!before.includes(project))
rmSync(join(tmpdir(), project), { recursive: true, force: true });
}
});
it("uses Count Up's authored three seconds for the wrapper and mounted snippet", async () => {
const html = await preparedHtml(
join(repoRoot, "registry/components/count-up"),
"count-up.html",
);
assert.deepEqual(
[...html.matchAll(/data-duration="([^"]+)"/g)].map((m) => m[1]),
["3", "3"],
);
});
it("lets Touch Indicator's registered timeline determine its preview length", async () => {
const html = await preparedHtml(
join(repoRoot, "registry/components/touch-indicator"),
"touch-indicator.html",
);
assert.ok(!html.includes("data-duration="));
});
for (const fixture of [
{
name: "root duration before a shorter child",
html: '<div data-composition-id="root" data-duration="2.5"><div data-duration="1"></div></div>',
manifest: 8,
expected: "2.5",
},
{
name: "composition duration on a template",
html: "<template data-composition-id='root' data-composition-duration='6.25'></template>",
manifest: 2,
expected: "6.25",
},
{
name: "authored duration inside a template",
html: '<template><div data-composition-id="root" data-duration="2.5"></div></template>',
manifest: 8,
expected: "2.5",
},
{
name: "manifest duration when none is authored",
html: '<div data-composition-id="root"></div>',
manifest: 7,
expected: "7",
},
{
name: "leaves duration for the runtime when none is declared",
html: '<div data-composition-id="root"></div>',
expected: null,
},
{
name: "invalid authored duration",
html: '<div data-composition-id="root" data-duration="3seconds"></div>',
manifest: 7,
expected: "7",
},
]) {
it(fixture.name, async () => {
const sourceDir = mkdtempSync(join(tmpdir(), "catalog-duration-"));
try {
writeFileSync(
join(sourceDir, "registry-item.json"),
JSON.stringify({ duration: fixture.manifest }),
);
writeFileSync(join(sourceDir, "entry.html"), fixture.html);
const html = await preparedHtml(sourceDir, "entry.html");
assert.deepEqual(
[...html.matchAll(/data-duration="([^"]+)"/g)].map((m) => m[1]),
fixture.expected === null ? [] : [fixture.expected, fixture.expected],
);
} finally {
rmSync(sourceDir, { recursive: true, force: true });
}
});
}
});