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 .value directly on the native input. The text changes visually, but no input event fires, so no value syncing can happen — in any web component, or in a plain <input> with an input listener. If your tool "types" this way, the host value staying empty is a defect in the tool, not the component.
  • Calling .click() on the esp-button host 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), or press("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: true when asserting a label-derived name. Playwright's name matching is substring by default, and a text input's placeholder participates 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). The getByRole guarantee 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.
Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O