-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
How to test React.Portal #62
Comments
checked source code, Dialog uses Portal so maybe thats why? |
was able to workaround with { container: document.body } passed into render |
Glad you found a workaround 👍 |
Make sure to unmount at the end of your test to clean up the document.body |
|
Thanks @bugzpodder. Worked! |
I would not recommend that solution. That's just hiding the problem and it's kinda annoying boilerplate. Here's another example of how to test a portal: https://github.com/kentcdodds/react-testing-library-course/blob/8069bf725dc0dd3774c39f7b8c5a3b226d2f06d0/src/__tests__/06.js Rather than using |
ahmmm...I can see now why I had never got
But, as I can see in your example, the getByTestId came from
This works as well. |
I struggle to find an accessible example on testing portals with react-testing-library. Can you guys help me? I have the following code: let portalRoot = document.getElementById("portal")
export default class Modal extends React.Component {
constructor(props) {
super(props)
this.el = document.createElement("div")
}
componentDidMount() {
portalRoot.appendChild(this.el)
}
componentWillUnmount() {
portalRoot.removeChild(this.el)
}
render() {
let {children, toggle, on, modalBgClass = "", modalWindowClass = ""} = this.props
return on && ReactDOM.createPortal(
<Background modalBgClass={modalBgClass} toggle={toggle}>
<div className={cc("window", modalWindowClass)}>
<div className="close">
<i className="icon fa fa-times" aria-label="Close" onClick={toggle}></i>
</div>
{children}
</div>
</Background>
, this.el)
}
} which breaks with
How to emulate the same structure or workaround it alternatively with this library? |
The problem is that when the code You can create it with: const portalRoot = document.createElement('div')
portalRoot.setAttribute('id, 'portal')
document.body.appendChild(portalRoot) There are various ways you can do this (in a test setup file, or in a test beforeAll which would require a slight modification to your component to query for that element within the lifecycles). What I would do personally that would solve this is in your component file, do this: let portalRoot = document.getElementById("portal")
if (!portalRoot) {
portalRoot = document.createElement('div')
portalRoot.setAttribute('id, 'portal')
document.body.appendChild(portalRoot)
} This would also mean that you don't have to have the HTML in your app with the portal element ahead of time which reduces the chance of this error happening in production 👌 Good luck! |
For my case, I can set my portal component's mount point different than document.body via component props, but I don't know how to specify the container before calling Is it possible to expect the created root |
@kentcdodds Thank you very much for the link and sample code how 'createPortal' can be tested. I'd like to ask how we can test functional component which contains 'createPortal'. Thanks!!! |
The test would be exactly the same. That's the beauty of react-testing-library being free of implementation details. |
For anyone curious: Specifically with material-ui, this fixed my problem. For my working example: const { container, getByText } = render(
<I18nTestProvider>
<LanguageSelect />
</I18nTestProvider>,
{ container: document.body },
);
const selectComponent = container.querySelector('#select-language');
act(() => {
fireEvent.click(selectComponent);
});
await wait(() => getByText('French'));
expect(container).toMatchSnapshot(); |
What is the recommended way to test portals now? Passing |
I'm wondering the same as above. I'm using material-ui v4 and the
|
Here's how I recommend writing/testing modals/portals: https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/master/?fontsize=14&module=%2Fsrc%2F__tests__%2Fportals.js |
Why do we render into import { render } from 'react-testing-library';
import { MyPortalledComponent } from '..';
describe('MyPortalledComponent', () => {
it('snapshot', () => {
render(<MyPortalledComponent/>);
expect(document.body.lastChild).toMatchSnapshot();
});
}); |
I'm guessing you're asking "why does React Testing Library append the container to the body?" And the answer is because otherwise React's event delegation system would not work and you wouldn't be able to fire DOM events at any of your elements. You don't need to bother with telling React Testing Library where to find the element. All queries are pre-bound to |
I was able to get this working the way I expected by snapshotting the // Note: You can't use expect(container).toMatchSnapshot() if you
// wish to include the dialog's contents in your snapshot
expect(baseElement).toMatchSnapshot() Full example with working code sandbox: import React from 'react'
import {
Button,
Dialog,
DialogContent,
DialogContentText,
DialogActions,
} from '@material-ui/core'
import {render, fireEvent} from '@testing-library/react'
import '@testing-library/react/cleanup-after-each'
import 'jest-dom/extend-expect'
const MaterialDialog = () => {
const [open, setOpen] = React.useState(false)
function handleClickOpen() {
setOpen(true)
}
function handleClose() {
setOpen(false)
}
return (
<React.Fragment>
<Button
variant="contained"
color="primary"
onClick={handleClickOpen}
data-testid="open-dialog"
>
Open dialog
</Button>
<Dialog open={open} onClose={handleClose} fullWidth={true} maxWidth="md">
<DialogContent>
<DialogContentText data-testid="dialog-message">
Hello from inside the dialog!
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
OK
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
)
}
test('material dialog button can be interacted with to show a message', () => {
const {baseElement, getByTestId} = render(<MaterialDialog />)
fireEvent.click(getByTestId('open-dialog'))
// Asserting content within the dialog after being opened
expect(getByTestId('dialog-message')).toHaveTextContent(
'Hello from inside the dialog!',
)
// Note: You can't use expect(container).toMatchSnapshot() if you
// wish to include the dialog's contents in your snapshot
expect(baseElement).toMatchSnapshot()
}) |
@ynotdraw did you find a solution to this? I am also having this issue with Material UI using React hooks and the latest version of @testing-library/react
debug shows an empty div
|
We had a wrapper over the Material Dialog that used withMobileDialog and that was the reason why snapshots did't display custom dialog content |
These questions are best answered on our spectrum.chat. I think the Select + Dialog integration test and Dialog unit test on the Material-UI repository are some good ressources if you want a deep dive into Material-UI + testing-library testing I don't use snapshot testing for React trees or DOM trees anymore so I can't give any advice on that. |
@ArtemAstakhov - thanks for that insight! Our team had the same problem that @aemc noted above, where We spent a good amount of time researching why we weren't able to access content inside our This issue lead us to this page in the MUI docs, and implementing that workaround allowed us to fully test our Dialogs. |
Awww yeah this worked for me. |
import React from 'react'
import { RenderResult, render, waitFor } from '@testing-library/react'
import ButtonBeAPartner from '~/components/ButtonBeAPartner'
import Design, { Modal } from '~/components/Design'
describe('components/ButtonBeAPartner', () => {
let wrapper: RenderResult
beforeEach(() => {
wrapper = render(
<Design>
<ButtonBeAPartner />
<Modal>
<span>Hi!</span>
</Modal>
</Design>
)
waitFor(() => null)
})
describe('when rendering', () => {
it('the modal does not exist in the document', () => {
expect(wrapper.queryAllByTestId('modal')).toHaveLength(0)
})
describe('when click the button', () => {
it('the modal appears', () => {
wrapper.getByTestId('button-be-a-partner').click()
expect(wrapper.getByTestId('modal')).toBeInTheDocument()
})
})
})
}) |
I have found following solution: const { baseElement } = render(<Modal />);
// Snapshot
expect(baseElement).toMatchSnapshot();
// Query element
const modal = getQueriesForElement(baseElement).queryByTestId('modal');
expect(modal.innerText).toBe('Modal content'); Resulting snapshot:
|
Reason: the portal results in render to return null . Compare testing-library/react-testing-library#62
For people getting <body>
<div />
</body> For me, It had nothing to do with |
You can also access it via screen
|
@eps1lon what do you think of this solution import React from "react";
import {render, cleanup} from "@testing-library/react";
import {Dialog} from "@material-ui/core";
afterEach(cleanup);
describe("<Dialog />", () => {
it("renders component", () => {
const container = document.createElement("div");
document.body.append(container)
const {asFragment} = render(
<Dialog open={true} container={container} />,
{container}
);
expect(asFragment()).toMatchSnapshot();
});
}); |
Thank you for this suggestion. This solved my issue. 🎸 |
This thread has helped me a lot 👍 I decided to:
|
I disagree; that page is now a 404, and all of the questions and answers that were presumably there are gone, and not in Google's index. Apparently the new home is Discord, which faces a similar problem - this issue has solved my problem today pretty quickly, and presumably many other people's - but if it only existed in Discord or Slack or something else, it would be completely undiscoverable, and would take much longer for any given person to find their answer, and would require people to actively take time out to help. |
In my case, I solved it in a much simpler way: Component: Spec:
|
Is this still recommended solution, because in another thread i saw this was deprecated. |
Tried so many ways from this thread unfortunately Mui Dialog is not being rendered! |
Here's how I managed to test portals: import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
it('shows hint on hover', async () => {
const children = 'children';
const hintBody = 'hint body';
render(<Hint text={hintBody}>{children}</Hint>)
const hint = screen.getByText('your text goes here')
await userEvent.hover(hint)
await screen.findByText(hintBody)
}) P.S. |
This worked for me and was the most simple way I could find: import {
getQueriesForElement,
render,
RenderResult,
screen,
} from "@testing-library/react";
describe('tests', () => {
let result: RenderResult;
const base = async () => {
const { baseElement } = result;
return baseElement instanceof HTMLElement
? await getQueriesForElement(baseElement)
: screen;
};
beforeEach(() => {
result = render(...);
});
}); then use |
I ran into issues when trying to test the I realized that I was missing the
// TestComponent.tsx
import { useEffect } from "react";
import { useToast } from "@chakra-ui/react";
interface Props {}
export const TestComponent: React.FC<Props> = () => {
const toast = useToast();
useEffect(() => {
toast({
title: "hello",
duration: 3000,
isClosable: true,
status: "success",
position: "top-right",
});
});
return <></>;
};
// TestComponent.spec.tsx
import { render, screen } from "@testing-library/react";
import { SaveButton } from "./SaveButton";
import { ChakraProvider, theme } from "@chakra-ui/react";
it("should display toast", () => {
render(
<ChakraProvider theme={theme}>
<TestComponent />
</ChakraProvider>
);
screen.getByText("hello");
}); |
The thing that worked for us was to use this:
You can now access container the same way you would normally, otherwise it tries to grab the first element which is an empty div in the case of a portal element. |
Thanks a lot, it worked in my case :) |
react-testing-library: 2.1.1
node: 8.9.3
yarn: 1.6.0
in the snapshot, it is just a simple div, and the Close button could not be found. It is not immediately not obvious what's went wrong here.
I was using enzyme and it is working fine.
The text was updated successfully, but these errors were encountered: