Cannot test combobox rendered in portal with react-testing-library (headlessui v2)
crissadriana opened this issue · comments
What package within Headless UI are you using?
@headlessui/react
What version of that package are you using?
v2.0.4
Describe your issue
After upgrading to version 2, the combobox using anchor opens in a portal on the body and the integration tests using react-testing-library are now failing. For example, I'm trying to test a component where I should render the combobox but when I have to check the contents of it, the test is failing because it doesn't find the list of options. I have tried to add the body as a wrapper for the tested component but that doesn't fix it. Do you have any suggestions on that?
+1
Happens with listbox
as well
Hey!
Can you share a minimal reproduction repo that shows how you are testing the component?
Hey @RobinMalfait, this is one of the most minimal example I can do
made with create-react-app, matching the versions in my package.json
npm run test
and watch is hang
https://github.com/optimistic-updt/repro-headless-jest-portal
Thank you for looking into it
@RobinMalfait Sorry for the late reply, I was off the past week. Similarly to what @optimistic-updt shared already, I'm trying to test a component (Example.tsx) using combobox.
Example.tsx
import { forwardRef, useState } from "react";
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
} from "@headlessui/react";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
const MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />;
});
function Example() {
const [selectedPeople, setSelectedPeople] = useState([people[0], people[1]]);
const [query, setQuery] = useState("");
const filteredPeople =
query === ""
? people
: people.filter((person) => {
return person.name.toLowerCase().includes(query.toLowerCase());
});
return (
<Combobox
multiple
value={selectedPeople}
onChange={setSelectedPeople}
onClose={() => setQuery("")}
>
{selectedPeople.length > 0 && (
<ul>
{selectedPeople.map((person) => (
<li key={person.id}>{person.name}</li>
))}
</ul>
)}
<ComboboxInput
aria-label="Assignees"
onChange={(event) => setQuery(event.target.value)}
/>
<ComboboxButton as={MyCustomButton}>Open</ComboboxButton>
<ComboboxOptions anchor="bottom" className="border empty:invisible">
{filteredPeople.map((person) => (
<ComboboxOption
key={person.id}
value={person}
className="data-[focus]:bg-blue-100"
>
{person.name}
</ComboboxOption>
))}
</ComboboxOptions>
</Combobox>
);
}
Example.test.tsx
import { fireEvent, render, screen } from "@testing-library/react";
import { expect, vi } from "vitest";
describe("Example", () => {
it("renders correctly the example options", async () => {
render(<Example />);
const dropdownButton = await screen.findByRole("button");
fireEvent.click(dropdownButton);
expect(screen.getByText("Katelyn Rohan")).toBeInTheDocument(); // Failing this line as the combobox options are not rendered
});
});
Appreciate your help!
Using userEvent.selectOptions()
worked for me. Note that Headless UI uses mousedown instead of click to select options.
screen.getByRole("input").focus();
const option = (await screen.getByRole("option", { name: "Some option" }));
await userEvent.selectOptions(screen.getByRole("listbox"), selectedOption);
I’m also facing a similar issue to the above.
When I am testing with no anchor passed to the options container the test works fine. When I add the anchor back in, the test hangs indefinitely.
There are warnings in the logs for NaN being an invalid value for the ‘left’ css style property, originating from ‘InternalPortalFn2’
Using
userEvent.selectOptions()
worked for me. Note that Headless UI uses mousedown instead of click to select options.screen.getByRole("input").focus(); const option = (await screen.getByRole("option", { name: "Some option" })); await userEvent.selectOptions(screen.getByRole("listbox"), selectedOption);
Are you making use of the anchor property in the options container?
Thanks @JordanVincent Using userEvent works for me too only if I don't pass the anchor prop to ComboboxOptions. If I pass the anchor (as I need to) the test is failing.
Hmm, it works on my end with anchor
:
<ComboboxOptions
static={true}
anchor={{ to: "bottom start", gap: 8, padding: 8 }}
>...</ComboboxOptions>
Make sure the options are shown. You can inspect the DOM with screen.debug()
.
Ok, so my original solution was flaky. But I was able to fix it that way:
const requestAnimationFrameMock = jest.spyOn(window, "requestAnimationFrame").mockImplementation(setImmediate as any);
const cancelAnimationFrameMock = jest.spyOn(window, "cancelAnimationFrame").mockImplementation(clearImmediate as any);
screen.getByRole("input").focus();
await screen.getByRole("option", { name: "Some option" });
await userEvent.selectOptions(screen.getByRole("listbox"), selectedOption);
screen.getByRole("input").blur();
requestAnimationFrameMock.mockRestore();
cancelAnimationFrameMock.mockRestore();
When immediate
is set to true
, it refocuses the input. This reopens the dropdown but because it's using requestAnimationTimeframe
it's hard to get the timing right, leading to flakiness. Mocking requestAnimationTimeframe
fixes the timing issue and calling .blur()
closes the dropdown. Headless UI's does something similar in their tests.
Hey! Small update: this is a bug in Headless UI, and #3357 will solve the issue which means that the reproduction provided here #3294 (comment) will just work without any changes (apart from a version bump once the PR is ready)
@JordanVincent thanks for the message - I got it working with:
fireEvent.focus(screen.getByRole("combobox")); // focus the combobox input
screen.getByRole("option", { name: "Katelyn Rohan" }); // get an option from the list
Also, as you pointed out, the immediate
was also needed so I just passed it to the component:
<Combobox
multiple
value={selectedPeople}
onChange={setSelectedPeople}
onClose={() => setQuery("")}
immediate
>.....</Combobox>
Thanks very much for your help!
This should be fixed by #3357, and will be available in the next release.
You can already try it using:
npm install @headlessui/react@insiders
.
This should be fixed by #3357, and will be available in the next release.
You can already try it using:
npm install @headlessui/react@insiders
.
This now works for me! Thank you for the quick turnaround!