|Kit

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 shapeExampleWhere testId lands
Single-root componentButton, Badge, a compound part like Tabs.TriggerThat element itself
Shell-wrapped controlInput, NumberInput, Select's triggerThe real inner control (e.g. the <input>), not the styled wrapper <div> around it
Data-driven array itemSelect/MultiSelect/Autocomplete's data, Menu's data, Tabs's dataAn 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 trigger

testId 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.

On this page