Controls & locators

Naming rules

Every control is a small class wrapping a Playwright locator, a controlType used in the narration, and a human name:

new Button(page, 'export', 'Export');
//         ^page  ^locator  ^name

The name is required — an unnamed control breaks the narration, so the constructor throws:

Control located by [data-testid="export"] needs a human-readable name:
an anonymous control breaks the narration.

controlType is fixed per class (button, field, select, radio group, ...) and shows up in every sentence and every StepEvent.controlType.

Control catalog

razo ships 20 controls, covering the interactive elements of a typical web app:

ControlPurpose
ButtonClickable button (click)
InputText field (fill, clear, expectEmpty; expectText reads the value)
TextAreaSame as Input, narrated as "text area"
RadioButtonRadio group container (select, expectSelected)
CheckboxCheckbox (check, uncheck, expectChecked, expectUnchecked)
SelectNative <select> (choose, chooseMany for multiple)
SliderRange input (setValue, expectValue)
FileInputFile input (attach, clear, expectFile)
LinkAnchor (open, expectHref)
LabelRead-only text (status messages, headings) — assertions only, no actions
SwitchARIA switch (turnOn, turnOff, expectOn, expectOff)
ComboboxARIA combobox (search, pick, expectSelected)
TableRead-only table (expectRowCount, expectRowContains)
DialogNative <dialog> or role="dialog" (close, expectOpen, expectClosed)
TabsARIA tablist (open, expectActive)
Image<img> (expectAlt, expectLoaded)
DatePickerinput[type="date"], ISO dates (pick, expectDate)
MenuDropdown menu (choose)
TooltipTrigger with aria-describedby (expectContent)
Editorcontenteditable region (write, clear)

Every control also inherits universal interactions — hover, doubleClick, rightClick, press(key), focus, scrollTo, dragTo(target) — and universal assertions — expectVisible, expectHidden, expectEnabled, expectDisabled, expectFocused, expectText, expectContainsText, expectAttribute, expectCount. Each has its own grammar sentence (Hover over button "Export", Drag file input "Model" onto button "Upload zone").

Locator strategies

A plain string means data-testid — the recommended, stable path (pair it with the auto-testid Vite plugin). razo also accepts every one of Playwright's user-facing locator strategies, so it works on apps that were never instrumented. The narration never changes with the strategy — only StepEvent.selector does:

Locator specResolves viaExample
'export'getByTestId('export')new Button(page, 'export', 'Export')
{ testId: 'export' }getByTestId('export')new Button(page, { testId: 'export' }, 'Export')
{ role, name, exact }getByRole(role, { name, exact })new Button(page, { role: 'button', name: 'Export', exact: true }, 'Export')
{ label }getByLabel(label)new Input(page, { label: 'Filename' }, 'Filename')
{ placeholder }getByPlaceholder(placeholder)new Input(page, { placeholder: 'my-app e2e' }, 'Project name')
{ text }getByText(text)new Button(page, { text: 'Upload to cloud' }, 'Upload to cloud')
{ altText }getByAltText(altText)new Image(page, { altText: 'Model preview' }, 'Model preview')
{ title }getByTitle(title)new Button(page, { title: 'Export options' }, 'Export options')
{ css }page.locator(css)new Label(page, { css: '#status' }, 'Export status')
{ locator }used as-isnew Button(page, { locator: page.getByTestId('export').first() }, 'Export')

{ locator } is the escape hatch — it's already bound to the page, so it ignores { within } scoping (see below).

within scoping

Controls can be scoped inside another control. The narration stays business-level (button "Yes, export") while the StepEvent.selector carries the full chain, so the artifact still knows exactly which element was hit:

const dialog = new Dialog(page, 'confirm-dialog', 'Confirm export');
const yes = new Button(page, 'confirm-yes', 'Yes, export', { within: dialog });

await yes.click();
// sentence:  Click button "Yes, export"
// selector:  [data-testid="confirm-dialog"] [data-testid="confirm-yes"]

Selector chaining also survives locator drift: if the dialog or the button drifts, the healed selector keeps the same parent child shape — see how deterministic healing works.

The auto-data-testid Vite plugin

The other half of a stable locator strategy: a component declares who it is, and the plugin generates the testid in dev/test.

<button data-component="SaveButton">Save</button>
<!-- becomes -->
<button data-component="SaveButton" data-testid="save-button">Save</button>

data-testid is the kebab-case of the component name; a manual data-testid always wins. See the README for the vite.config.ts setup and the toTestId() helper used from tests.