Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Testing AnimatePresence with RTL #285

Closed
Yagogc opened this issue Aug 20, 2019 · 22 comments
Closed

[BUG] Testing AnimatePresence with RTL #285

Yagogc opened this issue Aug 20, 2019 · 22 comments
Labels
bug Something isn't working

Comments

@Yagogc
Copy link

Yagogc commented Aug 20, 2019

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?

@Yagogc Yagogc added the bug Something isn't working label Aug 20, 2019
@made-by-jonny
Copy link

Having the same issue

@CurtisHumphrey
Copy link

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?

@CurtisHumphrey
Copy link

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.

@LasaleFamine
Copy link

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?

@Yagogc
Copy link
Author

Yagogc commented Dec 6, 2019

Any update around how to properly test framer/motion?

@akinorimizushima
Copy link

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

@jmathew
Copy link

jmathew commented Feb 16, 2020

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.

@jmathew
Copy link

jmathew commented Feb 16, 2020

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.

@JeroenReumkens
Copy link

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?

@johnevanofski
Copy link

@jmathew's workaround worked for us!

@mmmoli
Copy link

mmmoli commented Jul 15, 2020

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.

@Aromokeye
Copy link

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

@mattgperry
Copy link
Collaborator

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

@emmanuelviniciusdev
Copy link

emmanuelviniciusdev commented Sep 27, 2020

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

@elliotgonzalez-lk
Copy link

elliotgonzalez-lk commented Nov 19, 2020

@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
Copy link

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

@TSMMark
Copy link

TSMMark commented Feb 1, 2021

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

@chapster11
Copy link

chapster11 commented May 4, 2021

@TSMMark
I'm using your mock above but running into an issue when using motion as a custom component.
https://www.framer.com/api/motion/component/#custom-components

What I get is typeError: (0 , _framermotion.motion) is not a function.
Any idea how one would get around this when using motion with a custom component.

@TSMMark
Copy link

TSMMark commented May 4, 2021

@chapster11 I haven't used custom components, but it looks like the motion() function is the default export of the framer-motion package, so you may need to specify a default property in the returned object of the framer-motion module mock... something like this? (untested)

jest.mock('framer-motion', () => {
  const actual = require.requireActual('framer-motion')
  return {
    __esModule: true,
    ...actual,
    default: (component, options) => /* your mock here */

// ...etc

@ericmurrayclear
Copy link

ericmurrayclear commented May 6, 2021

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.

@jeffreyb5
Copy link

@TSMMark Do you know if there is a better solution to test framer motion divs? Your mock solution works great!

@pau-develop
Copy link

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

This solution did it for me, thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests