Browser Automation
Espalier controls are built to be driven by semantic browser automation: locate elements through their accessible roles and names, deliver input through the browser's real input pipeline, and you never need shadow-root selectors, coordinates, or hand-dispatched events.
The verified contract is pinned Playwright driving real Chrome — fixtures in the Espalier repository enforce every row of the table below on every change. Other transports (WebDriver BiDi, other drivers) are expected to work when they provide equivalent semantics — computing ARIA from the DOM and sending trusted input — but they are not part of the enforced fixture matrix.
The Supported Contract
Locate controls by role and accessible name, exactly as assistive technology resolves them. When one of these controls sits inside an esp-form-item, the item's label becomes the control's accessible name automatically — including when the control's module loads lazily after the form item rendered: esp-input, esp-pick-one, esp-pick-some, esp-textarea, esp-date-picker, and esp-font-picker (which forwards the name to its inner combobox). Controls that carry their own visible label or naming API — esp-checkbox, esp-switch, esp-radio-button, esp-slider, and the checkbox/radio groups — keep their own naming contract; the form-item label does not replace it.
// Sign-in form: <esp-form-item label="Email address"><esp-input></esp-input></esp-form-item>
await page.getByRole("textbox", { name: "Email address" }).fill("reader@example.test");
// or equivalently
await page.getByLabel("Email address").fill("reader@example.test");
// <esp-button label="Sign in"></esp-button>
await page.getByRole("button", { name: "Sign in" }).click();
// <esp-form-item label="Favorite fruit"><esp-pick-one>…</esp-pick-one></esp-form-item>
await page.getByRole("combobox", { name: "Favorite fruit" }).click();
await page.getByRole("option", { name: "Cherry" }).click();
What each interaction produces, per control:
| Control | Locator | Interaction | Observable result |
|---|---|---|---|
esp-input |
getByRole("textbox", { name }) / getByLabel |
fill(value) |
Host value updates; exactly one esp-value-changed with the entered value |
esp-input |
same | pressSequentially(text) |
One esp-value-changed per keystroke, ending on the full value — identical to real typing |
esp-button |
getByRole("button", { name }) |
click() |
Exactly one esp-clicked event |
esp-button |
same | press("Enter") |
Exactly one esp-clicked event |
esp-button[disabled] |
same | any click or keyboard reach | Nothing — the control is inert and out of the tab order, but stays reachable by role and name |
esp-pick-one / esp-pick-some |
getByRole("combobox", { name }) |
click() or press("ArrowDown") |
Menu opens; aria-expanded reflects the state; the open menu is a named listbox whose aria-multiselectable reflects the mode |
esp-pick-one options |
getByRole("option", { name }) |
click(), or ArrowDown/Enter on the combobox |
Host value updates; exactly one esp-value-changed (detail: the picked item) per pick; the menu closes; the option shows aria-selected="true" on reopen |
esp-pick-some options |
getByRole("option", { name }) |
click() per option |
Host values accumulates; one esp-value-changed (detail: the selected-item array) per pick; the menu stays open across picks; picked options show aria-selected="true" |
What the Transport Must Do
The contract assumes interactions travel through the browser's input pipeline — the same path a person's keyboard and pointer take. Playwright's fill, type/pressSequentially, click, and press all qualify: they focus the control and deliver trusted events, which is what Espalier's value syncing and activation listen to.
Two shortcuts some automation layers take are not supported, because the browser itself gives them no observable side effects:
- Setting
.valuedirectly on the native input. The text changes visually, but noinputevent fires, so no value syncing can happen — in any web component, or in a plain<input>with aninputlistener. If your tool "types" this way, the hostvaluestaying empty is a defect in the tool, not the component. - Calling
.click()on theesp-buttonhost element. A programmatic host click does not activate the native button inside the shadow root. Use a real click (any coordinates-based or role-located click), orpress("Enter")/press(" ")on the focused control.
If a flow works with a manual pointer and keyboard but not with your automation tool, compare what the tool actually dispatches against this section before filing an Espalier issue — and file the issue against the tool when it falls into one of the two categories above.
Notes for Test Authors
- Assert on the public contract: host properties (
value,values), component events (esp-value-changed,esp-clicked), and ARIA state (aria-expanded,aria-selected). None of these require reaching into shadow roots. - Prefer
exact: truewhen asserting a label-derived name. Playwright's name matching is substring by default, and a text input'splaceholderparticipates in accessible-name fallback — a substring match can keep passing on the placeholder when the label plumbing is what you meant to test. - Picker options render inside a top-layer popover. They are ordinary role/name targets while the menu is open; there is no need to wait for animations beyond your tool's normal actionability checks.
- Espalier exposes roles both through
ElementInternals(for assistive technology) and as content attributes (for automation tooling that computes ARIA from the DOM). ThegetByRoleguarantee is the pinned Playwright/Chrome matrix the fixtures run; other transports that compute ARIA from the DOM are expected to resolve the same roles, but are unverified.