import React from "react"; import { AnimationWrapper } from "./index"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; // Mock framer-motion const mockUseReducedMotion = jest.fn(() => false); jest.mock("framer-motion", () => { const actual = jest.requireActual("framer-motion"); return { ...actual, useReducedMotion: () => mockUseReducedMotion(), motion: new Proxy( {}, { get: (_target, prop: string) => { const motionKeys = [ "whileHover", "whileTap", "transition", "animate", "initial", "exit", "variants", ]; const MotionMock = React.forwardRef( (props: Record, ref: React.Ref) => { const rest: Record = {}; Object.keys(props).forEach(key => { if (!motionKeys.includes(key)) { rest[key] = props[key]; } }); return React.createElement(prop, { ...rest, ref, "data-motion": "true", "data-while-hover": JSON.stringify(props.whileHover), "data-while-tap": JSON.stringify(props.whileTap), "data-transition": JSON.stringify(props.transition), }); } ); MotionMock.displayName = `Motion(${prop})`; return MotionMock; }, } ), }; }); describe("AnimationWrapper", () => { beforeEach(() => { mockUseReducedMotion.mockReturnValue(false); }); describe("when animation is disabled", () => { it("returns the child directly when disableAnimation is true (default)", () => { render( ); const btn = screen.getByTestId("child"); expect(btn).toBeInTheDocument(); expect(btn.tagName).toBe("BUTTON"); expect(btn).not.toHaveAttribute("data-motion"); }); it("returns the child directly when disableAnimation is explicitly true", () => { render( Text ); expect(screen.getByTestId("child")).not.toHaveAttribute("data-motion"); }); it("returns the child directly when user prefers reduced motion", () => { mockUseReducedMotion.mockReturnValue(true); render(
Content
); expect(screen.getByTestId("child")).not.toHaveAttribute("data-motion"); }); }); describe("when animation is enabled", () => { describe("with intrinsic elements", () => { it("wraps intrinsic element with motion component for scale animation", () => { render(
Animated
); const el = screen.getByTestId("child"); expect(el).toHaveAttribute("data-motion", "true"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ scale: 1.02 }); const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}"); expect(whileTap).toEqual({ scale: 0.98 }); }); it("applies shadow animation preset", () => { render(
Shadow
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ boxShadow: "0 10px 25px rgba(0,0,0,0.15)", }); // shadow preset has no whileTap, so mergedWhileTap is undefined const whileTap = el.getAttribute("data-while-tap"); expect(whileTap).toBeNull(); }); it("applies lift animation preset", () => { render(
Lift
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ y: -5 }); const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}"); expect(whileTap).toEqual({ y: 0 }); }); it("applies opacity animation preset", () => { render(
Opacity
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ opacity: 0.8 }); const transition = JSON.parse( el.getAttribute("data-transition") || "{}" ); expect(transition).toEqual({ duration: 0.2 }); }); it("applies grow animation preset", () => { render(
Grow
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ scale: 1.1 }); const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}"); expect(whileTap).toEqual({ scale: 0.95 }); const transition = JSON.parse( el.getAttribute("data-transition") || "{}" ); expect(transition).toEqual({ type: "spring", stiffness: 300, damping: 20, }); }); it("works with different intrinsic HTML elements (span)", () => { render( Span ); const el = screen.getByTestId("child"); expect(el.tagName).toBe("SPAN"); expect(el).toHaveAttribute("data-motion", "true"); }); it("works with anchor elements", () => { render( Link ); const el = screen.getByTestId("child"); expect(el.tagName).toBe("A"); expect(el).toHaveAttribute("href", "/test"); expect(el).toHaveAttribute("data-motion", "true"); }); }); describe("with composite (non-intrinsic) elements", () => { const CustomComponent = ({ label }: { label: string }) => ( {label} ); it("wraps composite component in motion.div", () => { const { container } = render( ); // The wrapper should be a div with motion attributes const wrapper = container.firstElementChild; expect(wrapper?.tagName).toBe("DIV"); expect(wrapper).toHaveAttribute("data-motion", "true"); // The child should be rendered inside expect(screen.getByTestId("custom")).toBeInTheDocument(); expect(screen.getByText("Hello")).toBeInTheDocument(); }); it("applies animation presets to motion.div wrapper", () => { const { container } = render( ); const wrapper = container.firstElementChild; const whileHover = JSON.parse( wrapper?.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ scale: 1.1 }); }); }); describe("with multiple animation types", () => { it("merges multiple animation presets", () => { render(
Multi
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ scale: 1.02, boxShadow: "0 10px 25px rgba(0,0,0,0.15)", }); }); it("merges whileTap from multiple presets", () => { render(
Multi Tap
); const el = screen.getByTestId("child"); const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}"); expect(whileTap).toEqual({ scale: 0.98, y: 0 }); }); it("merges transitions from multiple presets", () => { render(
Merged
); const el = screen.getByTestId("child"); const transition = JSON.parse( el.getAttribute("data-transition") || "{}" ); expect(transition).toEqual({ duration: 0.2, type: "spring", stiffness: 300, damping: 20, }); }); it("handles empty array of animation types", () => { render(
No Animation
); const el = screen.getByTestId("child"); // With empty array, mergePresets returns undefined, so no preset is applied expect(el).toHaveAttribute("data-motion", "true"); }); }); describe("custom overrides", () => { it("overrides preset whileHover with custom prop", () => { render(
Override
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); // Custom override should merge with preset (custom wins) expect(whileHover.scale).toBe(2); }); it("overrides preset whileTap with custom prop", () => { render(
Override Tap
); const el = screen.getByTestId("child"); const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}"); expect(whileTap.scale).toBe(0.5); }); it("overrides preset transition with custom prop", () => { render(
Override Trans
); const el = screen.getByTestId("child"); const transition = JSON.parse( el.getAttribute("data-transition") || "{}" ); expect(transition.duration).toBe(1); }); it("merges custom whileHover with the default animation preset", () => { render(
Custom Only
); const el = screen.getByTestId("child"); const whileHover = JSON.parse( el.getAttribute("data-while-hover") || "{}" ); expect(whileHover).toEqual({ scale: 1.05, rotate: 10 }); }); it("merges custom whileTap with the default animation preset", () => { render(
Tap
); const el = screen.getByTestId("child"); const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}"); expect(whileTap).toEqual({ scale: 0.95, rotate: -5 }); }); it("merges custom transition with the default animation preset", () => { render(
Trans
); const el = screen.getByTestId("child"); const transition = JSON.parse( el.getAttribute("data-transition") || "{}" ); expect(transition).toEqual({ duration: 0.15, delay: 0.5 }); }); }); describe("no animationType and no custom overrides", () => { it("renders motion component with undefined animation props", () => { render(
Plain
); const el = screen.getByTestId("child"); expect(el).toHaveAttribute("data-motion", "true"); }); }); }); describe("displayName", () => { it("has correct displayName", () => { expect(AnimationWrapper.displayName).toBe("AnimationWrapper"); }); }); });