@ariakit/test
v0.8.0
Published
Ariakit test utils
Downloads
141,464
Readme
Ariakit Test
Important: This package is experimental and does not follow semantic versioning, meaning breaking changes may occur in patch and minor versions.
Utilities for simulating user interactions in Ariakit's unit and end-to-end tests.
Contents
Installation
npm i @ariakit/testUsage
Import helpers from the package root to simulate user interactions:
import { click, press, type } from "@ariakit/test";The @ariakit/test/react entry point renders React components for testing, and the @ariakit/test/playwright entry point provides query helpers for Playwright tests.
Test environment
The helpers expect a DOM that implements PointerEvent, which means happy-dom, jsdom v27 or later, or a real browser.
They still run on jsdom 26, which jest-environment-jsdom 30 depends on. Every event the helpers fire there carries the same mouse, modifier, and pointer members it carries anywhere else, so a listener reads the same clientX, button, pointerId, and modifier state it would in a browser. click, auxclick, and contextmenu are built from MouseEvent there rather than PointerEvent, which also keeps the members a browser computes.
The pointer* events are the ones that environment cannot build from an interface of their own, so they come from Event and are not instanceof MouseEvent. The dispatch layer still derives pageX, pageY, and which, but leaves the layout-dependent offsetX and offsetY undefined. The PointerEvent global is absent altogether, so both new PointerEvent() and event instanceof PointerEvent throw a ReferenceError.
API reference
blur
function blur(element?: DirtiableElement | null): Promise<void>;Removes focus from an element, simulating a real user moving focus away from it. When no element is passed, the currently focused element (document.activeElement) is used. If typing changed the element's value since it gained focus, a change event is dispatched before it's blurred.
Example:
await type("hello", q.textbox());
// Dispatches the pending `change` event, then blurs the textbox.
await blur();click
function click(
element: Element | null,
options?: PointerEventInit,
tap = false,
): Promise<void>;Clicks on an element, simulating the sequence of events a real mouse click produces — hovering the target, then pointerdown, mousedown, focus, pointerup, mouseup, and click.
Hidden and disabled elements are handled the same way a browser would, and clicks on labels, option elements, and form controls behave like native interactions. Pass options to set event properties such as modifier keys (e.g. { shiftKey: true }).
Pass button to click with another mouse button. Activation behavior runs on click, so a non-primary button fires auxclick instead and doesn't activate labels or option elements, and the secondary button also fires contextmenu while it's held down. Each step derives buttons from that button, so an explicit buttons is ignored here; mouseDown and mouseUp accept one to describe a chorded gesture.
Example:
await click(q.button("Submit"));
// With a modifier key held down:
await click(q.option("Item"), { shiftKey: true });
// With the middle mouse button, firing `auxclick`:
await click(q.link("Ariakit"), { button: 1 });dispatch
type Target = Document | Window | Node | Element | null;
type EventFunction = (element: Target, options?: object) => Promise<boolean>;
type DispatchEventType = Exclude<EventType, "doubleClick"> | "auxClick";
type EventsObject = {
[K in DispatchEventType]: EventFunction;
};
const dispatch: typeof baseDispatch & EventsObject;Creates and fires a DOM event on an element, then waits for the resulting microtasks to flush. Call dispatch.<eventName>(element, options) to build and fire a specific event (e.g. dispatch.keyDown, dispatch.click, dispatch.input), or call dispatch(element, event) directly with an Event instance.
Unlike higher-level helpers such as click and type, this fires a single event without simulating the surrounding interaction sequence. Pointer and mouse events fired on an element with pointer-events: none are re-dispatched on the nearest ancestor that has pointer events enabled, matching how browsers route those events.
A pointer event built by name reports the contact size and transducer angle browsers report for a device with neither, so width and height are 1 and altitudeAngle is a right angle. Supplying only the tilt or spherical angle pair derives the other pair. The members describing a gesture, such as pressure and isPrimary, keep their defaults here; the higher-level helpers fill those in. An event you construct yourself keeps whatever its constructor gave it.
Mouse and pointer events built by name derive pageX and pageY from the client coordinates and target window scroll, and derive which from button. The layout-dependent offsetX and offsetY keep the environment's values.
click, auxclick, and contextmenu are built as PointerEvent, the way browsers dispatch them, so they accept and report pointer properties such as pointerType. An environment with no PointerEvent builds them as MouseEvent instead, and they report the same properties there.
Returns: A promise that resolves to false when the event's default action was prevented with event.preventDefault(), and true otherwise.
Example:
await dispatch.keyDown(q.textbox(), { key: "Enter" });
await dispatch.click(q.button());
await dispatch.auxClick(q.link("Ariakit"), { button: 1 });
// Fire a custom event instance directly:
await dispatch(q.textbox(), new Event("selectstart", { bubbles: true }));focus
function focus(element: Element | null): Promise<void>;Moves focus to an element, simulating a real user focusing it. Elements that aren't focusable are ignored, and focusing the already focused element is a no-op. If typing changed another element's value since it gained focus, its pending change event is dispatched before focus moves.
Example:
await focus(q.textbox());
expect(q.textbox()).toHaveFocus();hover
function hover(
element: Element | null,
options?: PointerEventInit,
): Promise<void>;Moves the pointer over an element, simulating a real user hovering it. Fires the relevant pointer/mouse enter, over, and move events, and dispatches the matching leave events on the previously hovered element.
Hidden elements and elements with pointer-events: none are handled the way a browser would. Pass options to set event properties such as modifier keys. The pointer events report pressure: 0, or 0.5 when you pass buttons to describe a move with a button held down, as during a drag.
Example:
await hover(q.button("More options"));
expect(q.menu()).toBeVisible();mouseDown
function mouseDown(
element: Element | null,
options?: PointerEventInit,
): Promise<void>;Presses a pointer button down on an element, firing pointerdown and mousedown and moving focus the way a browser would. Disabled elements still receive the pointer event but not mousedown, and focus falls back to the closest focusable ancestor when the target itself isn't focusable.
This is one step of a full click; use it directly to test press-and-hold behavior. Pass options to set event properties such as modifier keys, or button to press another mouse button. The events report the pressed button in buttons, like a browser does, unless you pass buttons yourself to describe a chorded gesture. When that value shows another button was already held down, the press fires pointermove instead of pointerdown, the way Pointer Events routes a chorded press, and the compatibility mousedown still fires. The pointer event reports pressure: 0.5, the value Pointer Events defines while a device with no pressure sensor holds a button down.
Example:
await mouseDown(q.button("Resize"));
// ...assert the press state, then release:
await mouseUp(q.button("Resize"));mouseUp
function mouseUp(
element: Element | null,
options?: PointerEventInit,
): Promise<void>;Releases a pointer button on an element, firing pointerup and mouseup. Disabled elements still receive the pointer event but not mouseup.
This is the counterpart to mouseDown and one step of a full click. Pass options to set event properties such as modifier keys, or button to release another mouse button. The events report no button still held down in buttons, like a browser does, unless you pass buttons yourself to describe the buttons a chorded gesture keeps held. When that value shows another button stays held down, the release fires pointermove instead of pointerup, the way Pointer Events routes a chorded release, and the compatibility mouseup still fires. The pointer event reports pressure: 0, or 0.5 while a chorded gesture keeps a button held.
Example:
await mouseDown(q.button("Resize"));
await mouseUp(q.button("Resize"));press
function press(
key: string,
element?: Element | null,
options: KeyboardEventInit = {},
): Promise<void>;Presses a key on an element, simulating a real user keyboard interaction. Fires keydown and keyup and applies the browser's default behavior for that key — moving focus with Tab, activating buttons and submitting forms with Enter, clicking buttons, checkboxes, and radios with Space, moving the caret with the arrow and Home/End keys, and typing printable characters into text fields.
When no element is passed, the currently focused element is used. Shortcuts such as press.Enter() and press.Tab() are provided for common keys, and press.ShiftTab() moves focus backwards.
Use press.down and press.up to fire only the keydown or keyup half of a press. Each defaults to the currently focused element, so a key released after focus moved away — for example, an element that disables itself on keydown — lands where a real browser would deliver it.
Example:
await press.Tab();
await press.Enter();
// `press.Enter(element)` is shorthand for `press("Enter", element)`:
await press.Enter(q.button("Submit"));
// Split a press so the keyup lands on whatever is focused at release time:
await press.down.Space();
await press.up.Space();query
type Query = ReturnType<typeof createRoleQuery>;
type TextQuery = ReturnType<typeof createTextQuery>;
type RoleQueries = Record<AriaRole, Query>;
interface QueryObject extends RoleQueries {
text: TextQuery;
labeled: TextQuery;
within: (element?: HTMLElement | null) => QueryObject;
}
const query: QueryObject;Queries the DOM by ARIA role, accessible name, text, or label, built on top of Testing Library. Call a role method such as query.button(name) or query.dialog() to get the matching element, passing a string or RegExp to match its accessible name. Queries throw when no matching element is found. Use query.text() and query.labeled() to query by text content or associated label, and query.within(element) to scope queries to a subtree.
Every query also exposes .lazy (return a reusable function that runs the query when called), .all (return all matches, including an empty array), .wait (resolve once the element appears), and .maybe (return null when it's missing) variants. Role queries additionally expose .hidden to include otherwise-hidden elements.
Example:
const dialog = query.dialog.maybe.lazy("Settings");
expect(dialog()).not.toBeInTheDocument();
await click(query.button("Open settings"));
expect(dialog()).toBeVisible();
// Wait for an element to appear, or scope a query to a subtree:
await query.alert.wait();
query.within(dialog()).button("Close");q
type Query = ReturnType<typeof createRoleQuery>;
type TextQuery = ReturnType<typeof createTextQuery>;
type RoleQueries = Record<AriaRole, Query>;
interface QueryObject extends RoleQueries {
text: TextQuery;
labeled: TextQuery;
within: (element?: HTMLElement | null) => QueryObject;
}
const q: QueryObject;Short alias for query. Queries the DOM by ARIA role, accessible name, text, or label.
Example:
const dialog = q.dialog.maybe.lazy("Settings");
expect(dialog()).not.toBeInTheDocument();
await click(q.button("Open settings"));
expect(dialog()).toBeVisible();rightClick
function rightClick(
element: Element | null,
options?: PointerEventInit,
): Promise<void>;Right-clicks on an element, simulating the sequence of events a real secondary mouse click produces — hovering the target, then right-button pointerdown, mousedown, focus, contextmenu, pointerup, mouseup, and auxclick.
Hidden elements are handled the same way a browser would, and no synthetic click event is fired. Pass options to set event properties such as modifier keys.
Example:
await rightClick(q.text("Open menu"));select
function select(
text: string,
element: Element | null = document.body,
options?: PointerEventInit,
): Promise<void>;Selects a range of text within an element, simulating a real user dragging across it. Hovers and presses on the element, finds the given text in its descendant text nodes, sets the document selection to cover it, then releases.
When no element is passed, document.body is used. Pass options to set event properties such as modifier keys. Each step derives buttons from the button it presses, so an explicit buttons is ignored.
Example:
await select("hello world");
expect(document.getSelection()?.toString()).toBe("hello world");sleep
function sleep(ms = defaultMs): Promise<void>;Waits for the DOM to settle between simulated interactions by yielding across two animation frames and a short timeout.
The other helpers in this package call it internally, but you can await it directly to let pending updates, transitions, or effects flush before asserting. The default delay is small and environment-dependent; pass ms to override it. Outside a real browser it also drains the host scheduler so concurrent React work that the delay raced past settles before the call resolves.
Example:
await click(q.button("Open"));
await sleep();
expect(q.dialog()).toBeVisible();tap
function tap(
element: Element | null,
options?: PointerEventInit,
): Promise<void>;Clicks on an element without the brief delay that click waits between pressing and releasing, reproducing the timing of a quick tap. It fires the same pointer, mouse, and click events as click. Pass options to set event properties such as modifier keys.
Example:
await tap(q.button("Submit"));type
function type(
text: string,
element?: (DirtiableElement & HTMLElement) | null,
options: InputEventInit | KeyboardEventInit = {},
): Promise<void>;Types text into an element, simulating a real user pressing each key. Focuses the element, then for each character fires keydown, updates the value and caret position of text fields through an input event (preceded by keypress when inserting a printable character), and fires keyup.
Special characters map to their keys: "\b" is Backspace, "\x7f" is Delete, "\n" is Enter, and "\t" is Tab. When no element is passed, the currently focused element is used. Pass options to set event properties such as modifier keys or composition state.
Example:
await type("Hello", q.textbox());
// Delete the last character with a Backspace:
await type("\b");waitFor
function waitFor<T>(
callback: () => T,
options?: DOMTestingLibrary.waitForOptions,
): Promise<T>;Re-runs a callback until it stops throwing or the timeout is reached, re-exporting Testing Library's waitFor with this package's async batching applied. Use it to wait for an assertion to pass after an asynchronous update. Pass options to configure the timeout, interval, and other behavior.
Example:
await click(q.button("Close"));
await waitFor(() => expect(q.dialog.maybe()).not.toBeInTheDocument());React API reference
RenderOptions
interface RenderOptions extends Omit<
ReactTestingLibrary.RenderOptions,
"queries"
> {
strictMode?: boolean;
}Options for the render function. Accepts every option from Testing Library's render (except queries), plus strictMode to wrap the rendered UI in React's StrictMode.
Example:
const options: RenderOptions = { strictMode: true };
await render(<App />, options);render
function render(
ui: ReactNode,
options?: RenderOptions,
): Promise<{
unmount: () => void;
rerender: (newUi: ReactNode) => Promise<void>;
}>;Renders a React element into the document for testing, waiting for effects and the next frame to flush before resolving.
Built on Testing Library's render, it returns unmount to remove the tree and an async rerender to update it with new UI. Pass strictMode: true to wrap the element in React's StrictMode, or any other Testing Library render option.
Example:
const { rerender, unmount } = await render(<Button>Submit</Button>);
await click(q.button("Submit"));
await rerender(<Button>Sent</Button>);
unmount();Playwright API reference
query
type RoleQuery = (
name?: string | RegExp,
options?: Parameters<Page["getByRole"]>[1],
) => Locator;
type TextQuery = (
name: Parameters<Page["getByText"]>[0],
options?: Parameters<Page["getByText"]>[1],
) => Locator;
type RoleQueries = Record<AriaRole, RoleQuery>;
type Queries = RoleQueries & { text: TextQuery };
function query(locator: Page | Locator | FrameLocator): Queries;Creates role- and text-based query helpers for a Playwright Page, Locator, or FrameLocator. Call a role method such as query(page).button(name) to get a Locator from getByRole, or query(page).text(name) to match by text content.
Names are matched exactly by default. This mirrors the role-based query from the package root for end-to-end Playwright tests.
Example:
const { button, dialog } = query(page);
await button("Open").click();
await expect(dialog()).toBeVisible();Core Team
Contributing
Follow the instructions on the contributing guide.
