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?
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!
Most helpful comment
Ok so I think I found a workaround.
I changed
animate={{ y: 0 }}toanimate={{ 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.