diff --git a/packages/lint/src/rules/structure.test.ts b/packages/lint/src/rules/structure.test.ts index 9f3392d7c0..f4d4f619f1 100644 --- a/packages/lint/src/rules/structure.test.ts +++ b/packages/lint/src/rules/structure.test.ts @@ -95,17 +95,18 @@ describe("legacy data-end", () => { }); describe("media_missing_duration", () => { - it("flags img, video and audio without data-duration and passes them with one", async () => { - for (const tag of ["img", "video", "audio"]) { - expect( - has(await codes(`<${tag} src="a" data-start="0">`), "media_missing_duration"), - ).toBe(true); - expect( - has( - await codes(`<${tag} src="a" data-start="0" data-duration="3">`), - "media_missing_duration", - ), - ).toBe(false); + it("flags a timed img without data-duration and passes it with one", async () => { + const bare = await codes(''); + expect(has(bare, "media_missing_duration")).toBe(true); + const timed = await codes(''); + expect(has(timed, "media_missing_duration")).toBe(false); + }); + + it("takes video and audio length from the file, so data-start alone passes", async () => { + for (const tag of ["video", "audio"]) { + const found = await codes(`<${tag} src="a" data-start="0" data-track-index="1">`); + expect(has(found, "media_missing_duration")).toBe(false); + expect(has(found, "timeline_element_missing_timing")).toBe(false); } }); }); diff --git a/packages/lint/src/rules/structure.ts b/packages/lint/src/rules/structure.ts index 7273ee09a0..37f065afd3 100644 --- a/packages/lint/src/rules/structure.ts +++ b/packages/lint/src/rules/structure.ts @@ -45,7 +45,8 @@ const OPAQUE_TAGS = new Set([ ]); // Never layout: their content is code or inert markup. const NON_LAYOUT_TAGS = new Set(["style", "script", "template", "noscript"]); -const MEDIA_TAGS = new Set(["img", "video", "audio"]); +// Their length comes from the media file, so data-start alone is enough. +const FILE_LENGTH_TAGS = new Set(["video", "audio"]); const NODE_ATTRS = [ "id", "class", @@ -124,26 +125,27 @@ function nestedStructureFindings(rows: TagNode[], severity: Severity): Hyperfram } function missingDurationFindings(rows: TagNode[], severity: Severity): HyperframeLintFinding[] { - // A bare media element with no timing is a static layer, not a timeline clip. + // A bare img with no timing is a static layer, not a timeline clip. const intendedClip = (row: TagNode) => - !MEDIA_TAGS.has(row.tag) || + row.tag !== "img" || row.attrs["data-start"] !== undefined || row.attrs["data-track-index"] !== undefined; return rows .filter( (row) => + !FILE_LENGTH_TAGS.has(row.tag) && row.attrs["data-duration"] === undefined && row.attrs["data-end"] === undefined && !isSubCompositionHost(row) && intendedClip(row), ) .map((row) => { - const isMedia = MEDIA_TAGS.has(row.tag); + const isImage = row.tag === "img"; return { - code: isMedia ? "media_missing_duration" : "timeline_element_missing_timing", + code: isImage ? "media_missing_duration" : "timeline_element_missing_timing", severity, - message: isMedia - ? `${describe(row)} is media on the timeline without data-duration, so its clip has no length.` + message: isImage + ? `${describe(row)} is an image on the timeline without data-duration, and an image has no length of its own.` : `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`, elementId: row.attrs.id, fixHint: `Add data-duration (in seconds) to ${describe(row)}.`,