`);
expect(
result.findings.find(({ code }) => code === "overlapping_clips_same_track"),
).toBeUndefined();
});
});
describe("subcomposition guidance", () => {
it("warns when any HTML composition file is over 300 lines", async () => {
const html = Array.from({ length: 301 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n");
const result = await lintHyperframeHtml(html, {
filePath: "/project/compositions/scene.html",
});
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
});
it("does not warn when an HTML composition file is exactly 300 lines", async () => {
const html = Array.from({ length: 300 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n");
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeUndefined();
});
it("does not count a final trailing newline as an extra physical line", async () => {
const html =
Array.from({ length: 300 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n") + "\n";
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeUndefined();
});
it("does not count inline style block internals as structural lines", async () => {
const style = ``;
const html = `
${style}
TEXT
`;
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeUndefined();
});
it("does not warn for large registry source block files", async () => {
const html = Array.from({ length: 301 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n");
const result = await lintHyperframeHtml(html, {
filePath: "/project/registry/blocks/data-chart/data-chart.html",
});
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeUndefined();
});
it("warns for large installed block composition files", async () => {
const html = Array.from({ length: 301 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n");
const result = await lintHyperframeHtml(html, {
filePath: "/project/compositions/data-chart.html",
});
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
});
it("does not warn for large registry-installed block composition files", async () => {
const html =
"\n" +
Array.from({ length: 300 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n");
const result = await lintHyperframeHtml(html, {
filePath: "/project/compositions/data-chart.html",
});
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeUndefined();
});
it("uses nested split copy for large sub-composition files", async () => {
const html = Array.from({ length: 301 }, (_, i) =>
i === 0 ? "" : ``,
).join("\n");
const result = await lintHyperframeHtml(html, {
filePath: "/project/compositions/scene.html",
isSubComposition: true,
});
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding?.fixHint).toContain("Split this sub-composition further");
});
it("warns when more than 3 timed elements share the same track", async () => {
const html = `
A
B
C
D
`;
const result = await lintHyperframeHtml(html, {
filePath: "/project/compositions/scene.html",
});
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
expect(finding?.message).toContain("Track 0 has 4 timed elements");
});
it("does not warn when 3 timed elements share the same track", async () => {
const html = `
A
B
C
`;
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
expect(finding).toBeUndefined();
});
it("does not warn when timed elements are split across tracks", async () => {
const html = `
A
B
C
D
`;
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
expect(finding).toBeUndefined();
});
it("does not count timed media or script/style tags as dense track elements", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
expect(finding).toBeUndefined();
});
it("does not count transcript caption cues as dense track elements", async () => {
const html = `
一
二
三
四
`;
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
expect(finding).toBeUndefined();
});
it("does not count root composition or mounted sub-compositions as dense elements", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
expect(finding).toBeUndefined();
});
});
describe("duplicate_composition_id", () => {
it("flags a meta tag and root div sharing the same data-composition-id", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("does not flag a single valid composition id", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeUndefined();
});
it("does not flag distinct composition ids in one file", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeUndefined();
});
it("ignores composition ids inside inert template content", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeUndefined();
});
it("flags entity-equivalent composition ids", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeDefined();
});
it("uses the browser's first value for duplicate attributes", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeDefined();
});
});
it("reports error when querySelector uses template literal variable", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "template_literal_selector");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("reports error for querySelectorAll with template literal variable", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "template_literal_selector");
expect(finding).toBeDefined();
});
it("does not report error for hardcoded querySelector strings", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "template_literal_selector");
expect(finding).toBeUndefined();
});
it("reports error when a selector combines data attributes in one bracket", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { filePath: "compositions/scene.html" });
const findings = result.findings.filter((f) => f.code === "split_data_attribute_selector");
expect(findings.length).toBe(1);
expect(findings[0]?.severity).toBe("error");
expect(findings[0]?.fixHint).toContain('[data-composition-id="scene"][data-start="0"]');
});
describe("timed_element_missing_clip_class", () => {
it("flags element with data-start but no class='clip'", async () => {
const html = `
Hello
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "timed_element_missing_clip_class");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("does not flag element that has class='clip'", async () => {
const html = `
Hello
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "timed_element_missing_clip_class");
expect(finding).toBeUndefined();
});
it("does not flag audio or video elements", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "timed_element_missing_clip_class");
expect(finding).toBeUndefined();
});
it("does not flag element with only data-track-index (layer container, no timing)", async () => {
const html = `
Hello
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "timed_element_missing_clip_class");
expect(finding).toBeUndefined();
});
});
describe("overlapping_clips_same_track", () => {
it("flags overlapping clips on the same track", async () => {
const html = `
A
B
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "overlapping_clips_same_track");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("does not flag clips on different tracks", async () => {
const html = `
A
B
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "overlapping_clips_same_track");
expect(finding).toBeUndefined();
});
it("does not flag sequential clips on the same track", async () => {
const html = `
A
B
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "overlapping_clips_same_track");
expect(finding).toBeUndefined();
});
it("does not flag adjacencies where parseFloat + add drifts by a few ulps", async () => {
// parseFloat("0.1") + parseFloat("0.2") = 0.30000000000000004
const html = `
A
B
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "overlapping_clips_same_track");
expect(finding).toBeUndefined();
});
});
describe("root_composition_missing_html_wrapper", () => {
it("flags bare composition div as error", async () => {
// Exact scenario from the screenshot — bare div with composition attributes, no HTML wrapper
const html = `
`;
const result = await lintHyperframeHtml(html, { filePath: "index.html" });
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_html_wrapper",
);
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
expect(result.ok).toBe(false);
});
it("does not flag properly wrapped HTML composition", async () => {
const html = `
Hello
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_html_wrapper",
);
expect(finding).toBeUndefined();
});
it("does not flag composition starting with (no doctype)", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_html_wrapper",
);
expect(finding).toBeUndefined();
});
it("does not flag sub-compositions", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { isSubComposition: true });
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_html_wrapper",
);
expect(finding).toBeUndefined();
});
it("does not flag HTML without composition attributes", async () => {
const html = `
Not a composition
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_html_wrapper",
);
expect(finding).toBeUndefined();
});
it("includes root tag snippet in finding", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "standalone_composition_wrapped_in_template",
);
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("does not flag sub-compositions in template", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { isSubComposition: true });
const finding = result.findings.find(
(f) => f.code === "standalone_composition_wrapped_in_template",
);
expect(finding).toBeUndefined();
});
});
describe("requestanimationframe_in_composition", () => {
it("flags requestAnimationFrame usage in script content", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "requestanimationframe_in_composition",
);
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("does not flag requestAnimationFrame in comments", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "requestanimationframe_in_composition",
);
expect(finding).toBeUndefined();
});
it("does not flag installed registry blocks that use rAF (e.g. particle effects)", async () => {
const html =
`\n` +
`
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "requestanimationframe_in_composition",
);
expect(finding).toBeUndefined();
});
});
describe("missing_data_no_timeline", () => {
it("warns when root has no timeline registration and no data-no-timeline", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "missing_data_no_timeline");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
});
it("does not warn when data-no-timeline is present (boolean form)", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});
it("does not warn when a script registers window.__timelines[id]", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});
it("does not warn when there is no root composition-id", async () => {
const html = `
hello
`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});
it("does not false-positive when data-no-timeline appears only inside an attribute value", async () => {
// Regression: /\bdata-no-timeline\b/ matched substrings inside values
const html = `
`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeDefined();
});
it("does not suppress when a hyphenated variant like data-no-timeline-start is present", async () => {
// Regression: /\bdata-no-timeline\b/ matched data-no-timeline-start because
// hyphen is a non-word char and \b fires between 'e' and '-'
const html = `
`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeDefined();
});
it("does not warn for sub-compositions", async () => {
const html = ``;
const result = await lintHyperframeHtml(html, { isSubComposition: true });
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});
it("does not warn when composition has external scripts (cannot scan for timeline registration)", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});
});
describe("root_composition_missing_data_duration (removed)", () => {
// The rule was a static proxy for the runtime's loop-inflation Infinity
// emission, but lint cannot observe GSAP timeline duration statically and
// the looping shapes that drive it are already covered by
// `gsap_infinite_repeat` and `gsap_repeat_ceil_overshoot`. The rule has
// been removed (#243's Infinity-emission concern is now carried by those
// GSAP rules); these tests pin the removal so the rule does not silently
// come back.
it("does not warn on a docs-compliant root with no data-duration", async () => {
// The documented authoring model: root composition without
// data-duration, runtime derives it from the GSAP timeline.
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "root_composition_missing_data_duration",
);
expect(finding).toBeUndefined();
});
it("does not warn even on the original Infinity-risk shape (no media, looping timeline)", async () => {
// This was the canonical "warn" case under the old rule — root with no
// data-duration, no media, GSAP timeline driven by repeat: -1. The
// looping shape itself is now flagged by `gsap_infinite_repeat`; the
// duplicate `root_composition_missing_data_duration` warning is gone.
const html = `
hello
`;
const result = await lintHyperframeHtml(html);
// The deprecated rule must not fire.
const removedFinding = result.findings.find(
(f) => f.code === "root_composition_missing_data_duration",
);
expect(removedFinding).toBeUndefined();
// The looping shape is still surfaced — by `gsap_infinite_repeat`,
// which is the more actionable signal pointing at the real authoring
// mistake.
const gsapFinding = result.findings.find((f) => f.code === "gsap_infinite_repeat");
expect(gsapFinding).toBeDefined();
});
});
describe("root_composition_missing_data_start", () => {
it("does not warn for template-wrapped sub-composition files", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html, { isSubComposition: true });
const finding = result.findings.find((f) => f.code === "root_composition_missing_data_start");
expect(finding).toBeUndefined();
});
});
describe("unknown_variable_binding", () => {
it("warns when data-var-src references an undeclared variable", async () => {
const html = `
`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "unknown_variable_binding");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
expect(finding?.message).toMatch(/heroImge/);
});
it("stays quiet for declared binding ids and for fragment files", async () => {
const declared = `