For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt, and this page is available as Markdown at https://wallarm.github.io/design-system/guides/testing.md.

Testing

Compound components such as Alert, Dialog or Card render several DOM elements. To give tests stable selectors for all of them, pass one data-testid to the root component: it cascades to every sub-component through React context.

How the cascade works

Each sub-component derives its test id from the root's: {base}--{slot}, with a double dash. The slot is the sub-component's name without the parent prefix, in kebab-case: AlertClose is close, DialogHeader is header, DialogFooterControls is footer-controls.

import { Button } from '@wallarm-org/design-system/Button';
import {
  Alert,
  AlertClose,
  AlertContent,
  AlertControls,
  AlertDescription,
  AlertIcon,
  AlertTitle,
} from '@wallarm-org/design-system/Alert';

export const LoginError = ({ onClose }: { onClose: () => void }) => (
  <Alert data-testid='login-error'>
    <AlertIcon />
    <AlertContent>
      <AlertTitle>Error</AlertTitle>
      <AlertDescription>Invalid credentials</AlertDescription>
      <AlertControls>
        <Button>Retry</Button>
      </AlertControls>
    </AlertContent>
    <AlertClose onClick={onClose} />
  </Alert>
);

renders:

<div data-testid="login-error" role="alert">
  <div data-testid="login-error--icon">…</div>
  <div data-testid="login-error--content">
    <span data-testid="login-error--title">Error</span>
    <span data-testid="login-error--description">Invalid credentials</span>
    <div data-testid="login-error--controls">…</div>
  </div>
  <button data-testid="login-error--close">…</button>
</div>

In Playwright:

import { expect, test } from '@playwright/test';

test('login error can be dismissed', async ({ page }) => {
  await expect(page.getByTestId('login-error')).toBeVisible();
  await expect(page.getByTestId('login-error--title')).toHaveText('Error');

  await page.getByTestId('login-error--controls').getByRole('button').click();
  await page.getByTestId('login-error--close').click();
});

Testing Library's screen.getByTestId('login-error--close') works the same way.

Without a data-testid on the root, no derived ids are rendered, so production HTML stays clean. Leaf components such as Button or Badge take data-testid like any other attribute.

Exceptions

ComponentWhere the test id comes from
ToastThe toast's id: useToast().create({ id: 'save-success', title: 'Saved' }) renders data-testid="save-success", and its parts derive from it.
Tourdata-testid on Tour. Tour has no DOM root of its own, so the id is applied to its popup parts through the cascade.

Your own compound components

Use the same helpers to make your components cascade, from @wallarm-org/design-system/utils/testId:

import type { ReactNode } from 'react';
import { type TestableProps, TestIdProvider, useTestId } from '@wallarm-org/design-system/utils/testId';

interface PanelProps extends TestableProps {
  children: ReactNode;
}

export const Panel = ({ 'data-testid': testId, children }: PanelProps) => (
  <section data-testid={testId}>
    <TestIdProvider value={testId}>{children}</TestIdProvider>
  </section>
);

export const PanelTitle = ({ children }: { children: ReactNode }) => {
  const testId = useTestId('title'); // 'settings--title' under <Panel data-testid='settings'>
  return <h2 data-testid={testId}>{children}</h2>;
};

useTestId(slot) returns {base}--{slot}, or undefined outside a provider or when the root has no test id.

data-testid and data-slot

Components also render data-slot attributes. They exist for CSS selectors and can change with the markup, so don't use them in tests. Both attributes coexist on the same elements.