Mock the DOM for server-side DOM state and in tests.
Pseudo DOM recreates the browser DOM API (following the MDN documentation) so DOM-dependent code can run in Node - in tests, without a real or headless browser. It's written in TypeScript and ships type definitions. The API will change before 1.0.
classList, a real live style and dataset, children, and DOM-style mutation
(append, before, remove, replaceWith, ...)innerHTML / outerHTML / insertAdjacentHTML parse and serialize real HTMLquerySelector / querySelectorAll with real CSS selectors, getElementById, matches, closestcreateElement, createTextNode, and generateDocument() to get a window-like object with
document already on itcloneNode, isEqualNode, compareDocumentPosition, containsinstallGlobal() makes code written against real DOM globals (document.createElement,
...) run unmodified in Node; logElement() prints a readable, indented view of an element to the consoleEverything below goes into more detail, section by section.
The reference of every class, function and type is split by the folders of the source:
services: the classes which do the work (NodeService, ElementService, HTMLElementService, DocumentService,
EventService, the event kinds, ...). These are what you get from document.createElement(...) and friends.interfaces: the Pseudo* interfaces, the shape of each DOM object (PseudoNode, PseudoElement, PseudoEvent, ...).classes: the classes which are more than a service, such as PseudoHTMLDocument and PseudoNodeList.factories: functions which make things: generateDocument, installGlobal, logElement, createEvent, and the
parsing and query helpers behind innerHTML and querySelector.functions: small helpers such as getElementById and the focus tracking.simulate: click and keyPress, which send what a real user action sends.A real tree: parentNode, previousSibling / nextSibling, firstChild / lastChild, appendChild,
insertBefore, removeChild, replaceChild, contains, getRootNode. Nodes move when they're added somewhere
else, document fragments insert their children instead of themselves, and a node can't be put inside itself.
EventService / EventTargetService dispatch an event through the tree the way the DOM does:
stopPropagation, stopImmediatePropagation, once, passive, preventDefault, and the target's default action
(clicking a submit button sends a submit event to its form)createEvent(type, init, { browser, trusted }) makes the kind of event that suits the type (a click is a
MouseEvent, a keydown a KeyboardEvent, ...). Like a real constructor, it gives nothing (no bubbling, no
cancelling, not trusted) unless asked - but browser: true applies eventDefaults, the table of how a browser
creates each standard type (click bubbles and can be cancelled; focus doesn't bubble but focusin does; input
bubbles but can't be cancelled; ...), and trusted: true makes isTrusted true, as for a real user actionPseudoUIEvent, PseudoMouseEvent, PseudoPointerEvent, PseudoKeyboardEvent,
PseudoFocusEvent, PseudoInputEvent, PseudoCustomEventclick() (an untrusted click, like a script's) and focus() / blur() (blur/focusout/focus/focusin
and the related targets, with a focused element tracked per tree)simulate.click(element) / simulate.keyPress(element, key) send what a real user action sends - pointerdown,
mousedown, the focus moving, pointerup, mouseup, click; keydown, keyup - all trustedElementService / HTMLElementService cover Element and HTMLElement: attributes, NamedNodeMap, classList.
AttrService, DOMTokenListService and NamedNodeMapService back them.
Attribute helpers work like the DOM's: getAttributeNames, hasAttributes, toggleAttribute(name, [force]).
localName matches tagName and prefix is always null (there's no real namespace parsing);
getElementsByTagNameNS behaves exactly like getElementsByTagName, ignoring the namespace. All 37 aria*
reflected properties (ariaLabel, ariaExpanded, ariaValueNow, ...) are real too - each just reads/writes its
matching aria-* attribute (ariaLabel <-> aria-label; the mapping isn't camelCase-to-kebab-case, so
ariaColCount <-> aria-colcount, not aria-col-count).
getAttributeNode / setAttributeNode / removeAttributeNode give and take real Attr nodes (backed by
AttrService) instead of plain strings - setAttributeNode returns whatever Attr previously held that name (or
null when it's new), and removeAttributeNode throws when the element has no attribute matching the one given, like
the DOM's. Every *NS method (getAttributeNS, hasAttributeNS, setAttributeNS, removeAttributeNS,
getAttributeNodeNS, setAttributeNodeNS) behaves exactly like its non-NS counterpart, ignoring the namespace
argument entirely - there's no real namespace parsing here, matching getElementsByTagNameNS.
Form controls have a real value (input, textarea, select, button, option, output) and checked
(input). Like the DOM's, they start from the value / checked attributes (a checkbox with no value reads
'on'), and once set they stop following the attribute - setting them never changes the attribute. value is always
a string, so parseInt(input.value) works on a number attribute. cloneNode keeps the current value and checkedness.
style is a real, live CSSStyleDeclaration:
el.style.backgroundColor = 'red') alongside getPropertyValue / setProperty /
removeProperty / getPropertyPriority / cssText'', like the DOM'sdataset is a live DOMStringMap: reading/writing/deleting a camelCase name (el.dataset.fooBar)
reads/writes/removes the matching data-foo-bar attribute directly, so it can never fall out of sync with the
attributes themselves.
(style and dataset are both on HTMLElementService, matching the DOM - they're not on the base Element.)
Traversal & mutation work like the DOM's:
children is a live HTMLCollection of just the element children; childElementCount, firstElementChild /
lastElementChild and nextElementSibling / previousElementSibling skip text and comment nodesappend / prepend / before / after / remove / replaceWith / replaceChildren accept nodes or strings (a
string becomes a text node), and move a node already in a tree rather than duplicating itinsertAdjacentElement / insertAdjacentText / insertAdjacentHTML insert at beforebegin / afterbegin /
beforeend / afterendattachShadow({mode}) attaches a real (DocumentFragmentService-based) ShadowRoot, with host and mode
set. element.shadowRoot reaches it when the mode is 'open', like the DOM's (a 'closed' one still exists, just
not reachable this way); attaching a second one throws.
innerHTML / outerHTML / insertAdjacentHTML parse and serialize real HTML, via
htmlparser2 (a SAX-style tokenizer - pseudo-dom builds its own nodes
from its events, the same way it builds matches from css-select).
br, img, ...) auto-closeclass / style attributes populate className / classList and style for real, not just a generic attributeinnerHTML =, outerHTML =) and insertAdjacentHTML are on HTMLElementService (building new
elements needs a concrete element class); ElementService only has the getters, since serializing doesn'tThere's no layout engine, so anything that would need one is settable directly rather than really computed - set the value a test needs, and the getter/method returns it:
clientWidth / clientHeight / clientTop / clientLeft / scrollWidth / scrollHeight (plain numbers,
alongside the existing offsetWidth etc.)boundingClientRect backs getBoundingClientRect(), clientRects backs getClientRects(), animations backs
getAnimations(), isVisible backs checkVisibility()A few things in this group are genuinely real, not mocked:
scrollLeft / scrollTop are plain settable numbers, and scroll / scrollTo / scrollBy (a number pair or an
options object) update them for real; scrollIntoView is a callable no-op (there's no viewport to scroll within)hasPointerCapture / setPointerCapture / releasePointerCapture genuinely track capture per pointer idrequestFullscreen / requestPointerLock resolve, like a browser granting the request wouldcomputedStyleMap() is a thin read-only view of the element's own inline style (there's no CSS cascade)getElementsByTagName / getElementsByClassName (live, on any node) and querySelector / querySelectorAll (real
CSS selectors, via css-select matched against pseudo-dom's own tree
through a custom adapter) are on NodeService, so Document, DocumentFragment and Element all have them.
querySelectorAll returns a plain array - a snapshot taken when it's called, like the DOM's.
matches / closest are on ElementService. getElementById is on both DocumentService and
DocumentFragmentService (the DOM's NonElementParentNode mixin, so a ShadowRoot gets it too).
DocumentService matches the real Document - it's not an Element, so it has no tagName / classList /
matches / etc:
createElement, createTextNode, createComment, createDocumentFragmentgetElementByIdtextContent is always nullDocumentFragmentService is the plain DocumentFragment. PseudoHTMLDocument - what generateDocument() actually
creates - only adds the html / head / body structure on top of DocumentService. PseudoNodeList backs
childNodes.
Code written against a real DOM's globals (document.createElement, new Node(), ...) can run unmodified in Node,
without an if (typeof document === 'undefined') check at every call site:
if (typeof document === 'undefined') {
require('pseudo-dom').installGlobal(globalThis)
}
installGlobal(target = globalThis) fills in document / Node / Element / HTMLElement / HTMLDocument /
window (a self-reference, like a real browser's) on target, using
browser-or-node to check whether a real DOM (a real browser, or a
jsdom-based test environment) is already there first - it's always safe to call, everywhere: it does nothing when
one is. Node / Element / HTMLElement / HTMLDocument are the classes themselves (not instances), so
x instanceof Element works, matching the real DOM's.
There's no real rendering to look at, so logElement(node, [label]) prints a readable, indented view of a node's
markup to the console (prettyPrint(node, [indent]), from the same module, returns the string instead of printing
it) - useful for watching pseudo-dom-driven code run, or checking a final result, from a terminal.
cloneNode(deep) copies an element with its attributes (objects such as style are copied too, not shared) and,
when deep, everything below it - without the parent or the event listenersisEqualNode compares two nodes by what they hold (tag, attributes in any order, text and children in order)compareDocumentPosition says where another node is (NodeService.DOCUMENT_POSITION_*)isConnected is true when the tree has a document at the top; ownerDocument says which one made the nodenormalize joins neighbouring text nodesPseudoText, PseudoComment); textContent works like the DOM's (the text of
everything below, and setting it replaces the children with a text node)