Skip to main content

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.

SimpleButtonDriver.ts
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 TextFieldDrivers 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.

Pro tip

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.