import { describe, it, expect } from "vitest"; import { lintHyperframeHtml } from "../hyperframeLinter.js"; function compositionWithHead(headContent: string): string { return ` ${headContent}
`; } function compositionWithHeadBoundary(boundaryContent: string): string { return ` ${boundaryContent}
`; } function compositionWithBodyPrefix(prefixContent: string, rootContent = ""): string { return ` ${prefixContent}
${rootContent}
`; } function compositionWithImplicitBodyPrefix(prefixContent: string): string { return ` ${prefixContent}
`; } function templateCompositionWithHead(headContent: string): string { return ` `; } describe("core rules", () => { it("does not lint scripts embedded inside an iframe srcdoc attribute", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find((finding) => finding.code === "invalid_inline_script_syntax"), ).toBeUndefined(); expect( result.findings.find((finding) => finding.code === "gsap_timeline_not_registered"), ).toBeUndefined(); }); it("does not lint elements embedded inside an iframe srcdoc attribute", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find( (finding) => finding.elementId === undefined && finding.message.includes(" { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((item) => item.code === "id_requires_css_escape"); expect(finding?.severity).toBe("warning"); expect(finding?.elementId).toBe("123-frame"); expect(finding?.fixHint).toContain("CSS.escape"); }); it("accepts ids that start with a letter", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((item) => item.code === "id_requires_css_escape")).toBeUndefined(); }); it("reports error when root is missing data-composition-id", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "root_missing_composition_id"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); }); it("reports error when root is missing data-width or data-height", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "root_missing_dimensions"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); }); it("accepts body as the composition root", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "root_missing_composition_id")).toBeUndefined(); expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined(); }); it("skips a leading defs block when detecting the composition root", async () => { // Regression: two independent reports of a leading ... // block (icon/gradient/filter plumbing referenced via url(#id) elsewhere) // getting mistaken for the composition root, since findRootTag returned // the first non-script/style/meta/link/title body child unconditionally. // The here carries no composition markers, so it must be skipped in // favor of the real root that follows it. const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "root_missing_composition_id")).toBeUndefined(); expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined(); }); it("still treats an as the root when it carries composition markers itself", async () => { const html = ` `; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "root_missing_composition_id")).toBeUndefined(); expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined(); }); it("does not mistake a -shaped CSS comment inside `; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "root_missing_composition_id")).toBeUndefined(); expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined(); expect(result.findings.find((f) => f.code === "head_leaked_text")).toBeUndefined(); }); it("reports error when timeline registry is missing", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "missing_timeline_registry"); expect(finding).toBeDefined(); }); it("allows a timeline-free root that explicitly declares data-no-timeline", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "missing_timeline_registry")).toBeUndefined(); }); it("does not flag missing_timeline_registry on a sub-composition (inherits from host)", async () => { const html = `
`; const result = await lintHyperframeHtml(html, { isSubComposition: true }); const finding = result.findings.find((f) => f.code === "missing_timeline_registry"); expect(finding).toBeUndefined(); }); it("reports error for composition host missing data-composition-id", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "host_missing_composition_id"); expect(finding).toBeDefined(); }); it("reports error when timeline registry is assigned without initializing", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "timeline_registry_missing_init"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); expect(finding?.message).toContain("without initializing"); }); it("reports error when dot timeline registry is assigned without initializing", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "timeline_registry_missing_init"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); }); it("does not flag timeline assignment when init guard is present", async () => { const validComposition = `
`; const result = await lintHyperframeHtml(validComposition); const finding = result.findings.find((f) => f.code === "timeline_registry_missing_init"); expect(finding).toBeUndefined(); }); it("reports error when CSS text is left outside a style block in the document head", async () => { const html = compositionWithHead(` /* Decorative Elements */ .particle { position: absolute; width: 4px; height: 4px; background: #fff; } `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); expect(finding?.message).toContain(""); expect(finding?.snippet).toContain(".particle"); }); it("reports error when CSS variables leak between head and body", async () => { const html = compositionWithHeadBoundary(` --bg-color: #F5F1E8; --text-color: #212121; } body { background-color: var(--bg-color); color: var(--text-color); } `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.message).toContain(""); expect(finding?.snippet).toContain("body"); }); it("reports error when stray close tags leak between head and body", async () => { const html = compositionWithHeadBoundary(` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain(""); }); it("reports error when markdown code fences leak between head and body", async () => { const html = compositionWithHeadBoundary(` \`\`\`css .particle { color: white; } \`\`\` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain("```css"); }); it("reports error when CSS at-rules leak between head and body", async () => { const html = compositionWithHeadBoundary(` @media (min-width: 800px) { .particle { transform: scale(1.2); } } `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain("@media"); }); it("does not report leaked text for valid script and style blocks around the head boundary", async () => { const html = compositionWithHeadBoundary(` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeUndefined(); }); it("reports error when CSS text leaks before the composition root", async () => { const html = compositionWithBodyPrefix(` .orphan { position: absolute; inset: 0; } `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain(".orphan"); }); it("reports error when CSS text leaks before the composition root without an explicit body", async () => { const html = compositionWithImplicitBodyPrefix(` .implicit-body-orphan { position: absolute; inset: 0; } `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain(".implicit-body-orphan"); }); it("does not report leaked text for valid script and style blocks before the composition root", async () => { const html = compositionWithBodyPrefix(` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeUndefined(); }); it("does not report CSS-looking educational text inside the composition root", async () => { const html = compositionWithBodyPrefix( "", `
      body {
        margin: 0;
      }
    
`, ); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeUndefined(); }); it("reports error when CSS block comment syntax leaks into visible markup", async () => { const html = compositionWithBodyPrefix( "", ` /* Main Content Block */
Hello
`, ); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "visible_markup_comment"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); expect(finding?.message).toContain("visible HTML markup"); expect(finding?.snippet).toContain("Main Content Block"); }); it("reports error when a misbalanced style block leaves block comment syntax visible", async () => { const html = compositionWithBodyPrefix( "", ` /* Main Content Block */
Hello
`, ); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "visible_markup_comment"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain("Main Content Block"); }); it("does not report block comments inside style or script blocks", async () => { const html = ` /* tab name */ Particle Field
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "visible_markup_comment"); expect(finding).toBeUndefined(); }); it("does not report block comments in attributes, html comments, or protected text contexts", async () => { const html = compositionWithBodyPrefix( "", `
/* visible code sample */
/* visible inline code sample */ /* svg label */ `, ); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "visible_markup_comment"); expect(finding).toBeUndefined(); }); it("reports error when a stray style close tag is left in the document head", async () => { const html = compositionWithHead(` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain(""); }); it("reports error when a stray script close tag is left in the document head", async () => { const html = compositionWithHead(` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain(""); }); it("does not report leaked head text for valid closing tags with trailing whitespace", async () => { const html = compositionWithHead(` Particle Field `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeUndefined(); }); it("reports error when markdown code fences leak into the document head", async () => { const withLanguage = compositionWithHead(` \`\`\`css .particle { position: absolute; } \`\`\` `); const withoutLanguage = compositionWithHead(` \`\`\` .particle { position: absolute; } \`\`\` `); const withTsxLanguage = compositionWithHead(` \`\`\`tsx export function Particle() { return
; } \`\`\` `); const withLanguageResult = await lintHyperframeHtml(withLanguage); const withoutLanguageResult = await lintHyperframeHtml(withoutLanguage); const withTsxLanguageResult = await lintHyperframeHtml(withTsxLanguage); const languageFinding = withLanguageResult.findings.find((f) => f.code === "head_leaked_text"); const unlabeledFinding = withoutLanguageResult.findings.find( (f) => f.code === "head_leaked_text", ); const tsxLanguageFinding = withTsxLanguageResult.findings.find( (f) => f.code === "head_leaked_text", ); expect(languageFinding).toBeDefined(); expect(languageFinding?.snippet).toContain("```css"); expect(unlabeledFinding).toBeDefined(); expect(unlabeledFinding?.snippet).toContain("```"); expect(tsxLanguageFinding).toBeDefined(); expect(tsxLanguageFinding?.snippet).toContain("```tsx"); }); it("reports error when CSS at-rules leak into the document head", async () => { const html = compositionWithHead(` @media (min-width: 800px) { .particle { transform: scale(1.2); } } `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain("@media"); }); it("reports leaked CSS when a style block is unclosed in the document head", async () => { const html = compositionWithHead(` `); const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeUndefined(); }); it("reports leaked head text inside template-wrapped sub-compositions", async () => { const html = templateCompositionWithHead(` .particle { color: white; } `); const result = await lintHyperframeHtml(html, { isSubComposition: true }); const finding = result.findings.find((f) => f.code === "head_leaked_text"); expect(finding).toBeDefined(); expect(finding?.snippet).toContain(".particle"); }); describe("timeline_id_mismatch", () => { it("accepts dot timeline registration", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); expect(finding).toBeUndefined(); }); it("reports mismatched dot timeline registration", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); expect(finding).toBeDefined(); expect(finding?.message).toContain('Timeline registered as "intro"'); }); it("accepts bracket timeline registration for hyphenated ids", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); expect(finding).toBeUndefined(); }); it("matches timeline keys against browser-decoded composition ids", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "timeline_id_mismatch")).toBeUndefined(); }); it("accepts object-literal timeline registration and extracts its keys", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "missing_timeline_registry")).toBeUndefined(); expect( result.findings.find((f) => f.code === "timeline_registry_missing_init"), ).toBeUndefined(); expect(result.findings.find((f) => f.code === "timeline_id_mismatch")).toBeUndefined(); }); it("reports mismatched object-literal timeline registration keys", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); expect(finding).toBeDefined(); expect(finding?.message).toContain('Timeline registered as "main"'); }); }); describe("repeated_id_descendant_selector", () => { it("reports a selector that nests the same id inside itself", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "repeated_id_descendant_selector"); expect(finding?.severity).toBe("error"); expect(finding?.selector).toBe("#scene_01 #scene_01 .headline"); }); it("does not report distinct descendant ids", async () => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find((f) => f.code === "repeated_id_descendant_selector"), ).toBeUndefined(); }); it.each(["#scene_01 > #scene_01", "#scene_01 .wrapper #scene_01"])( "reports repeated ids across descendant combinators: %s", async (selector) => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find((f) => f.code === "repeated_id_descendant_selector"), ).toBeDefined(); }, ); it.each([ ":is(#scene_01) #scene_01", ":where(#scene_01) #scene_01", "#scene_01 :is(#scene_01)", ])("reports repeated ids required by selector pseudos: %s", async (selector) => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find((f) => f.code === "repeated_id_descendant_selector"), ).toBeDefined(); }); it.each([ ":is(#scene_01, .scene) #scene_01", ":not(#scene_01) #scene_01", ":has(#scene_01) #scene_01", ])("does not report ids that are not required by a selector pseudo: %s", async (selector) => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find((f) => f.code === "repeated_id_descendant_selector"), ).toBeUndefined(); }); it.each(["& #scene_01 .headline", "#scene_01 .headline"])( "reports repeated ids created by nested CSS: %s", async (nestedSelector) => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find( (candidate) => candidate.code === "repeated_id_descendant_selector", ); expect(finding).toBeDefined(); expect(finding?.selector).toContain("#scene_01 #scene_01"); }, ); it("preserves dollar sequences while resolving nested selectors", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find( (candidate) => candidate.code === "repeated_id_descendant_selector", ); expect(finding?.selector).toBe('#scene_01[data-query="$1"] #scene_01'); }); it.each(['[data-query="#scene_01 #scene_01"]', String.raw`#scene_01 #scene_01\:child`])( "does not report non-repeated parsed ids: %s", async (selector) => { const html = `
`; const result = await lintHyperframeHtml(html); expect( result.findings.find((f) => f.code === "repeated_id_descendant_selector"), ).toBeUndefined(); }, ); }); it("warns when a timeline-visible element has no stable id for Studio editing", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "studio_missing_editable_id"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("warning"); expect(finding?.message).toContain('
'); expect(finding?.fixHint).toContain("stable, human-readable id"); }); it("does not warn about the composition root or timeline elements with ids", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "studio_missing_editable_id"); expect(finding).toBeUndefined(); }); describe("non_deterministic_code", () => { it("detects Math.random() in script content", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); expect(finding?.message).toContain("Math.random"); }); it("detects Date.now() in script content", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); expect(finding?.message).toContain("Date.now"); }); it("does not flag non-deterministic calls inside single-line comments", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeUndefined(); }); it("detects gsap.utils.random() in script content", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("error"); expect(finding?.message).toContain("gsap.utils.random"); }); it("detects GSAP 'random(...)' string tween values", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeDefined(); expect(finding?.message).toContain('"random(...)"'); }); it("detects prefixed '+=random(...)' string tween values", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeDefined(); }); it("does NOT flag prose strings that merely mention random(", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "non_deterministic_code"); expect(finding).toBeUndefined(); }); }); describe("composition_self_attribute_selector", () => { it("warns when inline CSS targets the root composition id", async () => { const html = `

Hello

`; const result = await lintHyperframeHtml(html); const findings = result.findings.filter( (f) => f.code === "composition_self_attribute_selector", ); expect(findings).toHaveLength(1); expect(findings[0]?.severity).toBe("warning"); expect(findings[0]?.selector).toBe('[data-composition-id="scene"] .title'); expect(findings[0]?.fixHint).toContain("#scene"); expect(findings[0]?.fixHint).not.toContain("#556"); }); it("warns when external CSS targets the root composition id", async () => { const html = `
`; const result = await lintHyperframeHtml(html, { externalStyles: [ { href: "scene.css", content: '[data-composition-id="scene"] .title { opacity: 0; }', }, ], }); const finding = result.findings.find((f) => f.code === "composition_self_attribute_selector"); expect(finding).toBeDefined(); expect(finding?.selector).toBe('[data-composition-id="scene"] .title'); }); it("does not warn when CSS targets a different composition id", async () => { const html = `
`; const result = await lintHyperframeHtml(html); const finding = result.findings.find((f) => f.code === "composition_self_attribute_selector"); expect(finding).toBeUndefined(); }); }); });