DOM parity
Chrome is the oracle. 192 features — interfaces, members and behaviours — are put to all four runtimes, and 46 behaviour cases run the same code in each.
| Runtime | Agrees with Chrome | |
|---|---|---|
| esdev | 168 / 192 | 88% |
| jsdom | 155 / 192 | 81% |
| happy-dom | 158 / 192 | 82% |
By area
| Area | Features | esdev | jsdom | happy-dom |
|---|---|---|---|---|
| tree | 44 | 43 | 40 | 36 |
| traversal | 13 | 11 | 12 | 10 |
| cascade | 24 | 20 | 12 | 17 |
| events | 24 | 23 | 20 | 24 |
| components | 17 | 17 | 12 | 13 |
| parsing | 12 | 8 | 12 | 11 |
| selectors | 13 | 13 | 13 | 12 |
| forms | 14 | 14 | 14 | 14 |
| window | 31 | 19 | 20 | 21 |
Behaviour cases
44 of 46 cases match Chrome exactly, and 2 is a documented limit.
| Case | Area | esdev | jsdom | happy-dom |
|---|---|---|---|---|
| append-and-replace-children | tree | yes | yes | yes |
| fragment-contents-move-on-append | tree | yes | yes | yes |
| deep-clone-is-independent | tree | yes | yes | yes |
| collections-stay-live | tree | yes | yes | yes |
| capturing-and-bubbling-order | events | yes | yes | yes |
| cancelled-event-returns-false | events | yes | yes | yes |
| inline-handlers-run-for-modern-events | events | yes | yes | yes |
| once-listener-runs-once | events | yes | yes | yes |
| stopped-propagation-does-not-reach-parent | events | yes | yes | yes |
| fragment-serializes-text-and-attributes | parsing | yes | yes | yes |
| raw-text-is-not-parsed-as-elements | parsing | yes | yes | yes |
| svg-attributes-and-self-closing-children | parsing | yes | yes | no |
| namespace-attribute-access-and-replacement | tree | yes | yes | yes |
| dataset-reflects-data-attributes | tree | yes | yes | no |
| create-element-ns-preserves-identity | tree | yes | yes | yes |
| omitted-tags-html-allows-are-filled-in | parsing | yes | yes | no |
| table-collections-follow-the-section-order | tree | yes | yes | no |
| fragment-structure-follows-its-context-element | parsing | yes | yes | yes |
| malformed-markup-is-a-strict-esdev-limit | parsing | no | yes | yes |
| compound-and-attribute-selector | selectors | yes | yes | yes |
| logical-selector-list | selectors | yes | yes | yes |
| structural-selector | selectors | yes | yes | yes |
| invalid-selector-throws-syntax-error | selectors | yes | yes | no |
| nth-child-counts-only-the-of-list | selectors | yes | no | yes |
| defined-pseudo-class-follows-the-definition | components | yes | yes | no |
| adjacent-insertion-lands-at-four-positions | tree | yes | yes | yes |
| declarative-shadow-root-attaches-only-through-set-html-unsafe | components | yes | no | no |
| form-associated-custom-element-joins-its-form | components | yes | no | no |
| custom-state-matches-the-state-pseudo-class | components | yes | no | no |
| origin-importance-specificity-and-order-decide-the-winner | cascade | yes | yes | yes |
| inheritance-and-user-agent-defaults | cascade | yes | no | no |
| media-and-supports-conditions-gate-their-rules | cascade | yes | no | no |
| constructed-sheets-apply-while-adopted | cascade | yes | no | no |
| a-shadow-root-is-styled-by-its-own-sheets | cascade | yes | no | no |
| nesting-resolves-against-its-parent-rule | cascade | yes | no | no |
| geometry-answers-zero-and-rendering-is-knowable | cascade | no | no | no |
| input-indeterminate-is-non-reflecting-state | forms | yes | yes | yes |
| form-owner-follows-form-attribute | forms | yes | yes | yes |
| form-and-submitter-settings-reflect | forms | yes | no | yes |
| form-data-uses-successful-controls | forms | yes | yes | yes |
| required-control-blocks-submit | forms | yes | yes | yes |
| submitter-no-validate-bypasses-constraints | forms | yes | no | yes |
| reset-control-restores-default-value | forms | yes | yes | yes |
| radio-click-is-exclusive-by-form-owner | forms | yes | yes | yes |
| label-click-activates-nested-checkbox | forms | yes | yes | yes |
| disabled-fieldset-excludes-validation-and-data | forms | yes | yes | no |
Closer to Chrome than an emulator
39 features where esdev matches Chrome and jsdom or happy-dom does not.
| Feature | Chrome & esdev | jsdom | happy-dom |
|---|---|---|---|
| CDATASection | yes | yes | no |
| DOMImplementation | yes | yes | no |
| DOMTokenList | yes | yes | no |
| DOMStringMap | yes | yes | no |
| CharacterData in the prototype chain | Text < CharacterData < Node < EventTarget < Object | Text < CharacterData < Node < EventTarget < Object | Text < Text < CharacterData < Node < EventTarget < Object |
| document.doctype | html | html | none |
| Element.setHTMLUnsafe | yes | no | no |
| Element.getHTML | yes | no | yes |
| Element.checkVisibility | yes | no | no |
| StaticRange | yes | yes | no |
| AbstractRange | yes | yes | no |
| Range.getBoundingClientRect | yes | no | yes |
| element scroll offsets | 0,0,false | 40,0,true | 40,0,false |
| DragEvent | yes | no | yes |
| ClipboardEvent | yes | no | yes |
| IntersectionObserver | yes | no | yes |
| ResizeObserver | yes | no | yes |
| manual slot assignment | yes | no | yes |
| shadow root reports clonable and serializable | true,false | , | true,false |
| setHTMLUnsafe attaches a declarative root | attached | throws:TypeError | throws:TypeError |
| attachInternals | yes | yes | no |
| form-associated custom element | yes | no | throws:TypeError |
| CustomStateSet | yes | no | no |
| omitted paragraph end tag | <p>one</p><p>two</p> | <p>one</p><p>two</p> | <p>one<p>two</p></p> |
| invalid selector throws | SyntaxError | SyntaxError | no throw |
| adoptedStyleSheets applies | rgb(1, 2, 3) | rgb(0, 0, 0) | rgb(1, 2, 3) |
| user-agent display defaults | block,inline | block,inline | block, |
| no computed style outside the tree | yes | no | yes |
| @media follows the viewport | rgb(7, 7, 7) | rgb(0, 0, 0) | rgb(7, 7, 7) |
| matchMedia answers | true,false | throws:TypeError | true,false |
| CSS.supports | true,true | undefined,undefined | true,true |
| CSS nesting resolves | rgb(2, 2, 2) | rgb(1, 1, 1) | `` |
| scrollIntoView | yes | no | yes |
| structuredClone | yes | no | no |
| fetch | yes | no | yes |
| WebSocket | yes | yes | no |
| Worker | yes | no | no |
| URL.createObjectURL | yes | no | yes |
| MathMLElement | yes | no | no |
Differences from Chrome
24 features, each with its reason. There are no others.
No layout: there is no box model, so there is nothing to measure or scroll.
| Feature | Chrome | esdev |
|---|---|---|
| innerText | yes | no |
| getBoundingClientRect measures | measured | zero |
| offsetWidth measures | measured | zero |
Legacy or superseded by a modern API that is implemented.
| Feature | Chrome | esdev |
|---|---|---|
| NodeIterator | yes | no |
| XPathEvaluator (document.evaluate) | yes | no |
| TouchEvent | yes | no |
| XMLSerializer | yes | no |
| document.write | yes | no |
Deliberately strict: malformed markup is refused rather than repaired.
| Feature | Chrome | esdev |
|---|---|---|
| malformed HTML | <p><i>unclosed</i></p> | throws:SyntaxError |
| misnested tags | <b><i>x</i></b> | throws:SyntaxError |
No rendering: the top layer, animations and transitions have nothing to paint.
| Feature | Chrome | esdev |
|---|---|---|
| Element.animate (WAAPI) | yes | no |
| showPopover | yes | no |
| dialog.showModal | yes | no |
| document.startViewTransition | yes | no |
Specified values only: resolving one needs layout and a font.
| Feature | Chrome | esdev |
|---|---|---|
| resolved font size | 32px | 2em |
Harness artifact: Chrome ran the probe on about:blank, which is an opaque, insecure origin.
| Feature | Chrome | esdev |
|---|---|---|
| localStorage | throws:SecurityError | yes |
| crypto.randomUUID | no | yes |
No host resource: this DOM reaches no network, no file and no browsing context.
| Feature | Chrome | esdev |
|---|---|---|
| document.cookie | yes | no |
| XMLHttpRequest | yes | no |
| File / FileReader | yes | no |
| alert | yes | no |
| Image | yes | no |
| canvas.getContext | context | missing |
| iframe.contentWindow | window | null |