diff --git a/packages/core/src/lint/hyperframeLinter.test.ts b/packages/core/src/lint/hyperframeLinter.test.ts index a3fe608e2..1e4b6b618 100644 --- a/packages/core/src/lint/hyperframeLinter.test.ts +++ b/packages/core/src/lint/hyperframeLinter.test.ts @@ -217,6 +217,120 @@ describe("lintScriptUrls", () => { vi.unstubAllGlobals(); }); + + // ── gsap_css_transform_conflict ────────────────────────────────────────── + + it("warns when tl.to animates x on an element with CSS translateX", () => { + const html = ` + +
+
+
+ + +`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_css_transform_conflict"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("warning"); + expect(finding?.selector).toBe("#title"); + expect(finding?.fixHint).toMatch(/fromTo/); + expect(finding?.fixHint).toMatch(/xPercent/); + }); + + it("warns when tl.to animates scale on an element with CSS scale transform", () => { + const html = ` + +
+
+
+ + +`; + const result = lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_css_transform_conflict"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("warning"); + expect(finding?.selector).toBe("#hero"); + }); + + it("does NOT warn when tl.to targets element without CSS transform", () => { + const html = ` + +
+
+
+ + +`; + const result = lintHyperframeHtml(html); + const conflict = result.findings.find((f) => f.code === "gsap_css_transform_conflict"); + expect(conflict).toBeUndefined(); + }); + + it("does NOT warn when tl.fromTo targets element WITH CSS transform (author owns both ends)", () => { + const html = ` + +
+
+
+ + +`; + const result = lintHyperframeHtml(html); + const conflict = result.findings.find((f) => f.code === "gsap_css_transform_conflict"); + expect(conflict).toBeUndefined(); + }); + + it("emits one warning when a combined CSS transform conflicts with multiple GSAP properties", () => { + const html = ` + +
+
+
+ + +`; + const result = lintHyperframeHtml(html); + const conflicts = result.findings.filter((f) => f.code === "gsap_css_transform_conflict"); + expect(conflicts).toHaveLength(1); + expect(conflicts[0]?.message).toMatch(/x\/scale|scale\/x/); + }); }); describe("template_literal_selector rule", () => { diff --git a/packages/core/src/lint/hyperframeLinter.ts b/packages/core/src/lint/hyperframeLinter.ts index f43deb70c..f97c79e9f 100644 --- a/packages/core/src/lint/hyperframeLinter.ts +++ b/packages/core/src/lint/hyperframeLinter.ts @@ -21,6 +21,7 @@ type GsapWindow = { end: number; properties: string[]; overwriteAuto: boolean; + method: string; raw: string; }; @@ -522,6 +523,104 @@ export function lintHyperframeHtml( } } + // ── Rule: gsap_css_transform_conflict ───────────────────────────────────── + // Detects elements whose CSS