ReviewOS

stacks/very-happy-dom

No locator API: page.locator() and the getBy* helpers are missing

#1591
Closed glennmichael123 opened this 8 days ago ·
8 days ago

Summary

BrowserPage exposes selector-string actions (click, type, hover, focus, waitForSelector) but has no locator layer. None of Playwright's locator entry points exist, and neither do the older element-handle helpers, so there is no way to build a reusable, re-queried reference to an element.

Locators are the primary way modern Playwright tests are written, so their absence is the largest single gap between this Browser API and Playwright.

Reproduction

import { Browser } from 'very-happy-dom'

const page = new Browser().newPage()

console.log(typeof page.locator) // undefined
console.log(typeof page.getByRole) // undefined
console.log(typeof page.getByText) // undefined
console.log(typeof page.getByLabel) // undefined
console.log(typeof page.getByPlaceholder) // undefined
console.log(typeof page.getByAltText) // undefined
console.log(typeof page.getByTitle) // undefined
console.log(typeof page.getByTestId) // undefined
console.log(typeof page.frameLocator) // undefined
console.log(typeof page.$) // undefined
console.log(typeof page.$$) // undefined
console.log(typeof page.$eval) // undefined
console.log(typeof page.$$eval) // undefined

Confirmed against main at 5fae54d4 with Bun 1.4.3.

Current implementation

BrowserPage resolves a selector to an element inline on each call, via the frame's document.querySelector, and acts on it immediately. There is no object representing "the element this selector will match", so nothing can be stored, chained, filtered or re-queried.

Expected behavior

A Locator that resolves lazily on each use, rather than capturing an element at creation:

const saveButton = page.getByRole('button', { name: 'Save' })
await saveButton.click()

Entry points:

MethodResolves by
page.locator(selector)CSS (and ideally Playwright's text= / >> forms)
page.getByRole(role, options)ARIA role, with name, exact, checked, disabled, selected
page.getByText(text, options)text content, string or RegExp, with exact
page.getByLabel(text, options)associated <label>, including aria-label and aria-labelledby
page.getByPlaceholder(text)placeholder attribute
page.getByAltText(text)alt attribute
page.getByTitle(text)title attribute
page.getByTestId(id)the configured test-id attribute, data-testid by default

Locator methods worth covering first: click, hover, textContent, innerText, inputValue, getAttribute, count, first, last, nth, all, and the isVisible/isChecked/isEnabled predicates. Chaining (parent.locator('child')) and filter({ hasText }) are what make locators composable, so they matter more than breadth of individual methods.

getByRole needs role resolution and accessible-name computation, which is the substantial piece — implicit roles from tag names plus explicit role attributes would already cover most test usage.

Note that locator actions in Playwright auto-wait for actionability. Matching that fully is a larger change; resolving at call time and throwing a clear error when nothing matches would be a reasonable first step.

Related: #1590 (frame windows are stubs), which affects anything a locator needs to evaluate in page context. The element-handle helpers $, $$, $eval and $$eval are listed above for completeness, but locators supersede them and are the better target.

8 days ago

Implemented on main in a0b0889.

The headline example from the report:

const save = page.getByRole('button', { name: 'Save' })
await save.click()

Entry points: locator(css), getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle, getByTestId (attribute configurable via setTestIdAttribute). All of them exist on a locator too, so they scope — page.locator('#card').getByRole('button').

Locators carry count, first, last, nth (negative counts from the end), all, filter({ hasText, hasNotText }), the reads from #1592, the actions from #1588, and check/uncheck.

Resolution is lazy, which was the point: a locator picks up an element added after it was created and stops matching one that was removed. Both are tested.

Role and accessible-name computation

getByRole needed both, and neither existed here. They now live in src/aria/roles.ts:

  • Roles — explicit role (first token of a list), the tag mapping, and the cases that change it: input[type], select[multiple]/size, a with and without href, img with empty alt as presentational, section as a region only once named.
  • Names — aria-labelledby (joining several refs), aria-label, native labelling (associated and wrapping <label>, push-button value, alt, legend, caption), name-from-content for the roles that allow it, then title, then placeholder.

It is a working subset, not the full specification, and says so — where it cannot determine something it returns null or an empty string rather than guessing.

One trap worth recording: deciding whether a <section> is a region asks for its name, and the name algorithm asks for the role. That recursed to a stack overflow on the first run. The role check now consults only the author-supplied labelling attributes, which a section's name comes from anyway.

Two behaviours I decided rather than inferred

  • Strictness. A single-valued read or an action on an ambiguous locator throws and names the count, instead of silently taking the first match — that silence is how a test ends up asserting against the wrong element. first/last/nth are how you say which you meant.
  • getByText returns the innermost match. Every ancestor of a match also contains the text, so returning all of them would make it ambiguous for almost any markup.

Not done

Auto-waiting for actionability. The report flags it, and it is deliberately absent: there is no rendering loop to wait on, so a miss is an immediate named error rather than a timeout. If you want retry-until-timeout semantics that is a separate change.

Playwright's text= / >> selector-string syntax is also not parsed — the getBy* methods and chaining cover the same ground.

66 tests across test/browser-locators.test.ts and test/aria-roles.test.ts.

8 days ago

Fixed in a0b0889 (locators with strict resolution, filter, nth/first/last, and the getBy* helpers over computed ARIA roles and accessible names). Released in v0.1.11, so this is now available to consumers rather than only on main. The scope notes I left earlier on this issue still stand — read those for what was deliberately left out.

Sign in to comment on this issue.