Skip to main content

Core Concepts

Before using Atomic Testing, familiarize yourself with the following key concepts:

Architecture at a glance​

Every piece below fits into one pipeline: a declarative ScenePart is what the TestEngine is built from, each ComponentDriver it creates delegates to an Interactor, and the Interactor you get — ReactInteractor, VueInteractor, AngularInteractor, StorybookInteractor or PlaywrightInteractor — depends on which createTestEngine you called. Hover, focus, or press any node below to see what it does.

Which branch actually runs?

declarative / interface — not a runtime instance test environment concrete class that runs

Component Driver​

At the heart of Atomic Testing are component drivers. They define how to programmatically interact with UI components, such as clicking a button, selecting a value from a dropdown, or reading a row from a grid.

A growing number of component drivers are available for popular UI frameworks like Material UI. Each component driver offers a set of methods for interacting with the component. When using TypeScript for writing tests, auto-completion can help discover available methods.

info

Refer to the Component Driver APIs reference for a list of available component drivers.

Driver Types​

Every driver extends ComponentDriver<T>, and one specialization exists for a shape many UIs share — a repeated list of same-shaped items:

TypePurposeExample
ComponentDriver<T>Base driver with a fixed set of child partsMost drivers
ListComponentDriver<I>Driver for a repeated collection of same-shaped itemsMenu, List

A component whose interior varies per usage needs no special driver type. Every driver inherits within(parts) (packages/core/src/drivers/ComponentDriver.ts), which resolves a caller-supplied ScenePart against that driver's interior and returns one driver per named part. The anchor is the driver's interiorLocator, which defaults to its own locator; overlay drivers whose locator is a wrapper (MUI's Dialog and Drawer anchor at the Modal root, alongside the backdrop) override it to the surface holding your content, so the same scene line means the same thing on every design system. Where parts is the chrome the driver author hardcodes, within() is the interior the scene author owns: a dialog's title and close behavior are the same everywhere, but its body differs per usage. Because locators resolve lazily, within() is synchronous and can be called before the interior mounts — and one driver instance can serve any number of interiors.

DialogDriver in @atomic-testing/component-driver-mui-v7 (packages/component-driver-mui-v7/src/components/DialogDriver.ts) is a shipped example — it re-roots to the dialog's portal target (see Testing portals & overlays) and exposes driver-specific methods like getTitle() and closeByEscape(), while the caller reaches the dialog body through within():

const confirmContent = {
cancel: { locator: byDataTestId('cancel'), driver: HTMLButtonDriver },
confirm: { locator: byDataTestId('confirm'), driver: HTMLButtonDriver },
} satisfies ScenePart;

const parts = { dialog: { locator: byDataTestId('dialog'), driver: DialogDriver } } satisfies ScenePart;

await engine.parts.dialog.within(confirmContent).confirm.click();
note

Earlier versions modeled this with a ContainerDriver base and a content driver option, which required naming the interior scene twice. Both were removed in favor of within() — see ADR-019.

ListComponentDriver<ItemT> (packages/core/src/drivers/ListComponentDriver.ts) extends ComponentDriver and adds getItemByIndex(), getItemByLabel(), getItems(), and getItemCount() — all resolved by matching a single itemLocator repeatedly and wrapping each match in an itemClass driver. Reach for it whenever a component renders a variable-length collection of identically-shaped children. ListDriver in @atomic-testing/component-driver-mui-v7 (packages/component-driver-mui-v7/src/components/ListDriver.ts) is a shipped example driving a <List> of <ListItem>s.

Locator​

Locators help find components on a page, using various locator strategies such as byDataTestId and byRole.

Available locators
LocatorDescription
byDataTestId(dataTestId)Locate by data-testid attribute.
byRole(role)Locate by the value of role attribute.
byAriaLabel(value)Locate by the verbatim aria-label attribute — often composed with byRole to tell same-role siblings apart.
findByRole(role, name)Locate by role plus the COMPUTED accessible name (aria-labelledby, an associated <label>, or visible text) — use this instead of byAriaLabel when the name isn't a literal aria-label attribute.
byCssSelector(selector)Locate by CSS selector.
byCssClass(className)Locate by CSS class name. Locate by CSS class is not recommended because it is not reliable and can be changed easily.
byAttribute(name, value)Locate element by attribute
byTagName(tagName)Locate by HTML tag name (not recommended).
byValue(value)Locate by the value of value attribute
byInputType(type)Locate by input element by its type, such as text, radio, checkbox etc.
byName(name)Locate by the value of name attribute.
byLinkedElement()(Experimental) Locate an element by matching attributes from another element
byChecked(checked)Locate checkbox which is checked, usually it can be chained with checkbox locator
tip

The use of the data-testid attribute is recommended for locating components on a page. Refer to Best Practices for more details. Use the byDataTestId(value) API as the recommended approach for building locators.

By default, a child part's locator is resolved as a descendant of its parent driver's element. Dialogs, menus, and other overlays commonly break that assumption by rendering outside their trigger's subtree — see Testing portals & overlays for the recipe component drivers use to locate that content anyway.

ScenePart​

A ScenePart is a map describing all components of interest (part) within a scene (a page or a rich UI component). Each entry in a ScenePart outlines the part name, the component locator, and the component driver.

A sample ScenePart of a typical login screen
import { HTMLAnchorDriver, HTMLElementDriver } from '@atomic-testing/component-driver-html';
import { ButtonDriver, TextFieldDriver } from '@atomic-testing/component-driver-mui-v6';
import { byDataTestId, ScenePart } from '@atomic-testing/core';

const loginScenePart = {
username: {
locator: byDataTestId('username'),
driver: TextFieldDriver,
},
password: {
locator: byDataTestId('password'),
driver: TextFieldDriver,
},
error: {
locator: byDataTestId('error-display'),
driver: HTMLElementDriver,
},
submit: {
locator: byDataTestId('submit'),
driver: ButtonDriver,
},
forgetPassword: {
locator: byDataTestId('forget-password'),
driver: HTMLAnchorDriver,
},
} satisfies ScenePart;

Test Engine​

The Test Engine is where all the pieces come together. It is responsible for rendering a scene, locating all the components in the scene, and providing a set of methods to interact with the components.

Use createTestEngine to create a Test Engine instance. The createTestEngine function is specific to each rendering framework, such as React, Vue, and Playwright.

The examples below demonstrate how to create a Test Engine for the loginScenePart described earlier.

import { createTestEngine } from '@atomic-testing/react-18';

import { Login } from './components/Login';
import { loginScenePart } from './loginScenePart';

const testEngine = createTestEngine(<Login />, loginScenePart);

Once the test engine is created, it can be used to interact with the components in the scene.

// Test code is agnostic to the rendering framework

await testEngine.parts.username.setValue('john@example.com');
await testEngine.parts.password.setValue('');
await testEngine.parts.submit.click();

const error = await testEngine.parts.error.getText();
expect(error).toEqual('Password is required'); // Jest assertion, but any assertion library can be used

Going deeper​

Once these core concepts feel familiar, the "🔧 Advanced" section covers the next layer of detail:

  • Architecture — how the TestEngine, ComponentDriver, Interactor, and PartLocator layers fit together.
  • The Interactor — the environment-adapter layer underneath every component driver, and how it differs per framework.
  • Atomic Testing vs. React Testing Library — how the component-driver pattern compares to RTL's query-based approach.