Testing
Query by role and accessible name first; every component also exposes a testId prop for repeated or data-driven cases where that's ambiguous — plus a Playwright quickstart.
Every component in @42-central/ui-react is built on Ark UI, so interactive
elements already carry correct ARIA roles, accessible names, and keyboard behavior — the
strongest, most resilient thing an end-to-end test can assert, since it doubles as
accessibility coverage. Reach for that first:
await page.getByRole('button', { name: 'Save changes' }).click();
await page.getByRole('combobox', { name: 'Project' }).click();Role/name queries fall short in a few real cases though: a list of rows or menu items whose
content repeats or changes, a picker where the display label isn't a safe selector, or
anything you'd otherwise have to find by combining text content with DOM traversal
(.closest('tr')) or position (nth-child) — both break the moment copy or layout order
changes. For exactly those cases, every component exposes a testId prop that renders as
a stable data-testid — see the cheatsheet below for where it lands on each component shape.
The testId convention
testId always renders as data-testid on the component's real interactive DOM node — never
on a decorative wrapper. Where that lands depends on the component's shape:
| Component shape | Example | Where testId lands |
|---|---|---|
| Single-root component | Button, Badge, a compound part like Tabs.Trigger | That element itself |
| Shell-wrapped control | Input, NumberInput, Select's trigger | The real inner control (e.g. the <input>), not the styled wrapper <div> around it |
| Data-driven array item | Select/MultiSelect/Autocomplete's data, Menu's data, Tabs's data | An optional testId field on each item, defaulted from the item's own value |
DataTable rows/cells | — | Auto-derived from the resolved row id and column id — no config needed |
<Button testId="save-button">Save</Button>// Lands on the real <input>, not Input's styled wrapper div.
<Input label="Email" testId="email-input" />// Each option gets its own data-testid; omit it and one is derived from value.
<Select
label="Project"
data={[
{ value: 'libft', label: 'Libft' },
{ value: 'printf', label: 'ft_printf', testId: 'printf-option' },
]}
/>
// → data-testid="item-libft" and data-testid="printf-option"Compound components carry the same prop on every part that renders a DOM node — set it on
whichever part your test actually needs to find (Tabs.Trigger, Menu.Item,
Popover.Content, …).
Ark's own selectors — free, always on
Underneath every component, Ark UI's machines already stamp data-scope / data-part on
every rendered part, and data-value on selectable items (options, tabs, menu items) — with
zero code in this kit. These work as a selector today even without setting testId:
// Ark-machine-backed compounds (Select, Tabs, Menu, Combobox, …)
page.locator('[data-part="item"][data-value="printf"]');
page.locator('[data-part="trigger"][data-value="overview"]'); // a Tabs triggertestId exists for the cases this doesn't cover well: when value isn't a safe/stable
selector on its own, or for components (DataTable) that aren't Ark-machine-backed at all and
so don't get this for free.
DataTable: zero-config row/cell/header ids
DataTable derives a data-testid for every row, cell, and header automatically — from the
row's resolved id (getRowId, or react-table's own index-based fallback) and each column's
id. No configuration is required to get a usable, stable selector:
<DataTable columns={columns} data={users} />
// → data-testid="row-0", "row-0-cell-email", "header-email", …<DataTable columns={columns} data={users} getRowId={(u) => u.id} />
// → data-testid="row-<u.id>", "row-<u.id>-cell-email", …Override the row scheme with getRowTestId when you need a different naming convention —
every cell still derives from whatever it returns:
<DataTable
columns={columns}
data={users}
getRowId={(u) => u.id}
getRowTestId={(user, id) => `user-${user.email}-${id}`}
/>
// → data-testid="user-ada@lovelace.dev-1", "user-ada@lovelace.dev-1-cell-email", …Playwright quickstart
Nothing in this repo is Playwright-specific — testId is just a data-testid attribute, so
any tool that queries the DOM works. These snippets assume @playwright/test configured in
your own app, pointed at pages that render 42UI components.
Picking a Select option:
import { test, expect } from '@playwright/test';
test('picks a project from the Select', async ({ page }) => {
await page.goto('/settings');
await page.getByRole('combobox', { name: 'Project' }).click();
await page.getByTestId('printf-option').click();
await expect(page.getByRole('combobox', { name: 'Project' })).toHaveText('ft_printf');
});Switching a Tabs panel:
test('switches to the Activity tab', async ({ page }) => {
await page.goto('/dashboard');
await page.getByTestId('tab-activity').click();
await expect(page.getByTestId('tab-activity-panel')).toBeVisible();
});Clicking a DataTable row:
test('opens a user from the table', async ({ page }) => {
await page.goto('/users');
await page.getByTestId('row-1-cell-email').click();
await expect(page).toHaveURL(/\/users\/1$/);
});Internal test suite
This is also how @42-central/ui-react's own component tests are written — see
packages/react/TESTING.md
in the repo: role/accessible-name queries by default, getByTestId reserved for the same
repeated/data-driven/positional cases described above. Every component's own ## Accessibility
section (on its individual docs page) documents the exact roles and accessible names it exposes,
if you'd rather assert against those directly instead of testId.