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