ReviewOS

stacks/very-happy-dom

BrowserPage has no state-query methods (textContent, inputValue, isVisible, …)

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

Summary

BrowserPage can act on a selector (click, type, hover, focus) but cannot read anything back. Playwright's state-query methods are all absent, so assertions have to reach through page.mainFrame.document and work on the DOM directly, which defeats the point of the page abstraction.

These are small, self-contained additions and the underlying data is already available — this is a thin adapter over the frame's document rather than new capability.

Reproduction

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

const page = new Browser().newPage()
page.content = '<body><h1>Hello</h1><input id="n" value="Ada"><input type="checkbox" id="c" checked><button disabled>Go</button></body>'

console.log(typeof page.textContent) // undefined
console.log(typeof page.innerText) // undefined
console.log(typeof page.innerHTML) // undefined
console.log(typeof page.inputValue) // undefined
console.log(typeof page.getAttribute) // undefined
console.log(typeof page.isVisible) // undefined
console.log(typeof page.isHidden) // undefined
console.log(typeof page.isChecked) // undefined
console.log(typeof page.isEnabled) // undefined
console.log(typeof page.isDisabled) // undefined
console.log(typeof page.isEditable) // undefined
console.log(typeof page.title) // undefined

// The values are all reachable, just not through the page:
const doc = page.mainFrame.document
console.log(doc.querySelector('h1').textContent) // Hello
console.log(doc.querySelector('#n').value) // Ada
console.log(doc.querySelector('#c').checked) // true

Confirmed against main at 5fae54d4 with Bun 1.4.3.

Current implementation

BrowserPage has content and url getters and nothing else that reads page state. Every action method resolves its selector, mutates, and returns void.

Expected behavior

Selector-taking query methods on BrowserPage, each resolving against the main frame's document:

MethodReturns
textContent(selector)string | null — the element's textContent
innerText(selector)string — rendered text
innerHTML(selector)string
inputValue(selector)string — for input, textarea, select
getAttribute(selector, name)string | null
isVisible(selector)boolean
isHidden(selector)boolean
isChecked(selector)boolean
isEnabled(selector)boolean
isDisabled(selector)boolean
isEditable(selector)boolean
title()string — the document title

Two details worth deciding explicitly:

  • In Playwright these are async. Matching that keeps signatures portable even though nothing here needs to await.
  • isVisible has no layout engine to consult. A reasonable definition is: the element is in the document and neither it nor an ancestor has display: none, visibility: hidden or the hidden attribute. That should be documented, since it will not match a real browser for zero-size or off-screen elements.

For the non-existent-selector case, Playwright's own split is worth following: isVisible returns false, while textContent and friends throw.

Related: #1591 — these same methods belong on Locator, and sharing one resolve-and-read implementation between the two would avoid divergence.

8 days ago

Implemented on main in 49457fa.

All twelve from the table are in: title, textContent, innerText, innerHTML, inputValue, getAttribute, isVisible, isHidden, isChecked, isEnabled, isDisabled, isEditable — all async, so the signatures stay portable.

isVisible came out stronger than this issue anticipated

When I filed this I wrote that isVisible "has no layout engine to consult" and proposed defining it against inline styles. That is no longer the constraint: the cascade landed in 3af7403 (#1597), so it now resolves class-based rules properly.

<style>.gone { display: none }</style>
<div class="gone">…</div>

await page.isVisible('.gone')  // false

It walks the ancestry, honours the hidden attribute, and rejects a detached subtree. So "did the panel open" is genuinely answerable — which is what that assertion usually means in practice.

The remaining limit is size and position, not styling. With no layout, a 0x0 or off-screen element still reports visible. Rather than leave that to be discovered, there is a test asserting it, so the boundary is documented rather than implied.

Playwright's split on missing elements is followed

isVisible returns false for a selector that matches nothing — it is usually asked of something that may legitimately be absent — while a content read throws and names the selector. inputValue refuses a non-field rather than quietly returning an empty string.

inputValue reads the value property, not the attribute, so it reflects what was typed or assigned. That is the same distinction that made type() look broken before adea612.

19 tests in test/browser-page-queries.test.ts. Suite at 1463 passing, lint/typecheck/build clean.

8 days ago

Fixed in 49457fa (title, textContent, inputValue, isVisible and the rest), with the locator equivalents in a0b0889. 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.