diff --git a/scripts/creator-editing-recipes.test.mjs b/scripts/creator-editing-recipes.test.mjs index 5d00b4423c..a1fa442b94 100644 --- a/scripts/creator-editing-recipes.test.mjs +++ b/scripts/creator-editing-recipes.test.mjs @@ -73,3 +73,78 @@ test("the Studio skill's safe boxes equal the preview's", async () => { assert.match(skill, new RegExp(`Action-safe\\s*\\|\\s*${action}%`)); assert.match(skill, new RegExp(`Title-safe\\s*\\|\\s*${title}%`)); }); + +async function studioDefaultSeconds(kind) { + const helpers = await read("packages/studio/src/utils/studioHelpers.ts"); + const block = helpers.match(/DEFAULT_TIMELINE_ASSET_DURATION[^=]*=\s*\{([^}]*)\}/); + const defaults = block ? block[1] : ""; + const value = defaults.match(new RegExp(`${kind}:\\s*(\\d+(?:\\.\\d+)?)`)); + return value ? value[1] : undefined; +} + +async function addMediaSection() { + const doc = await read(OWNER); + const afterHeading = doc.split("## Add media")[1] ?? ""; + const section = afterHeading.split("\n## ")[0]; + const imageMatch = section.match(//); + const image = imageMatch ? imageMatch[0] : ""; + return { section, image }; +} + +test("the add-media recipe uses Studio's default durations", async () => { + const { section, image } = await addMediaSection(); + const imageSecs = await studioDefaultSeconds("image"); + assert.ok(imageSecs, "could not read Studio's image default"); + assert.match(section, new RegExp(`defaults to ${imageSecs} seconds`)); + assert.match(section, /`data-start` is enough/); + assert.doesNotMatch(section, /ffprobe/); + assert.match(section, /root composition's `data-duration` is at least/); + assert.match(section, new RegExp(`${imageSecs} for an image unless you set another`)); + assert.doesNotMatch(image, /data-duration/); +}); + +test("the add-media recipe uses Studio's full-frame geometry", async () => { + const { section, image } = await addMediaSection(); + const dropOps = await read("packages/studio/src/hooks/useTimelineAssetDropOps.ts"); + assert.match( + dropOps, + /fitTimelineAssetGeometry\(\s*null,/, + "Studio centres by natural size now; update the doc", + ); + assert.match(section, /fill the whole frame/); + assert.match(image, /left: 0px; top: 0px; width: 1920px; height: 1080px/); +}); + +test("the add-media example carries every attribute Studio's drop writes", async () => { + const { image } = await addMediaSection(); + const drop = await read("packages/studio/src/utils/timelineAssetDrop.ts"); + for (const attr of ['class="clip"', "data-start", "data-duration", "data-track-index"]) { + assert.ok(drop.includes(attr), `Studio no longer writes ${attr}`); + } + for (const attr of ['class="clip"', "data-start", "data-track-index"]) { + assert.ok(image.includes(attr), `doc example lacks ${attr}`); + } +}); + +const CLIP_ATTRS = ["id=", 'class="clip"', "data-start", "data-track-index"]; + +const mediaExample = (section, tag) => + section.match(new RegExp(`<${tag}[\\s\\S]*?`))?.[0] ?? ""; + +const assertNoAuthoredDuration = (example, tag) => { + assert.ok(example, `no <${tag}> example`); + assert.doesNotMatch(example, /data-duration/, `${tag} example must not author a duration`); +}; + +const assertHasAttrs = (example, tag, attrs) => { + for (const attr of attrs) assert.ok(example.includes(attr), `${tag} example lacks ${attr}`); +}; + +test("the video and audio add-media examples carry no data-duration and keep the clip attributes", async () => { + const { section } = await addMediaSection(); + for (const tag of ["video", "audio"]) { + const example = mediaExample(section, tag); + assertNoAuthoredDuration(example, tag); + assertHasAttrs(example, tag, CLIP_ATTRS); + } +}); diff --git a/skills-manifest.json b/skills-manifest.json index 0f9e21c193..b1789536e0 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -34,7 +34,7 @@ "files": 11 }, "hyperframes-core": { - "hash": "ca3bc11c4e163225", + "hash": "99a07539f407ca0d", "files": 11 }, "hyperframes-creative": { diff --git a/skills/hyperframes-core/references/creator-editing-recipes.md b/skills/hyperframes-core/references/creator-editing-recipes.md index 74932476a5..6977324653 100644 --- a/skills/hyperframes-core/references/creator-editing-recipes.md +++ b/skills/hyperframes-core/references/creator-editing-recipes.md @@ -2,6 +2,8 @@ Use these copyable contracts after `tracks-and-clips.md`. Global math: **consumed source = timeline duration × rate**; **natural timeline duration = remaining source / rate**. +Before any edit, run `npx hyperframes timeline` (add `--json` for a machine-readable list) to see the project's tracks and clips instead of reading the HTML. + These recipes keep sound on a separate `