diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index 17779e5435..e51353c21d 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -3721,6 +3721,43 @@ describe("SVG draw-on rules", () => { expect(finding).toBeUndefined(); }); + describe("gsap_fullscreen_overlay_starts_visible — a scene child hidden through a scoped query", () => { + const scene = (hide: string) => ` + +
+
+
+ +`; + const flagged = async (html: string) => + (await lintHyperframeHtml(html)).findings.some( + (f) => f.code === "gsap_fullscreen_overlay_starts_visible", + ); + + it("accepts gsap.set on a querySelector call with a literal selector", async () => { + expect(await flagged(scene('gsap.set(root.querySelector(".ov"), { opacity: 0 });'))).toBe( + false, + ); + }); + + it("accepts gsap.set on a variable bound to a querySelector call", async () => { + expect( + await flagged(scene('const ov = root.querySelector(".ov"); gsap.set(ov, { opacity: 0 });')), + ).toBe(false); + }); + + it("still flags an overlay nothing hides at zero", async () => { + expect(await flagged(scene(""))).toBe(true); + }); + }); + describe("gsap_fullscreen_overlay_starts_visible — the from() shape is not a defect", () => { const overlay = (style: string, script: string) => ` diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index d0611c9413..2471f4de36 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -253,7 +253,10 @@ function hiddenSetTargetSelectors(target: string, aliases: Map): return parts .flatMap((part) => { const trimmed = part.trim(); - const resolved = /^(["'`])([^"'`]+)\1$/.exec(trimmed)?.[2] ?? aliases.get(trimmed); + const resolved = + /^(["'`])([^"'`]+)\1$/.exec(trimmed)?.[2] ?? + /^[\w$.]+\.querySelector(?:All)?\(\s*(["'`])([^"'`]+)\1\s*\)$/.exec(trimmed)?.[2] ?? + aliases.get(trimmed); return resolved === undefined ? [] : resolved.split(","); }) .map((selector) => selector.trim()) @@ -270,8 +273,13 @@ function extractStandaloneHiddenSelectors(script: string): Set { )) { aliases.set(match[1] ?? "", match[3] ?? ""); } + for (const match of source.matchAll( + /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[\w$.]+\.querySelector(?:All)?\(\s*(["'`])([^"'`]+)\2\s*\)\s*;/g, + )) { + aliases.set(match[1] ?? "", match[3] ?? ""); + } const pattern = - /gsap\.set\s*\(\s*(\[[^[\]]*\]|"[^"]*"|'[^']*'|`[^`]*`|[^,()[\]]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g; + /gsap\.set\s*\(\s*(\[[^[\]]*\]|"[^"]*"|'[^']*'|`[^`]*`|[\w$.]+\.querySelector(?:All)?\(\s*(?:"[^"]*"|'[^']*'|`[^`]*`)\s*\)|[^,()[\]]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g; let match: RegExpExecArray | null; while ((match = pattern.exec(source)) !== null) { // Skip callback/handler bodies; keep IIFEs (they run at parse time).