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 ^nameThe 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:
| Control | Purpose |
|---|---|
Button | Clickable button (click) |
Input | Text field (fill, clear, expectEmpty; expectText reads the value) |
TextArea | Same as Input, narrated as "text area" |
RadioButton | Radio group container (select, expectSelected) |
Checkbox | Checkbox (check, uncheck, expectChecked, expectUnchecked) |
Select | Native <select> (choose, chooseMany for multiple) |
Slider | Range input (setValue, expectValue) |
FileInput | File input (attach, clear, expectFile) |
Link | Anchor (open, expectHref) |
Label | Read-only text (status messages, headings) — assertions only, no actions |
Switch | ARIA switch (turnOn, turnOff, expectOn, expectOff) |
Combobox | ARIA combobox (search, pick, expectSelected) |
Table | Read-only table (expectRowCount, expectRowContains) |
Dialog | Native <dialog> or role="dialog" (close, expectOpen, expectClosed) |
Tabs | ARIA tablist (open, expectActive) |
Image | <img> (expectAlt, expectLoaded) |
DatePicker | input[type="date"], ISO dates (pick, expectDate) |
Menu | Dropdown menu (choose) |
Tooltip | Trigger with aria-describedby (expectContent) |
Editor | contenteditable 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 spec | Resolves via | Example |
|---|---|---|
'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-is | new 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.