DEV Community

Discussion on: Mocking framer-motion v4

Collapse
 
jasongerbes profile image
Jason Gerbes

The motion function from framer-motion has two use cases:

  1. Wrapping every standard HTML and SVG element (e.g. motion.div, motion.ul, motion.li, etc).
  2. Wrapping custom components (e.g. const MotionComponent = motion(Component)).

You'll notice that the type of motion includes & HTMLMotionComponents & SVGMotionComponents, which means that motion can either be used as a function (for wrapping custom components), or as an object where the properties are all of the HTML and SVG element tags (e.g. motion.h6).

I expect Framer Motion uses the Proxy for standard HTML and SVG elements as an optimisation, so that the wrapped elements are only created when necessary (e.g. you may not need motion.h6 in your application).

Ultimately though, the expected rendered output of HTML and SVG motion elements should be:

  • <motion.div> -> <div>
  • <motion.ul> -> <ul>
  • <motion.li> -> <li>
  • <motion.h6> -> <h6>
  • ...

If you try snapshotting a few non-<div> elements with your current custom function, you'll notice that they all render as <div>. This is due to the typeof Component === 'string' check, which returns <div> for all HTML and SVG element string values.

Here's my full solution, which includes a as typeof custom & DOMMotionComponents cast on the motion proxy, as per the actual motion proxy:

/* eslint-disable react/display-name */
import { CustomDomComponent } from 'framer-motion/types/render/dom/motion-proxy';
import { DOMMotionComponents } from 'framer-motion/types/render/dom/types';
import React from 'react';

const actual = jest.requireActual('framer-motion');

// https://github.com/framer/motion/blob/main/src/render/dom/motion.ts
function custom<Props>(Component: string | React.ComponentType<Props>): CustomDomComponent<Props> {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    return React.forwardRef((props, ref) => {
        // do not pass framer props to DOM element
        const regularProps = Object.entries(props).reduce((acc, [key, value]) => {
            if (!actual.isValidMotionProp(key)) acc[key] = value;
            return acc;
        }, {});

        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        return <Component ref={ref} {...regularProps} />;
    });
}

const componentCache = new Map<string, any>();
const motion = new Proxy(custom, {
    get: (_target, key: string) => {
        if (!componentCache.has(key)) {
            componentCache.set(key, custom(key));
        }

        return componentCache.get(key)!;
    }
}) as typeof custom & DOMMotionComponents;

module.exports = {
    ...actual,
    AnimatePresence: ({ children }: { children: React.ReactChildren }) => <>{children}</>,
    motion
};

Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
byron121 profile image
byron121

I tried the above solution and overwriting AnimatePresence results in this error:

Error: Uncaught [TypeError: Cannot read property 'jsxDEV' of undefined]

I did some digging around and it looks like jsxDEV is from React. Any suggestions on how to resolve this?

Thread Thread
 
tmikeschu profile image
Mike Schutte

Weird! Sorry nothing off the top of my head. I'll post here if I find anything.

Thread Thread
 
neldeles profile image
neldeles

Thanks for this article and thanks Jason as well for the clarification! Indeed my tests started failing because motion elements were being mocked as divs.

This is working for me with Framer Motion 5.6. Maybe one day I'll be as smart as you guys and be able to come up with a mock like this. Will have to copy-paste this for now. 😅