DOM parity
Chrome is the oracle. 234 features — interfaces, members and behaviours — are put to all four runtimes, and 121 behaviour cases run the same code in each.
| Runtime | Agrees with Chrome | |
|---|---|---|
| esdev | 217 / 234 | 93% |
| jsdom | 184 / 234 | 79% |
| happy-dom | 185 / 234 | 79% |
By area
| Area | Features | esdev | jsdom | happy-dom |
|---|---|---|---|---|
| tree | 55 | 55 | 49 | 44 |
| traversal | 14 | 13 | 13 | 11 |
| selectors | 14 | 14 | 14 | 13 |
| cascade | 26 | 23 | 12 | 17 |
| window | 36 | 29 | 24 | 26 |
| events | 26 | 24 | 22 | 25 |
| components | 32 | 32 | 20 | 20 |
| parsing | 13 | 9 | 12 | 11 |
| forms | 18 | 18 | 18 | 18 |
Behaviour cases
119 of 121 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 |
| list-interfaces-iterate-as-web-idl-declares | tree | yes | no | no |
| details-dialog-and-popover-toggle-alike | events | yes | no | no |
| a-removed-subtree-stays-observed-until-delivery | events | yes | no | no |
| an-observer-that-throws-is-reported-and-the-rest-still-run | events | yes | yes | yes |
| a-click-runs-activation-behaviour-however-it-is-dispatched | events | yes | no | no |
| keyboard-and-mouse-events-carry-their-modifiers | events | yes | yes | no |
| 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 |
| an-equal-specificity-tie-goes-to-the-later-sheet | cascade | yes | no | no |
| floats-positioned-boxes-and-flex-items-are-blockified | cascade | yes | no | no |
| inner-text-is-the-rendered-text | cascade | yes | no | no |
| media-lists-and-grouping-rules-are-their-interfaces | 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 |
| html-names-are-case-insensitive | tree | yes | yes | yes |
| bare-constructors-and-fragment-lookups | tree | yes | no | yes |
| a-radio-group-is-exclusive-on-the-checked-setter | forms | yes | yes | yes |
| escapes-in-an-identifier | selectors | yes | yes | no |
| an-unknown-property-is-not-a-declaration | cascade | yes | no | no |
| a-node-iterator-walks-and-steps-back | traversal | yes | yes | no |
| a-secondary-document-is-a-whole-document | tree | yes | yes | no |
| reaction-order-on-creation-and-connection | components | yes | yes | yes |
| an-existing-element-upgrades-when-it-is-defined | components | yes | yes | no |
| parsed-children-upgrade-in-tree-order | components | yes | yes | yes |
| the-registry-answers-about-a-definition | components | yes | yes | no |
| an-element-can-be-upgraded-on-demand | components | yes | yes | no |
| constants-node-values-and-odd-nodes-serialize | tree | yes | no | no |
| every-way-an-attribute-changes-is-one-change | tree | yes | no | no |
| form-and-select-collections-are-their-interfaces | tree | yes | yes | no |
| character-data-is-edited-in-place | tree | yes | no | no |
| a-range-edits-text-through-character-data | tree | yes | yes | no |
| slots-assign-and-flatten | components | yes | yes | no |
| flattening-recurses-through-a-slotted-slot | components | yes | yes | no |
| a-slot-reports-a-change | components | yes | yes | yes |
| internals-reflect-aria-and-form-state | components | yes | no | no |
| a-clonable-shadow-root-is-cloned | components | yes | no | yes |
| shadow-styles-reach-the-host-and-the-slotted | components | yes | no | no |
| a-constructor-is-refused-when-it-misbehaves | components | yes | yes | no |
| an-element-moved-between-documents-is-adopted | components | yes | yes | no |
| attributes-report-their-namespace-and-old-value | components | yes | yes | no |
| a-shadow-host-refuses-a-second-root-and-the-wrong-element | components | yes | yes | no |
| focus-inside-a-root-is-reported-from-both-sides | components | yes | yes | yes |
| an-event-is-retargeted-and-a-closed-root-hides-its-path | components | yes | yes | no |
| aria-reflects-through-attributes-and-internals | components | yes | yes | no |
| a-slot-change-follows-the-slot-attribute | components | yes | yes | yes |
| a-template-holds-a-component-and-clones-it | components | yes | no | no |
| adoption-and-a-shadow-sweep-report-once | components | yes | yes | no |
| a-closed-root-serializes-when-it-is-asked-for | components | yes | no | no |
| a-reset-clears-a-custom-elements-value | components | yes | no | no |
| a-disabled-attribute-and-a-throwing-constructor | components | yes | no | no |
| moveBefore-keeps-state-and-connection | tree | yes | no | no |
| a-document-is-parsed-from-markup | parsing | yes | no | no |
| a-dialog-opens-closes-and-returns | components | yes | no | no |
| a-popover-toggles-and-reports-its-state | components | yes | no | no |
| a-command-button-acts-on-the-element-it-names | components | yes | no | no |
| an-attribute-name-is-lowercased-for-html-only | tree | yes | yes | yes |
| a-document-has-no-text-content | tree | yes | yes | no |
| internal-work-does-not-read-the-attributes-accessor | tree | yes | yes | yes |
| 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 |
| cancel-bubble-is-the-stop-propagation-flag | events | yes | yes | no |
| nodes-stringify-as-their-interface | tree | yes | no | no |
| svg-elements-get-their-own-interface | parsing | yes | no | yes |
| a-customized-built-in-upgrades-its-built-in | components | yes | no | no |
| custom-properties-substitute-into-computed-values | cascade | yes | no | no |
| a-value-a-property-cannot-take-is-dropped | cascade | yes | no | no |
| shorthands-expand-into-longhands | cascade | yes | no | no |
| named-access-on-the-window | tree | yes | yes | no |
| an-inline-declaration-holds-its-longhands | cascade | yes | no | no |
| a-controls-value-is-filtered-not-rewritten | forms | yes | no | no |
| interface-members-can-be-redefined | tree | yes | yes | yes |
| an-attribute-handler-is-compiled-and-a-throw-is-reported | events | yes | no | no |
| a-colour-resolves-when-it-is-computed | cascade | yes | no | no |
| a-value-is-checked-against-the-property | cascade | yes | no | no |
| colour-functions-are-canonical | cascade | yes | no | no |
| lengths-are-absolute-when-they-can-be | cascade | yes | no | no |
| the-layered-shorthands-expand-too | cascade | yes | no | no |
| a-computed-shorthand-comes-from-its-longhands | cascade | yes | no | no |
| a-declaration-block-holds-longhands | cascade | yes | no | no |
| every-html-name-has-its-interface | tree | yes | yes | yes |
| a-template-content-lives-in-its-own-document | tree | yes | yes | no |
Closer to Chrome than an emulator
64 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 |
| innerText | yes | no | yes |
| document.hasFocus() | yes | no | yes |
| implementation.createDocument | svg/http://www.w3.org/2000/svg | svg/http://www.w3.org/2000/svg | html/http://www.w3.org/1999/xhtml |
| unknown property is undefined | undefined/false/false | undefined/false/undefined | undefined/false/true |
| keyword initial values | none,1,visible,0px,auto | ,,,, | ,,,, |
| 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 |
| createEvent by interface name | Event/InvalidStateError/probe-created/true | Event/InvalidStateError/probe-created/true | Event/dispatched/probe-created/true |
| 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 |
| one constructor, one name | NotSupportedError | NotSupportedError | DOMException |
| node.assignedSlot | yes | yes | no |
| attachShadow refuses a non-host | NotSupportedError | NotSupportedError | attached |
| :host and ::slotted | rgb(9, 9, 9)/italic | rgb(0, 0, 0)/ | rgb(9, 9, 9)/normal |
| ARIA reflects | button/Save/null | button/Save/null | button/null/undefined |
| formDisabledCallback | true | none | none |
| a clonable root is cloned | <i>x</i> | no root | <i>x</i> |
| template content is inert | no | throws:TypeError | no |
| document.textContent | null/HTML | null/HTML | /HTML |
| Element.moveBefore | 1/0/kept | missing | missing |
| Document.parseHTMLUnsafe | <i>in</i> | missing | missing |
| dialog.show and close | true/false/ok | missing | true/false/ok |
| popover methods | auto/true/false | missing | missing |
| CommandEvent | show-popover/true | missing | missing |
| 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) | throws:TypeError | 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 |
| resolved font size | 32px | 2em | 16px |
| structuredClone | yes | no | no |
| fetch | yes | no | yes |
| WebSocket | yes | yes | no |
| Worker | yes | no | no |
| URL.createObjectURL | yes | no | yes |
| alert | yes | yes | no |
| MathMLElement | yes | no | no |
| SVGGeometryElement | yes | no | yes |
| showPopover | yes | no | no |
| dialog.showModal | yes | no | yes |
Differences from Chrome
17 features, each with its reason. There are no others.
Legacy or superseded by a modern API that is implemented.
| Feature | Chrome | esdev |
|---|---|---|
| XPathEvaluator (document.evaluate) | yes | no |
| TouchEvent | yes | no |
| XMLSerializer | yes | no |
| document.write | yes | no |
Refused by name: the modern interface name is accepted, and the HTML4 alias is answered with the constructor to use instead.
| Feature | Chrome | esdev |
|---|---|---|
| createEvent refuses the HTML4 aliases | created | NotSupportedError |
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 layout: there is no box model, so there is nothing to measure or scroll.
| Feature | Chrome | esdev |
|---|---|---|
| getBoundingClientRect measures | measured | zero |
| offsetWidth measures | measured | zero |
No rendering: the top layer, animations and transitions have nothing to paint.
| Feature | Chrome | esdev |
|---|---|---|
| Element.animate (WAAPI) | yes | no |
| document.startViewTransition | yes | no |
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 |
|---|---|---|
| XMLHttpRequest | yes | no |
| Image | yes | no |
| canvas.getContext | context | missing |
| iframe.contentWindow | window | null |