import { describe, it, expect } from "vitest"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import { getAnimationsForElement, resolveSelectorElementIds } from "./useGsapTweenCache"; // Minimal Document stub: querySelectorAll returns the elements mapped per selector. function fakeDoc(map: Record): Document { return { querySelectorAll: (sel: string) => (map[sel] ?? []) as unknown as NodeListOf, } as unknown as Document; } function anim(targetSelector: string): GsapAnimation { return { id: `${targetSelector}-to-0`, targetSelector, method: "to", position: 0, properties: {}, }; } describe("getAnimationsForElement", () => { const animations = [anim("#hero"), anim(".kicker"), anim(".kicker"), anim(".co-new")]; it("matches tweens by element id", () => { const result = getAnimationsForElement(animations, { id: "hero" }); expect(result.map((a) => a.targetSelector)).toEqual(["#hero"]); }); it("matches class-targeted tweens by the element's selector", () => { // Real compositions target tweens by class (querySelector(".kicker")); the // selected element has no id, so id-only matching would miss these. const result = getAnimationsForElement(animations, { id: null, selector: ".kicker" }); expect(result).toHaveLength(2); expect(result.every((a) => a.targetSelector === ".kicker")).toBe(true); }); it("matches by id or selector when both are present", () => { const result = getAnimationsForElement(animations, { id: "hero", selector: ".co-new" }); expect(result.map((a) => a.targetSelector).sort()).toEqual(["#hero", ".co-new"]); }); it("returns nothing when neither id nor selector is provided", () => { expect(getAnimationsForElement(animations, {})).toEqual([]); expect(getAnimationsForElement(animations, { id: null, selector: null })).toEqual([]); }); it("matches an element that is one member of a group-selector tween", () => { // Array/toArray targets serialize as a CSS group selector; selecting either // member element should surface the shared tween. const grouped = [anim(".clock-face, .clock-hand")]; expect(getAnimationsForElement(grouped, { selector: ".clock-face" })).toHaveLength(1); expect(getAnimationsForElement(grouped, { selector: ".clock-hand" })).toHaveLength(1); expect(getAnimationsForElement(grouped, { selector: ".unrelated" })).toHaveLength(0); }); it("attributes a class tween to an id-selected element via element.matches", () => { // gsap.from(".dot", {stagger}) — the element is selected by id (#dot-a), so // its selector string never equals ".dot", but the live element matches it. const dots = [anim(".dot")]; const el = { matches: (s: string) => s === ".dot" || s === "#dot-a" } as unknown as Element; expect(getAnimationsForElement(dots, { id: "dot-a", selector: "#dot-a" }, el)).toHaveLength(1); // Without the live element the class tween is still missed (legacy behavior). expect(getAnimationsForElement(dots, { id: "dot-a", selector: "#dot-a" })).toHaveLength(0); }); it("element.matches gates attribution — no over-matching", () => { const dots = [anim(".dot")]; const el = { matches: () => false } as unknown as Element; expect(getAnimationsForElement(dots, { id: "other", selector: "#other" }, el)).toHaveLength(0); }); }); describe("resolveSelectorElementIds", () => { it("resolves a bare #id without touching the DOM", () => { expect(resolveSelectorElementIds("#hero", null)).toEqual(["hero"]); }); it("resolves a class selector to every matching element id (the .dot+stagger case)", () => { const doc = fakeDoc({ ".dot": [{ id: "dot-a" }, { id: "dot-b" }] }); expect(resolveSelectorElementIds(".dot", doc)).toEqual(["dot-a", "dot-b"]); }); it("resolves a group selector across its parts (deduped)", () => { const doc = fakeDoc({ ".a": [{ id: "x" }], ".b": [{ id: "y" }, { id: "x" }] }); expect(resolveSelectorElementIds(".a, .b", doc).sort()).toEqual(["x", "y"]); }); it("falls back to a leading #id when there is no DOM", () => { expect(resolveSelectorElementIds("#card .label", null)).toEqual(["card"]); expect(resolveSelectorElementIds(".dot", null)).toEqual([]); }); });