ReviewOS

stacks/very-happy-dom

public
Clone

Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.

A very performant virtual DOM implementation.

19 releases, newest first.

v0.4.0

v0.4.0 · main · published 26 days ago · by github-actions[bot]

This release gives very-happy-dom a layout pass. Boxes have positions now, where getBoundingClientRect() previously reported a size at (0, 0) for everything — so overlap, ordering, hit testing and is-this-above-the-fold are answerable, and text is measured with real font metrics rather than a flat estimate.

Everything below was checked against Chrome by serving the same markup to a real browser and diffing the rects: 122 measurements over eight pages, with the structural ones exact.

  • Normal flow. Block boxes stack and fill their containing block; inline and inline-block boxes flow along a line, shrink to fit and wrap. box-sizing, margins, padding, borders, min/max sizes and percentages are all modelled, as are position: relative and absolute/fixed against the nearest positioned ancestor.
  • Flexbox. Both axes and both reverses, wrapping including wrap-reverse, justify-content, align-items, align-content, align-self, flex-grow/shrink/basis, gap, order, auto margins, and align-items: baseline.
  • Grid. Track lists with fr, auto, minmax() and repeat(auto-fill, ...), named areas, implicit tracks, grid-auto-flow including dense, placement by line number or span, and the justify-* and align-* family for both tracks and items.
  • Margin collapsing, including a first or last child's margin escaping its parent and an empty box collapsing through entirely.
  • Real font metrics. Per-glyph advance widths and kerning pairs for serif, sans-serif and monospace at both weights, which agree with Chrome to within a tenth of a percent. Text was measured at a flat half the font size per character before, which was out by up to 50% either way.
  • CSS plumbing the above needed. Shorthands expand into their longhands, font-size and line-height inherit, and em, rem, the viewport units, calc(), min(), max() and clamp() all resolve.
  • toBeInViewport, which could not be written while every box sat at the origin.

These are the reads whose values change. Each was wrong before rather than merely absent, so a test asserting the old value was asserting a placeholder.

beforenow
getBoundingClientRect().x / .yalways 0the box's real position
offsetWidth on an unsized block0its containing block's width
offsetTop / offsetLeftalways 0measured from offsetParent
offsetParentparentElementnearest positioned ancestor
elementFromPoint()always bodythe element at the point
scrollWidth / scrollHeightalways 0the content's extent
getClientRects()always emptyone box per rendered element
IntersectionObserver entriesa fixed 100x100 rectmeasured geometry
an inline-block's ytop of its lineon the text baseline
a text-driven widthlength x font-size x 0.5real advances and kerning

A declared size is still reported for an element outside a document, so a test that measures something it never appended keeps working.

A font outside the three generic families is measured as the family it falls back to, so a page in Inter is measured as Arial, and a character outside ASCII takes the family's average advance. A line's baseline comes from an estimated ascent, within about a third of a pixel. There is no user-agent stylesheet, so body starts at (0, 0) rather than a browser's 8px inset. An item spanning several grid tracks does not contribute to those tracks' sizes, and named grid lines are not resolved. Floats, transform, z-index ordering and intrinsic image sizes are absent.

The compatibility guide has the full table.

v0.3.5

v0.3.5 · main · published 1 month ago · by github-actions[bot]

Fixes a parser failure on framework markup. @click="tap()" was not a name the attribute scanner accepted, so it stopped at the @, abandoned the whole tag, and emitted @click="tap()">Go as text:

<button @click="tap()">Go</button>
textContent was '@click="tap()">Go'   -- the <button> was gone

A page of Alpine, Vue or stx markup came through as visible attribute soup with its elements missing. Found by reading a real application's served HTML, where innerText returned a wall of @click and x-class strings where the copy should have been.

The spec allows almost any character in an attribute name, which is why these work in a browser with no build step. @click, #slot, :aria-pressed, x-cloak and v-on:click are all attributes now. A nameless position steps forward rather than breaking out of the attribute loop, so a stray character cannot silently truncate a tag's attributes either.

v0.3.4

v0.3.4 · main · published 1 month ago · by github-actions[bot]

Two more fixes found by pointing the library at a real application's served HTML. Neither needs anything exotic — a social meta tag and a lang attribute are on essentially every page on the web.

meta[property="og:title"] threw Unsupported pseudo-class: ":title". The pseudo-class scan read the raw selector including the inside of [...], so a colon in a quoted attribute value was taken as the start of a pseudo-class. Every og: meta selector failed, as did any value holding a URL or a time. Quoted content is masked before the scan now, which leaves :not([property="og:x"]) working.

setContent silently dropped every attribute on <html>. An innerHTML assignment cannot set them and nothing carried them across, so lang was lost — a test asserting the page declares a language failed against a page that declares one. A fragment that says nothing about <html> leaves the existing attributes alone.

v0.3.3

v0.3.3 · main · published 1 month ago · by github-actions[bot]

Fixes a crash on valid HTML. A < that cannot open a tag is now text, as it is in a browser.

<p>a &lt; b</p>        threw `Invalid tag name` — a document containing a
                       comparison could not be parsed at all
<p>a &lt;1 b</p>       parsed, but reported "a " — the `<1` was read as a tag
                       and the rest of the line silently discarded

Found by pointing the parser at a real application's served HTML and delta-minimising the 634KB page that crashed it down to a fragment of a template expression: rating() < 1 || submitting(). Markup like that is ordinary — a comparison inside an attribute expression, a price, "<3" — and no fixture in the test suite had ever contained one.

<! that is neither a comment nor a DOCTYPE, <?, and a nameless </> are also text now, rather than the bogus comments a browser would make of them. That is a deliberate narrowing: text can neither crash nor discard content, which are the two failures that matter.

v0.3.2

v0.3.2 · main · published 1 month ago · by github-actions[bot]

A second type-only fix. 0.3.0 and 0.3.1 both ship a declaration graph that does not resolve: dist/screenshot/webview.d.ts was never written, while capture.d.ts imports three types from it and the entry point re-exports capture. So every consumer who typechecks fails, not only those touching dialogs:

dist/screenshot/capture.d.ts(3,111): error TS2307: Cannot find module './webview'

Runtime behaviour was never affected — dist/*.js has been correct throughout. Upgrade from 0.3.0 or 0.3.1 if you use TypeScript. There is nothing else here.

0.3.1 added a guard that typechecks a consumer file against the built package. It filtered to TS1xxx syntax errors, and a module that was never written is a resolution error, so it passed while this was broken.

It now fails on any error naming a file under the package's own dist/, whatever the code, and is verified against three shapes: the missing declaration, 0.3.0's malformed Dialog comment verbatim, and a dangling type reference.

Five earlier versions of that filter each passed while something was broken, which is the thing worth knowing: a guard that matches nothing looks exactly like a guard that found nothing. skipLibCheck: true skips the very files being checked; a lone .d.ts reports unresolved imports for every file; a file argument beside a tsconfig raises TS5112 and exits 0 without checking; an absolute path match fails because tsc reports relative; an unanchored dist/ match catches every node_modules/x/dist in the tree.

v0.3.1

v0.3.1 · main · published 1 month ago · by github-actions[bot]

A type-only fix. 0.3.0's dist/browser/Dialog.d.ts was not valid TypeScript, so the package failed to typecheck for every consumer the moment they imported it:

readonly or an empty string. */
  defaultValue?: string;
constructor(type: DialogType, message: string);

A doc comment inside the constructor's parameter list made the declaration emitter splice the comment's own text into the class body as a member, and drop defaultValue from the emitted signature. Runtime behaviour was never affected — dist/*.js was correct throughout — so anyone not typechecking saw nothing wrong.

Upgrade from 0.3.0 if you use TypeScript. There is nothing else in this release.

No test read the generated declarations. The suite typechecks src/, and the packing guard added in #1598 checks the declared exports exist in the tarball. Neither looks at what the emitter produced, and the only position that finds this is a consumer's.

There is now a guard that takes that position: it writes a file importing the built package, typechecks it against dist/ with its own tsconfig, and fails on any TS1xxx syntax error anywhere in the declaration graph.

v0.3.0

v0.3.0 · main · published 1 month ago · by github-actions[bot]

Closes the last of the Playwright-parity gaps. A minor rather than a patch: nine of these change what existing code observes, and most of them do so by reporting something that used to pass silently.

Each of these can turn a passing test red. In every case the old behaviour was the bug — an assertion that held whatever the page did.

  • isChecked, toBeChecked, check/uncheck and getByRole({ checked }) read aria-checked. A design system's <div role="checkbox"> reported unchecked whatever its state, so not.toBeChecked() passed unconditionally and check() on an already-ticked control turned it off. (#1616)
  • getBy* skip elements collapsed to a declared zero size. A closed accordion or drawer built with height: 0 was fully reachable, so a role query matched a button inside it and the click succeeded. (#1617)
  • innerText collapses whitespace. It only trimmed the ends before, so indentation between elements came through as literal runs of spaces. (#1614)
  • @media rules apply. A dark-mode or responsive stylesheet had no effect, while matchMedia reported the query matching — the two disagreed about the same document. (#1611)
  • waitForSelector throws TimeoutError instead of returning null. The null turned "the selector never appeared" into null is not an object at the call site. null now means only a deliberate detached/hidden wait. (#1605)
  • keyboard.press('Control+a') dispatches key: 'a' with ctrlKey: true. It dispatched the literal string 'Control+a' with no flags set, a value no real KeyboardEvent can carry — so the handler never ran and the test asserted that nothing happened, which was true. A key held with Control, Alt or Meta no longer inserts text. (#1615)
  • request and response fire for intercepted fetches, not only navigations. route() enabled interception without ever reporting a request; using it together with setRequestInterception(true) reported every request twice. (#1606)
  • filter() throws on an unrecognised key. filter({ has: ... }) was dropped silently and matched every candidate. (#1619)
  • A full document string without a doctype parses correctly. Its <html> wrapper was assigned into documentElement, nesting an <html> inside the <html>: elements existed but document.querySelector could not reach them. Affected goto as much as setContent. (#1610)

And one fix in the other direction: setRequestInterception(false) no longer kills every route registered afterwards, which used to send the request to the real network with nothing reporting a problem. (#1613)

  • Dialogs — page.on('dialog') with accept/dismiss, so the destructive half of a confirm-gated flow is testable. Decisions must be synchronous; a handler that awaits first is reported rather than silently defaulted. (#1612)
  • page.setContent() (#1610), page.emulateMedia() (#1611)
  • The waitFor* family — waitForEvent, waitForResponse, waitForRequest, waitForLoadState, waitForURL (#1606)
  • Locator composition — filter({ has, hasNot }), or(), and(), .page (#1619)
  • Locator actions — selectOption, setInputFiles, press, clear, dblclick, setChecked (#1608), evaluate, evaluateAll, allTextContents, allInnerTexts (#1607), boundingBox, scrollIntoViewIfNeeded (#1609)
  • Seven more matchers — toHaveRole, toHaveAccessibleName, toHaveAccessibleDescription, toHaveCSS, toHaveId, toHaveJSProperty, toHaveValues (#1618)
  • keyboard.down() / up(), and event.code is populated (#1615)

toBeInViewport and waitForLoadState('networkidle') are not provided, and beforeunload is not in DialogType. With no layout pass and no in-flight request tracking, each could only ever answer "yes" — a silent pass wearing an assertion's clothes. A test records each decision so it is not added later from Playwright's list.

1692 → 1965 tests.

very-happy-dom v0.1.6

v0.1.6 · main · published 3 months ago · by chrisbbreuer

Patch release with complete custom-element upgrade semantics, Shadow DOM slot assignment and slotchange behavior, plus deterministic WebSocket connection-failure handling. The attached package is the same npm-compatible artifact produced by the repository release build.