DEV Community

loading...

Mocking framer-motion v4

Mike Schutte
🗣🗻 I ❤️ programming / mostly front-end 💅 / 😍 typescript, react, graphql (apollo), xstate, RTL, chakra
・2 min read

Testing Library has changed the UI testing game for the better. If you haven't tried it yet, check it out.

At work our new UI efforts are powered by Chakra UI, which uses Framer Motion under the hood for animations. With all this work, we're testing all of it using Jest and React Testing Library (RTL).

One great way to defend against UI regressions (copy, styles, etc) is snapshot testing. As we're getting more and more into Chakra's features and better tests, we've run into issues where the animated style properties have minute differences between snapshots.

RTL recommends mocking animation libraries to solve this problem. There are a few solutions for doing this with framer-motion on the web, but I don't think they are up to snuff for the current version of the library (4._).

After digging around the framer motion source, I realized our attempt at mocking the motion export as an object (see here) wasn't working because motion is constructed using Proxy.

Enough with the words, how do I stabilize my snapshot tests?!

// __mocks__/framer-motion.ts

import { CustomDomComponent, CustomMotionComponentConfig } from 'framer-motion/types/render/dom/motion-proxy';
import * as 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>,
  _customMotionComponentConfig: CustomMotionComponentConfig = {},
): CustomDomComponent<Props> {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return React.forwardRef((props, ref) => {
    const regularProps = Object.fromEntries(
      // do not pass framer props to DOM element
      Object.entries(props).filter(([key]) => !actual.isValidMotionProp(key)),
    );
    return typeof Component === 'string' ? (
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      <div ref={ref} {...regularProps} />
    ) : (
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      <Component ref={ref} {...regularProps} />
    );
  });
}

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

    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return componentCache.get(key)!;
  },
});

module.exports = {
  __esModule: true,
  ...actual,
  AnimatePresence: ({ children }: { children: React.ReactChildren }) => <>{children}</>,
  motion,
};
Enter fullscreen mode Exit fullscreen mode

Now in your test setup file you can call jest.mock('framer-motion') and all the animation related properties will be filtered out.

Happy testing!

Discussion (3)

Collapse
jayantbh profile image
Jayant Bhawal

Somehow removing even a single eslint or ts-ignore comment breaks my tests, but leaving it as is, works. What's going on?

/home/username/path/to/project/__mocks__/framer-motion.tsx:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import * as React from "react";
                                                                                      ^^^^^^

    SyntaxError: Cannot use import statement outside a module

    > 1 | import { AnimatePresence, motion, MotionProps } from "framer-motion";
        | ^
      2 | import React, { FC, useContext } from "react";
Enter fullscreen mode Exit fullscreen mode
Collapse
tmikeschu profile image
Mike Schutte Author

That is indeed strange. Which comment do you removes that produces this error?

Collapse
jayantbh profile image
Jayant Bhawal

Ah, sorry about having commented and basically causing a bit of spam.

Turns out I was facing that issue due to something completely unrelated. No idea why removing comments was causing the issue, but, it is what it is.

Removing any ts-ignore or eslint comment caused the issue.

My fix was to use "module": "commonjs" instead of "esnext" in my tsconfig.test.json.