diff --git a/packages/core/src/lint/rules/composition.test.ts b/packages/core/src/lint/rules/composition.test.ts index 34c0fffcd..6d50c42e0 100644 --- a/packages/core/src/lint/rules/composition.test.ts +++ b/packages/core/src/lint/rules/composition.test.ts @@ -201,6 +201,149 @@ describe("composition rules", () => { }); }); + describe("root_composition_missing_html_wrapper", () => { + it("flags bare composition div as error", () => { + // Exact scenario from the screenshot — bare div with composition attributes, no HTML wrapper + const html = `
+ +
+ + + +
`; + const result = 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", () => { + const html = ` + +
+
Hello
+
+ +`; + const result = 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)", () => { + const html = ` +
+ +`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find( + (f) => f.code === "root_composition_missing_html_wrapper", + ); + expect(finding).toBeUndefined(); + }); + + it("does not flag sub-compositions", () => { + const html = `
+ +
`; + const result = 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", () => { + const html = `

Not a composition

`; + const result = 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", () => { + const html = `
+ +
`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find( + (f) => f.code === "root_composition_missing_html_wrapper", + ); + expect(finding).toBeDefined(); + expect(finding?.snippet).toContain("data-composition-id"); + }); + }); + + describe("standalone_composition_wrapped_in_template", () => { + it("flags root index.html wrapped in template", () => { + const html = ``; + const result = lintHyperframeHtml(html); + const finding = result.findings.find( + (f) => f.code === "standalone_composition_wrapped_in_template", + ); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("warning"); + }); + + it("does not flag sub-compositions in template", () => { + const html = ``; + const result = 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", () => { const html = ` diff --git a/packages/core/src/lint/rules/composition.ts b/packages/core/src/lint/rules/composition.ts index d47e45edf..e7e24e1cd 100644 --- a/packages/core/src/lint/rules/composition.ts +++ b/packages/core/src/lint/rules/composition.ts @@ -231,21 +231,26 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding }, // root_composition_missing_html_wrapper - ({ rawSource, options }) => { + ({ rawSource, rootTag, options }) => { const findings: HyperframeLintFinding[] = []; if (options.isSubComposition) return findings; const trimmed = rawSource.trimStart().toLowerCase(); + // Compositions inside