motion: [BUG] Testing AnimatePresence with RTL

Hi!

Describe the bug I’m using AnimatePresence to animate the enter & exit of a modal based on a boolean prop. The issue comes when I want to test, with React Testing Library, the rendering of the AnimatePresence childrens. I get an error and I’m not able to resolve the issue.

To Reproduce

// Modal.tsx
import React from 'react'
import { AnimatePresence } from 'framer-motion'
import { ModalBackground, ModalContainer } from './styles'

interface ModalProps {
  isOpen: boolean
}

const Modal: React.FC<ModalProps> = ({ children, isOpen }) => {
  return (
    <AnimatePresence>
      {isOpen && (
        <ModalBackground
          key="modal-background"
          initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
          animate={{ opacity: 1, backdropFilter: 'blur(4px)' }}
          exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
          transition={{ duration: 0.5 }}
        >
          <ModalContainer
            key="modal-container"
            initial={{ y: '100%' }}
            animate={{ y: 0 }}
            exit={{ y: '100%' }}
            data-testid="modal-container"
          >
            {children}
          </ModalContainer>
        </ModalBackground>
      )}
    </AnimatePresence>
  )
}

export default Modal
// Modal.test.tsx
import React from 'react'
import Modal from './Modal'
import { render } from '@testing-library/react'

describe('Modal', () => {
  const MockComponent = () => <span />
  it(`doesn't render any children`, () => {
    const { container } = render(
      <Modal isOpen={false}>
        <MockComponent />
      </Modal>
    )

    expect(container.firstChild).toBeFalsy()
  })

  // THIS TEST FAILS
  it(`renders the modal`, () => {
    const { findByTestId } = render(
      <Modal isOpen>
        <MockComponent />
      </Modal>
    )

    findByTestId('modal-container')
  })
})

TypeError: Cannot read property '1' of null

  console.error ../node_modules/react-dom/cjs/react-dom.development.js:19814
    The above error occurred in one of your React components:
        in Unknown (created by ForwardRef(MotionComponent))
        in ForwardRef(MotionComponent) (created by ModalContainer)
        in ModalContainer (at Modal.tsx:34)
        in div (created by ForwardRef(MotionComponent))
        in ForwardRef(MotionComponent) (created by ModalBackground)
        in ModalBackground (at Modal.tsx:26)
        in PresenceChild (created by AnimatePresence)
        in AnimatePresence (at Modal.tsx:24)
        in Modal (at Modal.test.tsx:20)

Additional context The error only happens when I pass the boolean prop isOpen as true. As false doesn’t render anything as expected.

Any ideas?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 15
  • Comments: 22

Most upvoted comments

I don’t like this at all but I solved the issue with this jest mock

jest.mock('framer-motion', () => {
  const actual = require.requireActual('framer-motion')
  return {
    __esModule: true,
    ...actual,
    AnimatePresence: ({ children }) => (
      <div className='mocked-framer-motion-AnimatePresence'>
        { children }
      </div>
    ),
    motion: {
      ...actual.motion,
      div: ({ children }) => (
        <div className='mocked-framer-motion-div'>
          { children }
        </div>
      ),
    },
  }
})

Obviously if you’re using other motion elements other than motion.div you will need to mock those as well

Thanks @TSMMark unfortunately that gives the same error result. I wish framer-motion would give some test examples. So far I have to use @testing-library/react waitFor to wait for the animation to complete. This is slowing down tests. I was hoping your method would allow me to mock it in a way where I could not have to wait for the animation to complete before I could see the rendered result.

Any update around how to properly test framer/motion?

Ok so I think I found a workaround.

I changed animate={{ y: 0 }} to animate={{ y: '0%'}} and it seems to pass the test and produce the exact same result as before animation wise.

Must be a bug in the code where if it sees a numeric for the animate target it treats the initial as a numeric as well. Or something like that.

This issue unfortunately occurs with any x% transform. Currently experiencing the same with x: '100%' as a normal animate on a motion.div. Anyone who already found a solution to this?

Same issue for me. The @CurtisHumphrey solution doesn’t seem to be the proper one, is there any correct solution for this or we need to wait the bug to be solved?

So I solved my bug my simply wrapping my variants that changed y in an if(!__JEST__) so that no y translation happens and the bug disappeared.

@emmanuelviniciusdev

I am also having this issue. My use case is a little different since I am trying to implement animated notifications. In addition to a close button, my notifications have a settimeout that removes them from the DOM after 30 seconds. Testing for either of these outcomes with <AnimatePresence >does not work. RTL still sees them in the DOM even after using faketimers. If I remove <AnimatePresence> from my iterated list, all tests pass as normal but I lose the exit animations.

There is a workaround which may work in your case since you aren’t implementing a timeout. You can wrap your expect assertion in RTLs waitFor (make sure to async your test and await waitFor). That should pass your test above, its worth a try.

I’m having the same issue with a similar use case. I have a modal component as well and I am wrapping it using AnimatePresence. When I click the close button the content of my modal should disappear from DOM, but it doesn’t happen when I’m using AnimatePresence. Any ideas on how do handle it?

My component

const OceanoModal: React.FunctionComponent<OceanoModalType> = ({
  title,
  text,
  open = !false,
  children,
}) => {
  const [isOpened, setIsOpened] = useState(open);

  useEffect(() => setIsOpened(open), [open]);

  /**
   * It adds a listener for 'ESC' key. When pressed, 'isOpened' is set to false.
   */
  useEffect(() => {
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') closeModal();
    });
  }, []);

  const closeModal = () => setIsOpened(false);

  return (
    <>
      <AnimatePresence>
        {isOpened && (
          <div data-testid="oceano-modal-wrapper">
            <ModalBackground>
              <motion.div
                key="motion-wrapper-modal-content"
                initial={{ scale: 0 }}
                animate={{ scale: 1 }}
                exit={{ scale: 0.7 }}
              >
                <ModalContent>
                  <ModalCloseButton
                    data-testid="oceano-modal-close-button"
                    onClick={closeModal}
                  >
                    <CloseIcon fontSize="inherit" />
                  </ModalCloseButton>
                  <ModalTitle>{title}</ModalTitle>
                  <ModalText>{text}</ModalText>
                  <ModalActions>{children}</ModalActions>
                </ModalContent>
              </motion.div>
            </ModalBackground>
          </div>
        )}
      </AnimatePresence>
    </>
  );
};

export default OceanoModal;

My test

it('should close modal when close button is pressed', () => {
    const { debug } = render(
      <OceanoModal open title="My modal title" text="My modal text" />
    );

    fireEvent.click(screen.getByTestId('oceano-modal-close-button'));

    // expect(screen.getByTestId('oceano-modal-wrapper')).not.toBeInTheDocument();

    debug();
  });

@Aromokeye Motion accepts any value type. Animating between them requires DOM measurements though, which aren’t available in jsdom. I believe the solution is to test with a browser-based suite (we have to use Cypress as well as Jest to be able to properly test Motion) or explore mocking getTranslateFromMatrix.

This fails for me with react testing library:


const Background = ({children}) => {
  return (
    <div className="h-screen">
      <Frame>
        frame
        {children}
      </Frame>
    </div>
  )
}


test('renders background color', () => {
    const {getByText} = renderComp()
    expect(getByText(/frame/i)).toBeInTheDocument()
  })

Console complains:

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

I did a bit more digging and it happening in the getTranslateFromMatrix function. I’m guessing from the transform.match(/^matrix that matrix is expected in the field but the actual string in my case is translateY(100%) translateZ(0). Thus JSDOM must be doing something different than a real browser?

Having the same issue