Interactor
An Interactor provides the low level operations used by component drivers to manipulate and query the UI. Drivers delegate every action such as clicking, entering text or reading an attribute to an interactor. By swapping the interactor implementation, the same driver code works in different environments like unit tests running in JSDOM or endβtoβend tests with Playwright.
Available interactorsβ
The project ships with several interactors:
- DOMInteractor β runs against a DOM environment using
@testing-libraryutilities. This is used for unit/integration tests. - ReactInteractor from @atomic-testing/react-core and the versioned
React adapters β extensions of
DOMInteractorthat wrap interactions in React'sact()helper so state updates are flushed correctly when testing React 18 or later. - VueInteractor from @atomic-testing/vue-3 β an extension of
DOMInteractorthat calls Vue'snextTick()after every interaction so reactive state settles before the next assertion. - AngularInteractor from @atomic-testing/angular-core (used by the
versioned
@atomic-testing/angular-20,-21and-22adapters) β an extension ofDOMInteractorthat awaits the app'sApplicationRef.whenStable()after every interaction, so change detection has settled before the next assertion. This works under both zone.js and zoneless change detection. - StorybookInteractor from @atomic-testing/storybook β an extension of
DOMInteractorfor driving component drivers inside a real-browser Storybook:playfunctions and stories running under@storybook/addon-vitest. Unlike the React/Vue/Angular interactors, it has no frameworkact()/nextTick()to hook into, so it settles after every interaction with a macrotask plus two animation frames instead, and dispatches through Storybook's instrumenteduserEventso interactions show up in the Interactions panel. - PlaywrightInteractor β drives a Playwright
Pageobject to execute tests in a real browser.
Building an interactorβ
To build your own interactor, implement the Interactor interface from the core
package. Most custom interactors extend an existing one and override only the
behaviour that differs. The snippet below logs every click before delegating to
DOMInteractor:
import { ClickOption, PartLocator } from '@atomic-testing/core';
import { DOMInteractor } from '@atomic-testing/dom-core';
export class LoggingInteractor extends DOMInteractor {
async click(locator: PartLocator, option?: Partial<ClickOption>): Promise<void> {
console.log('clicking', await this.innerHTML(locator));
await super.click(locator, option);
}
}
None of the framework createTestEngine factories β @atomic-testing/react-18/
react-19, @atomic-testing/vue-3, @atomic-testing/angular-20/-21/-22,
@atomic-testing/playwright, or @atomic-testing/dom-core β accept a custom
interactor; each one constructs its own hardcoded Interactor subclass
internally (see e.g. packages/dom-core/src/createTestEngine.ts). To use a
custom interactor, construct TestEngine (packages/core/src/TestEngine.ts)
directly instead of calling createTestEngine. For a DOM-only interactor, that
means reproducing what @atomic-testing/dom-core's createTestEngine does
internally, with your interactor swapped in for DOMInteractor:
import { TestEngine } from '@atomic-testing/core';
import { LoggingInteractor } from './LoggingInteractor';
// Same shape as @atomic-testing/dom-core's own createTestEngine, with
// LoggingInteractor swapped in for DOMInteractor. `element` is whatever root
// node your component was already rendered into.
const testEngine = new TestEngine([], new LoggingInteractor(element), { parts: partDefinitions });
The cleanup callback (TestEngine's 4th constructor argument) is optional and
defaults to a no-op if omitted. If your custom interactor needs React's
act() or Vue's nextTick() wrapping too, extend ReactInteractor /
VueInteractor instead of DOMInteractor, and mirror the locator/mount/cleanup
logic from that framework's own createTestEngine
(packages/react-core/src/createTestEngine.ts,
packages/vue-3/src/createTestEngine.ts).