Build Component Driver
Component drivers encapsulate the logic for interacting with a UI component. While many drivers are provided out of the box, you can build your own for custom widgets.
Basic structureβ
A driver extends ComponentDriver
and implements any interfaces that describe its capabilities. The constructor
receives a locator, an interactor and optional configuration.
import { ComponentDriver, IClickableDriver } from '@atomic-testing/core';
export class SimpleButtonDriver extends ComponentDriver implements IClickableDriver {
click(): Promise<void> {
return this.interactor.click(this.locator);
}
get driverName(): string {
return 'SimpleButtonDriver';
}
}
Why use drivers?β
Drivers abstract away the DOM details of complex components. Instead of manipulating HTML in tests, you interact with high level methods (next()
, setValue()
, etc.). Drivers can expose child parts so you can compose them into larger units and reuse them across tests.
For example, the signup form in the example project is implemented with a CredentialFormDriver
that wraps four MUI text fields and a navigation component. Tests only call setValue()
and next()
on this driver, keeping the assertions focused on behavior rather than DOM markup.
By composing multiple drivers you can model entire workflows with minimal test code.
Example: composing a login form driverβ
The snippet below defines a LoginFormDriver
that combines two TextFieldDriver
s and a ButtonDriver
. It exposes a high level login()
helper so tests no longer deal with individual inputs.
import { TextFieldDriver, ButtonDriver } from '@atomic-testing/component-driver-mui-v5';
import {
ComponentDriver,
Interactor,
IComponentDriverOption,
IInputDriver,
PartLocator,
ScenePart,
byDataTestId,
} from '@atomic-testing/core';
const parts = {
username: { locator: byDataTestId('username'), driver: TextFieldDriver },
password: { locator: byDataTestId('password'), driver: TextFieldDriver },
submit: { locator: byDataTestId('submit'), driver: ButtonDriver },
} satisfies ScenePart;
export interface LoginCredential {
username: string;
password: string;
}
export class LoginFormDriver extends ComponentDriver<typeof parts> implements IInputDriver<LoginCredential> {
constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
super(locator, interactor, { ...option, parts });
}
async setValue(value: LoginCredential): Promise<boolean> {
await this.parts.username.setValue(value.username);
await this.parts.password.setValue(value.password);
return true;
}
async login(value: LoginCredential): Promise<void> {
await this.setValue(value);
await this.parts.submit.click();
}
get driverName(): string {
return 'LoginFormDriver';
}
}
By wrapping the lower level drivers, the login test becomes very small:
await testEngine.parts.form.login({ username: 'alice', password: 's3cr3t' });
Environment agnosticβ
Drivers rely on the Interactor
abstraction, so the same driver can run in unit tests with JSDOM or in browser tests using Playwright or Cypress. Tests remain identical across environments.
Encapsulating interactions in drivers keeps tests declarative. When the login flow changes, update the driver once and reuse it for both unit and endβtoβend scenarios.