diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index 2715ff5a9c..6ee7f65713 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -1698,4 +1698,25 @@ describe("GSAP rules", () => { const finding = result.findings.find((f) => f.code === "gsap_non_transform_motion"); expect(finding).toBeUndefined(); }); + + it("scene_layer_missing_visibility_kill: does NOT mistake fromTo entrance opacity for an exit", async () => { + const html = ` + +
+
+
+
+ +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "scene_layer_missing_visibility_kill"); + expect(finding).toBeUndefined(); + }); }); diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index b54b7c8732..0ce612fd04 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -1020,9 +1020,16 @@ export const gsapRules: LintRule[] = [ // For each scene, check if there's a visibility:hidden set after exit tweens for (const tag of sceneElements) { const id = readAttr(tag.raw, "id") || ""; - // Check if this scene has exit tweens (opacity: 0) - const exitPattern = new RegExp(`["']#${id}["'][^)]*opacity\\s*:\\s*0`); - const hasExit = exitPattern.test(content); + // Check for an actual exit tween. A fromTo entrance often puts + // opacity:0 in its FIRST vars object; that is not an exit and the final + // scene must remain visible through the last frame. + const toExitPattern = new RegExp( + `\\.to\\(\\s*["']#${id}["']\\s*,\\s*\\{[^}]*opacity\\s*:\\s*0`, + ); + const fromToExitPattern = new RegExp( + `\\.fromTo\\(\\s*["']#${id}["']\\s*,\\s*\\{[^}]*\\}\\s*,\\s*\\{[^}]*opacity\\s*:\\s*0`, + ); + const hasExit = toExitPattern.test(content) || fromToExitPattern.test(content); if (!hasExit) continue; // Check if there's a hard visibility kill