test("Escape closes the dialog and restores focus", async () => {
const user = userEvent.setup();
render(<DeleteButton />);
const trigger = screen.getByRole("button", { name: /delete/i });
await user.click(trigger);
expect(screen.getByRole("dialog")).toBeInTheDocument();
await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(trigger).toHaveFocus();
});
Open the dialog with user-event, press Escape, then assert that the dialog is gone and the trigger button has focus again.
The steps:
- Render the component.
- Open the dialog with
user.click. - Check it's visible with
getByRole('dialog'). - Press Escape with
await user.keyboard('{Escape}'). - Assert the dialog is gone with
queryByRole. - Assert the trigger button has focus with jest-dom's
toHaveFocus().
You can test focus order the same way. await user.tab() moves focus like the Tab key. Then you assert which element has focus now.
user-event moves focus and fires real key events. So these tests catch broken focus traps or missing key handlers that fireEvent would hide. A focus trap keeps Tab focus inside the dialog.
jsdom (the fake DOM used in tests) doesn't do layout, so it never calculates what the page looks like. Visual details like a focus ring belong in E2E or visual tests.
- Use
user.keyboard('{Escape}')to press special keys. - Use
user.tab()to move focus like the Tab key. - Use
toHaveFocus()to assert which element has focus. - Use
queryByRole('dialog')to assert the dialog closed.
Read more
- user-event intro (https://testing-library.com/docs/user-event/intro)testing-library.com