Motion: [BUG] Testing AnimatePresence with RTL

Created on 20 Aug 2019  路  16Comments  路  Source: framer/motion

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?

bug

Most helpful comment

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.

All 16 comments

Having the same issue

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?

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.

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?

Any update around how to properly test framer/motion?

It might not solve your issue, but I could solve mine by replacing { x: '100%' } with { x: 100 }

I can confirm that switching to a numeric value works. However '100%' is radically different from 100. I basically have to calculate the exact amount that represents the height of the container and then apply it. Or I can just pick a big enough value and accept that the animation is going to look a little different.

Also I'm not in favor of if(__JEST__) solutions. I am actually specifically trying to test that a cover appears over an element on mouse hover so simply disabling the covering effect would beat the point.

I also debugged and the point of failure is the getTranslateFromMatrix not accounting for the translateY(100%) translateZ(0) as the transform string.

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?

@jmathew's workaround worked for us!

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.

Ok so It seems framer-motion only accepts pixels so one is expected to just use numbers only.

@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.

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();
  });

@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.

@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.

Thanks!

Was this page helpful?
0 / 5 - 0 ratings