Skip to Content
Test Environments

Test Environments

Axiom should work out of the box with most testing libraries. But certain test environments may require custom configuration to work.

#

Global provider

#

Wrap tests in AxiomProvider

Always make sure to wrap your elements with AxiomProvider during tests.

For example if you’re using React Testing Library  then you should export a custom render function that sets AxiomProvider as the default wrapper.

import { AxiomProvider } from "@optiaxiom/react"; import { render } from "@testing-library/react"; const customRender = (...args: Parameters<typeof render>) => ({ ...render(args[0], { wrapper: AxiomProvider, ...args[1], }), }); export * from "@testing-library/react"; export { customRender as render };

#

Disable animations

Running animations during unit tests can cause unpredictable test results or force developers to write more code waiting for animations to finish. Instead a better strategy is to disable animations during unit tests to ensure consistent test results.

Enable the TransitionGlobalConfig.skipAnimations option to disable animations.

import { TransitionGlobalConfig } from "@optiaxiom/react"; // Do this in your global test setup file TransitionGlobalConfig.skipAnimations = true;

#

Cleanup toasts and dialogs

Toasts and managed dialogs are persisted globally which can cause tests to affect one another.

Use the cleanup method to clear out toasts and dialogs in your test setup/teardown hooks.

import { cleanup } from "@optiaxiom/react/testing"; beforeEach(() => { cleanup(); });

#

Locating components

#

Query by role and name

Find components by ARIA role and accessible name, in Playwright (page.getByRole) and React Testing Library (screen.getByRole) alike. Roles and names are the stable contract. Class names are generated per build, and data-radix-* attributes and wrapper elements belong to internal dependencies.

Popovers, menus, dialogs, toasts and tooltips render in a portal at the end of body, so query them from the page, not from inside the element that opened them.

#

Accessible names hold more than the label

A field’s accessible name can include text beyond its label: Select adds the selected value ("Language French") and a required field adds a visually hidden "Required". Match the start of the name instead of the whole of it.

// Playwright matches a substring by default; avoid `exact: true` here. page.getByRole("combobox", { name: "Language" }); // React Testing Library matches the whole name by default. screen.getByRole("combobox", { name: /^Language/ });

#

Select and Menu

Select renders a combobox trigger. Menu renders a button trigger named by its content. Both open a listbox whose items are role option, not menuitem.

await page.getByRole("combobox", { name: "Language" }).click(); await page.getByRole("option", { name: "French" }).click(); await page.getByRole("button", { name: "Actions" }).click(); await page.getByRole("option", { name: "Duplicate" }).click();

#

Checkbox, Radio and Switch

Use the input’s role with check(), uncheck() or click(): checkbox, radio or switch, named by its label.

await page.getByRole("switch", { name: "Notifications" }).check();

#

Tables

Table and DataTable header cells are role columnheader, and a sortable header carries aria-sort and holds a button that toggles it. Playwright before 1.57 reports header cells as cell, so upgrade to query them by columnheader.

const header = page.getByRole("columnheader", { name: "Volume" }); await header.getByRole("button").click(); await expect(header).toHaveAttribute("aria-sort", "ascending");

#

Toasts

Toasts render inside a region whose name starts with "Notifications". Scope to it: a new toast’s text is also repeated in a hidden live region for screen readers, so a page-wide text query can match twice.

const notifications = page.getByRole("region", { name: /notifications/i }); await expect(notifications.getByText("Changes saved")).toBeVisible();

#

Tooltips

Hover or focus the trigger, then query the tooltip role.

await page.getByRole("button", { name: "Add" }).hover(); await expect(page.getByRole("tooltip")).toHaveText("Add to library");

#

jsdom

#

Stub missing DOM APIs

jsdom (Jest’s default environment) leaves out some DOM APIs. Axiom components work without them, but a few dependencies call them unguarded:

  • ResizeObserver: Tooltip positions its arrow with a Radix hook that requires it.
  • Element.prototype.hasPointerCapture: Toast calls it on pointer up.
  • document.elementFromPoint: RichTextEditor calls it when clicked.

Stub them in your global test setup file. Don’t mock Axiom components to work around them: query them by role and drive them with user-event instead.

globalThis.ResizeObserver ??= class { disconnect() {} observe() {} unobserve() {} }; Element.prototype.hasPointerCapture ??= () => false; Document.prototype.elementFromPoint ??= () => null;

#

Don’t answer every matchMedia query with false

jsdom doesn’t implement window.matchMedia, and Axiom doesn’t need it: without it, responsive components render their desktop layout.

A common stub answers every query with matches: false. Axiom reads that as a screen narrower than the sm breakpoint, so components render their mobile layout instead. Menu opens as a dialog, and its sub-menus don’t open on hover, so tests written against the desktop menu fail.

Remove the stub if nothing else in your tests needs it. Otherwise answer width queries against the window width, which jsdom sets to 1024px:

const matchesWidth = (query: string) => { const bounds = [ ...query.matchAll(/\((min|max)-width:\s*([\d.]+)(px|em|rem)\)/g), ]; return ( bounds.length > 0 && bounds.every(([, bound, value, unit]) => { const px = Number(value) * (unit === "px" ? 1 : 16); return bound === "min" ? window.innerWidth >= px : window.innerWidth <= px; }) ); }; window.matchMedia = (query) => ({ addEventListener: () => {}, addListener: () => {}, dispatchEvent: () => false, matches: matchesWidth(query), media: query, onchange: null, removeEventListener: () => {}, removeListener: () => {}, });

#

“Could not parse CSS stylesheet”

jsdom 25 and older can’t parse CSS cascade layers (@layer) or @property, which Axiom’s styles use. When those styles reach the test document, jsdom logs Error: Could not parse CSS stylesheet with a stack trace for each stylesheet. Tests still pass: jsdom skips the stylesheet, and it doesn’t compute layout anyway.

Jest 29 bundles jsdom 20. Upgrade to Jest 30 (jsdom 26), or jsdom 26 or later in Vitest, to stop the errors.

#

Jest

#

Mock CSS imports

Jest does not support CSS imports by default and needs to be manually configured to handle them.

Install the jest-transform-stub package.

npm install -D jest-transform-stub

Stub CSS files by transforming them into empty modules in your Jest config file.

jest.config.js
module.exports = { moduleNameMapper: { "\\.css$": "jest-transform-stub", }, };

#

Vitest

#

Process CSS imports

Vitest does not process CSS imports from within dependencies by default. So we have to manually configure it to bundle the Axiom dependencies:

vitest.config.ts
import { defineConfig } from "vitest/config"; export default defineConfig({ test: { server: { deps: { inline: ["@optiaxiom"], }, }, }, });

#

Cannot find ‘react/jsx-runtime’

If you’re using React 16 or 17 then running tests using Vitest will cause errors like this:

Error: Cannot find module 'react/jsx-runtime' imported from /node_modules/@optiaxiom/react/... Did you mean to import "react/jsx-runtime.js"?

In order to fix this we have to manually configure it to bundle the Axiom dependencies:

vitest.config.ts
import { defineConfig } from "vitest/config"; export default defineConfig({ test: { server: { deps: { inline: ["@optiaxiom", "@radix"], }, }, }, });
Last updated on