ReviewOS

stacks/very-happy-dom

No page.setContent(): putting markup in a page reaches through to document.body.innerHTML

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

Problem

typeof page.setContent   // 'undefined'

Putting markup in a page means reaching through three objects to the document:

// Playwright
await page.setContent('<button>Save</button>')

// here
page.mainFrame.window.document.body.innerHTML = '<button>Save</button>'

page.content exists as a getter, so the read side is there and the write side is not.

Why it matters

Almost every Playwright example that is not about navigation starts with setContent, so its absence is hit on the first line of a port. It is also the first thing tried when writing a test for this library: both test/locator-visibility.test.ts and test/browser-page-events.test.ts open with a local helper

function render(css: string, markup: string): void {
  document.head.innerHTML = ...
  document.body.innerHTML = markup
}

which is a setContent with a different name, written twice because there was nothing to call.

The reach-through is not just verbose. page.mainFrame.window.document.body assumes there is a body, which is not true of a page whose document was replaced — the bug fixed in #1595 — and it skips whatever setContent should be doing around the assignment: replacing the whole document rather than only the body, and settling before it resolves.

Proposed scope

  • page.setContent(html, { timeout }) — replace the document, not only body.innerHTML, so a full <!DOCTYPE html><html><head>… string lands where it belongs and a bare fragment still becomes the body.
  • Resolve once parsing has settled, and fire domcontentloaded and load as a navigation does. This is what makes setContent followed by an assertion safe without a wait.
  • frame.setContent too, since BrowserFrame owns the document.
  • Reuse the parse path BrowserFrame._navigate already uses for an HTML response rather than adding a second one — two HTML entry points that disagree about how a fragment is wrapped would be a new class of bug.

Once it exists, the two local render() helpers in the test suite should be replaced by it, which is also the proof it covers the real use.

Acceptance

  • await page.setContent('<button>Save</button>') leaves page.getByRole('button') matching, with no wait in between.
  • A full document string sets document.title from its <head>, proving the whole document was replaced rather than the body filled.
  • load and domcontentloaded fire.
  • page.content round-trips what was set.
  • The existing render() helpers in test/locator-visibility.test.ts and test/browser-page-events.test.ts are replaced with setContent, and those suites still pass.

Found while assessing whether wildloop's Playwright specs could port here.

Sign in to comment on this issue.