ReviewOS

stacks/very-happy-dom

BrowserPage.type() does not focus controls or dispatch input and keyboard events

#1588
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Summary

BrowserPage.type() changes an input's value attribute but does not focus the control or dispatch keyboard and input events. Reactive forms therefore do not observe typing, even though the DOM value appears to change.

This differs from real user input and from Playwright's input APIs.

Reproduction

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

const page = new Browser().newPage()
page.content = '<body><input id="name"></body>'

const input = page.mainFrame.document.querySelector('#name')!
let inputEvents = 0
let changeEvents = 0

input.addEventListener('input', () => inputEvents++)
input.addEventListener('change', () => changeEvents++)

await page.type('#name', 'Ada')

console.log((input as HTMLInputElement).value) // Ada
console.log(inputEvents) // 0
console.log(changeEvents) // 0
console.log(page.mainFrame.document.activeElement === input) // false

Confirmed against main at 59d2e7c3 with Bun 1.4.1.

Expected behavior

type() should focus the target and dispatch a browser-like event sequence for each character, including keyboard events and an input event after the value changes. A separate fill() API would be useful for the faster Playwright behavior of focusing, replacing the value, and firing input once.

At minimum:

  • The element becomes document.activeElement.
  • Controlled/reactive forms receive input events.
  • Keyboard handlers receive useful keydown/keyup data.
  • Disabled/read-only/non-editable targets fail clearly.
  • Existing text selection/value behavior is defined.

Playwright distinguishes fill() from character-by-character keyboard input and documents the expected event-oriented behavior here: https://playwright.dev/docs/input

page.keyboard.press() dispatches events on document, not the focused element, and creates a generic Event with an assigned key. Bringing type(), keyboard.press(), and focus tracking under one interaction path would avoid divergent behavior.

Why this matters

Frameworks normally update state from input/keyboard events, not by polling attributes. Current tests can pass while the same form fails under real browser automation, which is exactly the class of false confidence a Playwright-compatible interaction layer should prevent.

9 days ago

Implemented on main in adea612.

The reported reproduction:

await page.type('#name', 'Ada')

input.value             // 'Ada'      (was '')
inputEvents             // 3          (was 0)
changeEvents            // 0, by design — change belongs to fill/commit
document.activeElement  // the input  (was not)

type() now focuses the target and emits the browser's sequence per character — keydown, beforeinput, the value change, input, keyup — writing the value property and leaving the attribute alone. Reading the attribute back was also why typing after a programmatic input.value = … silently discarded it.

Also fixed, since they were the same fault in neighbouring methods

  • fill() added, as the report suggested: focus, replace, one input, then change.
  • focus() calls element.focus() instead of only dispatching a focus event, so document.activeElement agrees with the event that just fired. blur() added for symmetry.
  • click() focuses first, as a real click does.
  • keyboard.press/type go to the focused element rather than the document, use real KeyboardEvents, and insert text — keyboard.type() previously moved no characters anywhere. Backspace deletes; a named key like Enter does not insert itself.
  • hover() dispatches bubbling mouseover as well as mouseenter, since delegated handlers listen for the former.
  • Events are real KeyboardEvent/InputEvent/MouseEvent rather than bare Event, so key, data, inputType, clientX/clientY arrive populated.

One test was asserting the bug

test/legacy/new-features.test.ts checked getAttribute('value') after typing. I corrected it to assert the property, and added an assertion that the attribute is left alone — flagging it because it is a deliberate expectation change, not an incidental edit.

Still not done here

mouse.click(x, y) continues to dispatch on the document: with no layout, a coordinate cannot be resolved to an element. That is now stated in a comment rather than implied. Hit-testing would need a layout engine.

19 tests in test/browser-page-interaction.test.ts; 15 fail against the previous implementation. Suite at 1463 passing.

9 days ago

Fixed in adea612 (type/fill/hover focus controls and dispatch real keyboard, input and mouse events). 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.