DOM Simulation API Reference
NogginLessDom provides a complete, zero-dependency DOM simulation for testing web applications without a browser. Every class and function described here is available from the top-level package import.
import {
Document, Element, Node, TextNode, Comment, DocumentFragment,
Event, createWindow,
} from '@asymmetric-effort/nogginlessdom';
Core Classes
Node
Base class for all DOM nodes. Provides tree manipulation, traversal, and comparison methods.
Node Type Constants
| Constant | Value |
Node.ELEMENT_NODE | 1 |
Node.TEXT_NODE | 3 |
Node.COMMENT_NODE | 8 |
Node.DOCUMENT_NODE | 9 |
Node.DOCUMENT_FRAGMENT_NODE | 11 |
Document Position Constants
| Constant | Value |
Node.DOCUMENT_POSITION_DISCONNECTED | 1 |
Node.DOCUMENT_POSITION_PRECEDING | 2 |
Node.DOCUMENT_POSITION_FOLLOWING | 4 |
Node.DOCUMENT_POSITION_CONTAINS | 8 |
Node.DOCUMENT_POSITION_CONTAINED_BY | 16 |
Properties
| Property | Type | Description |
nodeType | number | The node type constant |
nodeName | string | The node name (#text, #comment, tag name, etc.) |
childNodes | Node[] | Direct child nodes |
parentNode | `Node \ | null` | Parent node |
parentElement | `Element \ | null` | Parent element (null if parent is not an Element) |
ownerDocument | `Document \ | null` | The owning document |
isConnected | boolean | True if the node is in a document tree |
nodeValue | `string \ | null` | Node value (null for non-text nodes) |
textContent | string | Text content of the node and all descendants |
firstChild | `Node \ | null` | First child node |
lastChild | `Node \ | null` | Last child node |
nextSibling | `Node \ | null` | Next sibling node |
previousSibling | `Node \ | null` | Previous sibling node |
Methods
appendChild(child: Node): Node -- Appends a child node. Removes the child from its current parent if it has one. Fires connectedCallback on custom elements.
const div = document.createElement('div');
const span = document.createElement('span');
div.appendChild(span);
removeChild(child: Node): Node -- Removes a child node. Throws if the node is not a child. Fires disconnectedCallback on custom elements.
insertBefore(newChild: Node, referenceChild: Node | null): Node -- Inserts a node before a reference child. If referenceChild is null, appends to the end.
replaceChild(newChild: Node, oldChild: Node): Node -- Replaces an existing child with a new node.
contains(other: Node): boolean -- Returns true if other is a descendant of this node (or is this node itself).
hasChildNodes(): boolean -- Returns true if the node has children.
cloneNode(deep?: boolean): Node -- Creates a copy. If deep is true, clones all descendants.
normalize(): void -- Merges adjacent text nodes and removes empty text nodes.
isEqualNode(other: Node | null): boolean -- Deep-compares two nodes for structural equality.
isSameNode(other: Node | null): boolean -- Returns true if both references point to the same node.
compareDocumentPosition(other: Node): number -- Returns a bitmask describing the position of other relative to this node.
Element
Extends Node. Represents an HTML or SVG element with attributes, styling, events, and DOM manipulation capabilities.
Constructor
new Element(tagName: string, namespaceURI?: string | null)
Tag names are uppercased for HTML, preserved for SVG.
Properties
| Property | Type | Description |
tagName | string | Uppercase tag name (e.g. 'DIV') |
id | string | The element's ID |
className | string | Space-separated class names |
namespaceURI | `string \ | null` | Namespace URI |
innerHTML | string | HTML content of the element |
outerHTML | string (read-only) | HTML including the element itself |
classList | DOMTokenList | Token list for class manipulation |
style | CSSStyleDeclaration | Inline style object |
dataset | DOMStringMap | Data attribute access via data-* |
children | HTMLCollection | Child elements (not text/comment nodes) |
childElementCount | number | Number of child elements |
firstElementChild | `Element \ | null` | First child element |
lastElementChild | `Element \ | null` | Last child element |
nextElementSibling | `Element \ | null` | Next sibling element |
previousElementSibling | `Element \ | null` | Previous sibling element |
shadowRoot | `ShadowRoot \ | null` | Open shadow root, or null |
slot | string | Slot name for shadow DOM distribution |
assignedSlot | `Element \ | null` | The slot element this is assigned to |
tabIndex | number | Tab order (0 for interactive elements, -1 otherwise) |
contentEditable | string | 'true', 'false', or 'inherit' |
isContentEditable | boolean | Resolved editability |
Attribute Methods
element.setAttribute('href', '/page');
element.getAttribute('href'); // '/page'
element.hasAttribute('href'); // true
element.removeAttribute('href');
element.toggleAttribute('disabled'); // toggles presence
element.getAttributeNames(); // ['class', 'id', ...]
element.hasAttributes(); // true if any attributes exist
// Namespaced variants
element.setAttributeNS(ns, name, value);
element.getAttributeNS(ns, name);
element.removeAttributeNS(ns, name);
element.hasAttributeNS(ns, name);
Query Methods
element.querySelector('.active'); // first match or null
element.querySelectorAll('li.item'); // NodeList of all matches
element.getElementsByTagName('span'); // live HTMLCollection
element.getElementsByClassName('highlight'); // live HTMLCollection
element.closest('.wrapper'); // nearest ancestor matching selector
element.matches('.active'); // true if this element matches
DOM Manipulation
element.before(nodeA, nodeB); // insert before this element
element.after(nodeA); // insert after this element
element.prepend(child); // insert as first child
element.append(childA, childB); // append children
element.remove(); // remove from parent
element.replaceWith(newElement); // replace with another node
element.click(); // dispatch a click event
element.focus(); // dispatch focus event
element.blur(); // dispatch blur event
Layout and Geometry (Test Helpers)
All layout properties default to 0. Use setBoundingClientRect and setLayoutMetrics to configure values for testing.
element.setBoundingClientRect({ x: 10, y: 20, width: 100, height: 50 });
const rect = element.getBoundingClientRect();
// { x: 10, y: 20, width: 100, height: 50, top: 20, right: 110, bottom: 70, left: 10 }
element.setLayoutMetrics({
offsetWidth: 200, offsetHeight: 100,
clientWidth: 180, clientHeight: 80,
scrollWidth: 500, scrollHeight: 300,
});
element.scrollIntoView(); // no-op stub
element.scroll(x, y);
element.scrollTo(x, y);
element.scrollBy(dx, dy);
element.getClientRects(); // [getBoundingClientRect()]
Event Handler Properties
All standard on* event handler properties are supported:
onclick, ondblclick, onmousedown, onmouseup, onmousemove, onmouseover, onmouseout, onmouseenter, onmouseleave, onkeydown, onkeyup, onkeypress, onfocus, onblur, onchange, oninput, onsubmit, onreset, onscroll, onwheel, ondrag, ondragstart, ondragend, ondragover, ondragenter, ondragleave, ondrop, onload, onerror, onresize
element.onclick = (event) => { console.log('clicked'); };
TextNode
Extends Node with nodeType 3. Represents text content.
const text = new TextNode('Hello');
text.data; // 'Hello'
text.data = 'World'; // updates text; fires characterData mutation
text.textContent; // 'World'
text.nodeValue; // 'World'
text.cloneNode(); // new TextNode('World')
Comment
Extends Node with nodeType 8. Represents an HTML comment.
const comment = new Comment('TODO: fix this');
comment.data; // 'TODO: fix this'
comment.textContent; // 'TODO: fix this'
DocumentFragment
Extends Node with nodeType 11. A lightweight container for nodes that can be inserted into the DOM as a group.
const fragment = new DocumentFragment();
fragment.appendChild(new Element('p'));
fragment.querySelector('p'); // works
fragment.querySelectorAll('*'); // works
parent.appendChild(fragment); // transfers all children
Document
Extends Node with nodeType 9. The root of a DOM tree.
Properties
| Property | Type | Description |
readyState | string | Always 'complete' |
activeElement | `Element \ | null` | Currently focused element |
contentType | string | 'text/html' |
characterSet | string | 'UTF-8' |
URL | string | Document URL |
domain | string | Document domain |
referrer | string | Referrer URL |
defaultView | unknown | The parent Window |
visibilityState | string | 'visible' or 'hidden' |
hidden | boolean | True when visibilityState is 'hidden' |
cookie | string | Cookie string (full cookie jar) |
body | `Element \ | null` | The <body> element |
head | `Element \ | null` | The <head> element |
title | string | Content of the <title> element |
documentElement | `Element \ | null` | The <html> element |
customElements | CustomElementRegistry | Custom element registry |
Collection Properties
| Property | Description |
forms | All <form> elements |
images | All <img> elements |
links | All <a> and <area> elements with href |
scripts | All <script> elements |
embeds | All <embed> elements |
anchors | All <a> elements with name |
Factory Methods
document.createElement('div');
document.createElementNS('http://www.w3.org/2000/svg', 'circle');
document.createTextNode('hello');
document.createComment('note');
document.createDocumentFragment();
document.createRange();
document.createEvent('Event');
document.createTreeWalker(root, whatToShow, filter);
document.createNodeIterator(root, whatToShow, filter);
Query Methods
document.getElementById('app');
document.querySelector('#app .content');
document.querySelectorAll('button');
document.getElementsByTagName('div');
document.getElementsByClassName('active');
Utility Methods
document.adoptNode(externalNode); // removes from original tree
document.importNode(node, deep); // clone with optional deep copy
document.addEventListener('click', handler);
document.removeEventListener('click', handler);
document.dispatchEvent(event);
Test Helpers
document.setVisibility('hidden'); // changes visibilityState and fires event
HTML Elements
Form Elements
HTMLInputElement (input)
| Property | Type | Default | Description |
type | string | 'text' | Input type |
value | string | '' | Current value |
name | string | '' | Form control name |
disabled | boolean | false | Disabled state |
checked | boolean | false | Checked state (checkbox/radio) |
placeholder | string | '' | Placeholder text |
readOnly | boolean | false | Read-only state |
required | boolean | false | Required state |
min / max / step | string | '' | Numeric constraints |
pattern | string | '' | Regex validation pattern |
minLength / maxLength | number | -1 | Length constraints |
defaultValue | string | '' | Default value |
defaultChecked | boolean | false | Default checked |
willValidate | boolean | computed | True when not disabled |
validity | ValidityState | computed | Full validity state object |
validationMessage | string | '' | Custom validation message |
Validation methods: checkValidity(), reportValidity(), setCustomValidity(message)
Focus methods: focus(), blur(), select(), click()
const input = document.createElement('input');
input.type = 'email';
input.required = true;
input.value = 'invalid';
input.validity.typeMismatch; // true
input.validity.valid; // false
ValidityState
The validity property on form elements returns a ValidityState with these boolean properties: badInput, customError, patternMismatch, rangeOverflow, rangeUnderflow, stepMismatch, tooLong, tooShort, typeMismatch, valueMissing, valid.
HTMLButtonElement (button)
Properties: type ('submit', 'reset', 'button'), name, value, disabled.
HTMLSelectElement (select)
Properties: name, disabled, multiple, required, selectedIndex, value, options (array of HTMLOptionElement).
Validation: validity, checkValidity(), setCustomValidity().
const select = document.createElement('select');
const option = document.createElement('option');
option.value = 'a';
option.textContent = 'Option A';
select.appendChild(option);
select.value; // 'a'
HTMLOptionElement (option)
Properties: value, selected, disabled, defaultSelected, index, label, text.
HTMLTextAreaElement (textarea)
Properties: value, name, disabled, readOnly, required, placeholder, rows (default 2), cols (default 20), minLength, maxLength, defaultValue.
Validation: validity, checkValidity(), setCustomValidity().
HTMLFormElement (form)
Properties: action, method, enctype, target, name, elements (array of form controls).
Methods: submit(), reset().
const form = document.createElement('form');
const input = document.createElement('input');
input.name = 'email';
form.appendChild(input);
form.elements; // [input]
HTMLLabelElement (label)
Properties: htmlFor, form, control.
HTMLFieldSetElement (fieldset)
Properties: disabled, name.
HTMLOutputElement (output)
Properties: value (mapped to textContent), defaultValue, htmlFor.
Media Elements
HTMLImageElement (img)
Properties: src, alt, width, height, naturalWidth, naturalHeight, complete.
HTMLVideoElement (video)
Properties: src, controls, autoplay, loop, muted, width, height, currentTime, duration, paused, ended.
Methods: play() (returns Promise, sets paused = false), pause().
HTMLAudioElement (audio)
Same media properties and methods as HTMLVideoElement (without width/ height).
HTMLPictureElement (picture)
Container element. No additional properties.
HTMLSourceElement (source)
Properties: src, type, srcset, media.
Table Elements
HTMLTableElement (table)
Properties: rows (all TR descendants), tBodies, tHead, tFoot.
Methods: insertRow(index?), deleteRow(index), createTBody(), createTHead(), createTFoot().
const table = document.createElement('table');
const row = table.insertRow();
const cell = row.insertCell();
cell.textContent = 'Hello';
table.rows.length; // 1
HTMLTableRowElement (tr)
Properties: cells (array of HTMLTableCellElement), rowIndex.
Methods: insertCell(index?), deleteCell(index).
HTMLTableCellElement (td / th)
Properties: colSpan, rowSpan, cellIndex.
Interactive Elements
HTMLDialogElement (dialog)
Properties: open, returnValue.
Methods: show(), showModal(), close(returnValue?).
const dialog = document.createElement('dialog');
dialog.showModal();
dialog.open; // true
dialog.close('ok');
dialog.returnValue; // 'ok'
dialog.open; // false
HTMLDetailsElement (details)
Properties: open.
HTMLProgressElement (progress)
Properties: value, max.
HTMLMeterElement (meter)
Properties: value, min, max, low, high, optimum.
Other Elements
HTMLAnchorElement (a)
Properties: href, target, rel, download, hash, host, hostname, pathname, port, protocol, search, text.
HTMLCanvasElement (canvas)
Properties: width (default 300), height (default 150).
Methods: getContext('2d'), toDataURL(type?), toBlob(callback).
HTMLTemplateElement (template)
Properties: content (a DocumentFragment).
HTMLIFrameElement (iframe)
Properties: src, width, height, name, contentDocument, contentWindow.
HTMLScriptElement (script)
Properties: src, type, async, defer, text.
HTMLSlotElement (slot)
Properties: name.
Methods: assignedNodes(options?), assignedElements(options?).
HTMLLinkElement (link)
Properties: rel, href, type.
Automatically triggers stylesheet loading when rel="stylesheet" and a stylesheet loader is configured on the Window.
HTMLTimeElement (time)
Properties: dateTime.
SVG Elements
SVG elements are created with createElementNS using the SVG namespace.
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', '50');
circle.setAttribute('r', '25');
svg.appendChild(circle);
SVGElement (base class)
All SVG elements extend SVGElement, which sets namespaceURI to http://www.w3.org/2000/svg and preserves lowercase tag names.
Property: viewBox -- parses the viewBox attribute into { baseVal: { x, y, width, height } }.
Supported SVG Element Classes
| Class | Tag | Additional API |
SVGSVGElement | svg | createSVGRect(), createSVGPoint(), createSVGMatrix() |
SVGPathElement | path | -- |
SVGCircleElement | circle | cx, cy, r (as { baseVal: { value } }) |
SVGRectElement | rect | -- |
SVGLineElement | line | -- |
SVGTextElement | text | -- |
SVGGElement | g | -- |
SVGDefsElement | defs | -- |
SVGUseElement | use | href (as { baseVal }) |
CSS and Styling
CSSStyleDeclaration
Provides access to inline styles via a Proxy, supporting both camelCase property access and standard methods.
const el = document.createElement('div');
el.style.backgroundColor = 'red'; // camelCase setter
el.style.getPropertyValue('background-color'); // 'red'
el.style.setProperty('color', 'blue');
el.style.setProperty('font-size', '14px', 'important');
el.style.getPropertyPriority('font-size'); // 'important'
el.style.removeProperty('color'); // returns 'blue'
el.style.cssText; // 'background-color: red; font-size: 14px'
el.style.cssText = 'margin: 10px; padding: 5px';
el.style.length; // 2
el.style.item(0); // 'margin'
getComputedStyle
Window.getComputedStyle(element, pseudoElement?) returns a CSSStyleDeclaration with styles resolved from:
- Default display values based on tag name
- External stylesheets loaded via
configureStylesheetLoader <style> elements in the document- Inline styles
- Inherited properties from the ancestor chain
const win = createWindow();
const doc = win.document;
const style = doc.createElement('style');
style.textContent = '.box { color: red; font-size: 16px; }';
doc.head.appendChild(style);
const div = doc.createElement('div');
div.className = 'box';
doc.body.appendChild(div);
const computed = win.getComputedStyle(div);
computed.getPropertyValue('color'); // 'red'
computed.getPropertyValue('display'); // 'block' (default for div)
Specificity and Cascade
Rules are sorted by specificity [id-count, class-count, element-count] and then by source order. Higher specificity wins; for equal specificity, the later rule wins.
CSS Inheritance
The following properties are inherited from parent elements when not explicitly set: color, font-family, font-size, font-style, font-weight, line-height, text-align, text-indent, text-transform, visibility, cursor, direction, letter-spacing, word-spacing, white-space, list-style-type, list-style-position, list-style-image, border-collapse, border-spacing, caption-side, empty-cells, quotes, speak, orphans, widows.
const parent = doc.createElement('div');
parent.style.color = 'blue';
doc.body.appendChild(parent);
const child = doc.createElement('span');
parent.appendChild(child);
win.getComputedStyle(child).getPropertyValue('color'); // 'blue' (inherited)
CSS Variables (Custom Properties)
Custom properties (--*) are inherited through the DOM tree and resolved with var().
const style = doc.createElement('style');
style.textContent = `
:root { --primary: blue; }
.btn { color: var(--primary); }
.btn.alt { color: var(--accent, green); }
`;
doc.head.appendChild(style);
Variables resolve through ancestor chains: a var(--name, fallback) expression uses the fallback when the variable is undefined. Nested var() references are resolved iteratively up to 10 levels.
calc() Expressions
calc() expressions are evaluated when all operands share the same unit. Mixed-unit expressions are left as-is.
style.textContent = '.box { width: calc(100px + 50px); }';
// getComputedStyle resolves to '150px'
Supports +, -, *, / with units: px, em, rem, %, vh, vw, and more.
Pseudo-elements
::before and ::after pseudo-elements are supported in getComputedStyle.
style.textContent = '.icon::before { content: "X"; color: gold; }';
const pseudoStyle = win.getComputedStyle(icon, '::before');
pseudoStyle.getPropertyValue('content'); // '"X"'
pseudoStyle.getPropertyValue('color'); // 'gold'
@media Rules
Rules inside @media blocks are only applied when the media query matches the window's current state.
style.textContent = `
@media (min-width: 768px) { .sidebar { display: block; } }
@media (max-width: 767px) { .sidebar { display: none; } }
`;
External Stylesheets
Use configureStylesheetLoader to load external CSS files referenced by <link rel="stylesheet"> elements.
win.configureStylesheetLoader((href) => {
return '.external { color: purple; }';
});
const link = doc.createElement('link');
link.rel = 'stylesheet';
link.href = '/styles.css';
doc.head.appendChild(link);
!important Handling
Properties declared !important in stylesheets override non-important inline styles. Inline !important declarations override stylesheet !important.
style.textContent = '.box { color: red !important; }';
div.style.setProperty('color', 'blue');
// getComputedStyle returns 'red' (stylesheet !important wins)
div.style.setProperty('color', 'green', 'important');
// getComputedStyle returns 'green' (inline !important wins)
Shorthand Expansion
The following shorthand properties are automatically expanded to longhands:
margin / padding to -top, -right, -bottom, -leftborder to border-width, border-style, border-colorbackground to background-colorfont to font-weight, font-size, line-height, font-family
Events
Base Event Class
const event = new Event('click', {
bubbles: true,
cancelable: true,
composed: false,
});
Event Properties
| Property | Type | Description |
type | string | Event type name |
bubbles | boolean | Whether event bubbles |
cancelable | boolean | Whether default can be prevented |
composed | boolean | Whether event crosses shadow DOM boundaries |
isTrusted | boolean | Always false (synthetic events) |
timeStamp | number | Timestamp when created |
defaultPrevented | boolean | Whether preventDefault() was called |
target | `Node \ | null` | The event target |
currentTarget | `Node \ | null` | Current handler's target |
eventPhase | number | 0=NONE, 1=CAPTURING, 2=AT\_TARGET, 3=BUBBLING |
Event Methods
preventDefault() -- Prevents the default action (if cancelable)stopPropagation() -- Stops propagation to further listenersstopImmediatePropagation() -- Stops propagation and skips remaining listeners on the current targetcomposedPath() -- Returns the event path, crossing shadow boundaries if composed
Event Dispatch
Elements support the full W3C event dispatch model: capturing phase, target phase, and bubbling phase.
element.addEventListener('click', handler);
element.addEventListener('click', handler, { capture: true, once: true });
element.removeEventListener('click', handler);
element.dispatchEvent(new Event('click', { bubbles: true }));
Event Subclasses
CustomEvent
new CustomEvent('my-event', { bubbles: true, detail: { key: 'value' } });
// Properties: detail
MouseEvent
new MouseEvent('click', {
bubbles: true, cancelable: true,
clientX: 100, clientY: 200,
button: 0, buttons: 1,
altKey: false, ctrlKey: false, shiftKey: false, metaKey: false,
});
KeyboardEvent
new KeyboardEvent('keydown', {
key: 'Enter', code: 'Enter',
altKey: false, ctrlKey: true, shiftKey: false, metaKey: false,
repeat: false,
});
FocusEvent
Properties: relatedTarget.
InputEvent
Properties: data, inputType.
WheelEvent
Extends MouseEvent. Properties: deltaX, deltaY, deltaZ, deltaMode.
Constants: DOM_DELTA_PIXEL (0), DOM_DELTA_LINE (1), DOM_DELTA_PAGE (2).
PointerEvent
Extends MouseEvent. Properties: pointerId, pointerType, isPrimary, width, height, pressure, tiltX, tiltY, twist.
TouchEvent
Properties: touches, targetTouches, changedTouches (arrays of Touch objects with identifier, target, clientX, clientY, pageX, pageY, screenX, screenY).
DragEvent
Extends MouseEvent. Properties: dataTransfer (a DataTransfer instance).
ClipboardEvent
Properties: clipboardData (a DataTransfer instance).
TransitionEvent
Properties: propertyName, elapsedTime, pseudoElement.
AnimationEvent
Properties: animationName, elapsedTime, pseudoElement.
ErrorEvent
Properties: message, filename, lineno, colno, error.
MessageEvent
Properties: data, origin, source, ports.
StorageEvent
Properties: key, oldValue, newValue, url, storageArea.
PopStateEvent
Properties: state.
ProgressEvent
Properties: lengthComputable, loaded, total.
HashChangeEvent
Properties: oldURL, newURL.
PromiseRejectionEvent
Properties: promise, reason.
BeforeUnloadEvent
Properties: returnValue (settable string).
Selectors
NogginLessDom implements a CSS selector engine supporting:
- Tag name:
div, span, * - ID:
#my-id - Class:
.my-class - Attribute selectors:
[attr], [attr=value], [attr~=value], [attr|=value], [attr^=value], [attr$=value], [attr*=value] - Pseudo-classes:
:first-child, :last-child, :nth-child(n), :not(selector) - Combinators: descendant (space), child (
>) - Comma-separated lists:
div, span - Compound selectors:
div.class#id[attr]
document.querySelector('div.container > p:first-child');
document.querySelectorAll('input[type="text"], textarea');
element.matches('.active:not(.disabled)');
element.closest('form');
Observers
MutationObserver
Observes changes to the DOM tree: child additions/removals, attribute changes, and text content changes.
const observer = new MutationObserver((mutations) => {
for (const record of mutations) {
console.log(record.type, record.target);
}
});
observer.observe(element, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
attributeFilter: ['class', 'id'],
});
// Mutations are delivered asynchronously via microtask
element.setAttribute('class', 'new');
await Promise.resolve(); // mutations delivered
observer.takeRecords(); // get pending records synchronously
observer.disconnect(); // stop observing
MutationRecord Properties
| Property | Type | Description |
type | `'attributes' \ | 'characterData' \ | 'childList'` | Mutation type |
target | Node | The mutated node |
addedNodes | Node[] | Nodes added (childList) |
removedNodes | Node[] | Nodes removed (childList) |
previousSibling | `Node \ | null` | Previous sibling of added/removed node |
nextSibling | `Node \ | null` | Next sibling of added/removed node |
attributeName | `string \ | null` | Changed attribute name |
oldValue | `string \ | null` | Previous value (when old-value tracking enabled) |
IntersectionObserver
Tracks element visibility. Since there is no layout engine, use the triggerIntersection helper to simulate intersections.
import { triggerIntersection } from '@asymmetric-effort/nogginlessdom';
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(entry.target, entry.isIntersecting, entry.intersectionRatio);
});
}, {
root: null,
rootMargin: '0px',
threshold: [0, 0.5, 1],
});
observer.observe(element);
triggerIntersection(observer, [
{ target: element, isIntersecting: true, intersectionRatio: 0.75 },
]);
observer.takeRecords();
observer.unobserve(element);
observer.disconnect();
ResizeObserver
Tracks element size changes. Use triggerResize to simulate.
import { triggerResize } from '@asymmetric-effort/nogginlessdom';
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log(entry.contentRect.width, entry.contentRect.height);
console.log(entry.borderBoxSize, entry.contentBoxSize);
}
});
observer.observe(element, { box: 'content-box' });
// Single-target form
triggerResize(observer, element, {
x: 0, y: 0, width: 200, height: 100,
top: 0, right: 200, bottom: 100, left: 0,
});
// Array form with partial entries
triggerResize(observer, [
{ target: element, contentRect: { width: 300, height: 150 } },
]);
observer.unobserve(element);
observer.disconnect();
Fetch API
The Window.fetch method defaults to making real HTTP/HTTPS requests via node:http/node:https. For testing, configure a mock handler.
const win = createWindow();
// Configure a mock fetch handler
win.configureFetch((url, options) => {
return new Response(JSON.stringify({ id: 1 }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
});
const response = await win.fetch('/api/users');
const data = await response.json();
Request
const req = new Request('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"key":"value"}',
});
req.url; // '/api'
req.method; // 'POST'
req.headers; // Map { 'Content-Type' => 'application/json' }
req.clone(); // new Request with same properties
// Body methods (can only be called once)
await req.json();
await req.text();
await req.blob();
await req.arrayBuffer();
await req.formData();
Response
const res = new Response('body text', {
status: 201,
statusText: 'Created',
headers: { 'X-Custom': 'value' },
});
res.status; // 201
res.ok; // true (200-299)
res.statusText; // 'Created'
res.headers; // Map
res.bodyUsed; // false
res.clone(); // new Response
// Body methods (can only be called once)
await res.json();
await res.text();
await res.blob();
await res.arrayBuffer();
await res.formData();
Headers
A standard Headers class wrapping Map<string, string>.
FormData
A standard FormData implementation with append, get, getAll, has, set, delete, entries, keys, values, and forEach.
Blob
Re-exported from globalThis.Blob.
XMLHttpRequest
Simulates the full XMLHttpRequest lifecycle. Configure a handler via Window.configureXHR.
State Constants
| Constant | Value |
XMLHttpRequest.UNSENT | 0 |
XMLHttpRequest.OPENED | 1 |
XMLHttpRequest.HEADERS_RECEIVED | 2 |
XMLHttpRequest.LOADING | 3 |
XMLHttpRequest.DONE | 4 |
Usage
const win = createWindow();
win.configureXHR(async (request) => {
return {
status: 200,
statusText: 'OK',
headers: { 'content-type': 'application/json' },
body: '{"result": true}',
};
});
const xhr = new win.XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.responseType = 'json';
xhr.onload = function () {
console.log(this.response); // { result: true }
};
xhr.send();
Properties
readyState, status, statusText, responseText, response, responseType ('', 'text', 'json', 'arraybuffer'), responseURL, timeout, withCredentials.
Event Handlers
onreadystatechange, onload, onerror, onabort, ontimeout, onloadstart, onloadend, onprogress.
Methods
open(method, url), send(body?), abort(), setRequestHeader(name, value), getResponseHeader(name), getAllResponseHeaders(), addEventListener(type, listener), removeEventListener(type, listener).
WebSocket
Simulates WebSocket connections for testing. Configure a handler via Window.configureWebSocket.
State Constants
| Constant | Value |
WebSocket.CONNECTING | 0 |
WebSocket.OPEN | 1 |
WebSocket.CLOSING | 2 |
WebSocket.CLOSED | 3 |
Usage
const win = createWindow();
win.configureWebSocket({
onOpen: () => console.log('opened'),
send: (data) => console.log('sent:', data),
onClose: (code, reason) => console.log('closed', code, reason),
});
const ws = new win.WebSocket('ws://localhost:8080');
ws.onmessage = (event) => console.log(event.data);
// Simulate server events
ws.simulateOpen();
ws.simulateMessage('hello from server');
ws.simulateClose(1000, 'normal');
ws.simulateError(new Error('connection lost'));
Properties
readyState, url, protocol, bufferedAmount, extensions, binaryType.
Event Handlers
onopen, onmessage, onclose, onerror.
Test Helpers
simulateOpen() -- Sets state to OPEN and fires open eventsimulateMessage(data) -- Fires message event with datasimulateClose(code?, reason?) -- Sets state to CLOSED and fires close eventsimulateError(error?) -- Fires error event
WebSocket Events
WSMessageEvent -- extends Event with data: stringCloseEvent -- extends Event with code, reason, wasClean
IndexedDB
In-memory implementation of the IndexedDB API, available as window.indexedDB.
IDBFactory
const win = createWindow();
const request = win.indexedDB.open('mydb', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
// use database...
};
Methods: open(name, version?), deleteDatabase(name), databases().
IDBDatabase
Properties: name, version, objectStoreNames.
Methods: createObjectStore(name, options?), deleteObjectStore(name), transaction(storeNames, mode?), close().
IDBTransaction
Properties: db, mode ('readonly', 'readwrite', 'versionchange'), error, objectStoreNames.
Methods: objectStore(name), abort().
Events: oncomplete, onabort, onerror.
IDBObjectStore
Properties: name, keyPath, autoIncrement, indexNames.
Methods: add(value, key?), put(value, key?), get(key), getAll(query?, count?), delete(key), clear(), count(query?), createIndex(name, keyPath, options?), deleteIndex(name), index(name), openCursor(query?, direction?).
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.add({ id: 1, name: 'Alice', age: 30 });
store.put({ id: 1, name: 'Alice', age: 31 });
const getReq = store.get(1);
getReq.onsuccess = () => console.log(getReq.result);
IDBIndex
Properties: name, keyPath, unique, multiEntry.
Methods: get(key), getAll(query?, count?), count(query?), openCursor(query?, direction?).
IDBCursor
Properties: source, direction, key, primaryKey, value.
Methods: continue(), advance(count), delete(), update(value).
Directions: 'next', 'nextunique', 'prev', 'prevunique'.
IDBKeyRange
Static factory methods for defining key ranges:
IDBKeyRange.only(42);
IDBKeyRange.lowerBound(10);
IDBKeyRange.lowerBound(10, true); // open (exclusive)
IDBKeyRange.upperBound(100);
IDBKeyRange.bound(10, 100, false, true);
range.includes(50); // true
IDBRequest and IDBOpenDBRequest
Properties: result, error, source, transaction, readyState.
Events: onsuccess, onerror. IDBOpenDBRequest adds onupgradeneeded and onblocked.
Canvas
CanvasRenderingContext2D
A stub that records all draw calls for test inspection.
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 300;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ff0000';
ctx.fillRect(10, 10, 100, 50);
ctx.beginPath();
ctx.arc(200, 150, 50, 0, Math.PI * 2);
ctx.fill();
// Inspect recorded draw calls
const calls = ctx.__getDrawCalls();
// [
// { method: 'fillRect', args: [10, 10, 100, 50] },
// { method: 'beginPath', args: [] },
// { method: 'arc', args: [200, 150, 50, 0, 6.28..., undefined] },
// { method: 'fill', args: [] },
// ]
ctx.__clearDrawCalls(); // reset for next assertions
State Properties
fillStyle, strokeStyle, lineWidth, lineCap, lineJoin, font, textAlign, textBaseline, globalAlpha, globalCompositeOperation.
Path Methods
beginPath(), closePath(), moveTo(x, y), lineTo(x, y), arc(x, y, r, start, end, ccw?), arcTo(x1, y1, x2, y2, r), bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y), quadraticCurveTo(cpx, cpy, x, y), rect(x, y, w, h), ellipse(x, y, rx, ry, rotation, start, end, ccw?).
Drawing Methods
fill(), stroke(), fillRect(x, y, w, h), strokeRect(x, y, w, h), clearRect(x, y, w, h), fillText(text, x, y, maxWidth?), strokeText(text, x, y, maxWidth?), drawImage(...), clip().
Measurement
measureText(text) -- Returns { width: text.length * 10 }.
Image Data
createImageData(w, h), getImageData(sx, sy, sw, sh), putImageData(data, dx, dy).
CanvasGradient
const grad = ctx.createLinearGradient(0, 0, 100, 0);
grad.addColorStop(0, 'red');
grad.addColorStop(1, 'blue');
ctx.fillStyle = grad;
grad.__getStops(); // [{ offset: 0, color: 'red' }, { offset: 1, color: 'blue' }]
Also: createRadialGradient(x0, y0, r0, x1, y1, r1).
CanvasPattern
createPattern(image, repetition) -- Returns a CanvasPattern stub.
ImageData
const imageData = new ImageData(100, 100);
imageData.width; // 100
imageData.height; // 100
imageData.data; // Uint8ClampedArray of length 40000
Transform Methods
translate(x, y), rotate(angle), scale(x, y), transform(a, b, c, d, e, f), setTransform(a, b, c, d, e, f), resetTransform().
State Stack
save() and restore() push/pop the full drawing state (fill, stroke, line, font, alpha, composite operation).
Web Animations
element.animate()
const animation = element.animate(
[
{ opacity: '0', transform: 'translateY(-10px)' },
{ opacity: '1', transform: 'translateY(0)' },
],
{ duration: 300, easing: 'ease-in', fill: 'forwards' },
);
animation.playState; // 'running'
animation.currentTime; // 0
animation.pause();
animation.playState; // 'paused'
animation.finish(); // jumps to end, fires onfinish
animation.cancel(); // resets, fires oncancel
animation.reverse(); // reverses playbackRate
Animation
| Property | Type | Description |
id | string | Animation ID |
playState | string | 'idle', 'running', 'paused', 'finished' |
currentTime | `number \ | null` | Current playback time |
startTime | `number \ | null` | Start time |
playbackRate | number | Playback speed multiplier |
effect | `KeyframeEffect \ | null` | The animation effect |
timeline | `AnimationTimeline \ | null` | The timeline |
onfinish | callback | Fired when animation finishes |
oncancel | callback | Fired when animation is cancelled |
finished | Promise<Animation> | Resolves when finished |
ready | Promise<Animation> | Resolves when ready to play |
Methods: play(), pause(), cancel(), finish(), reverse().
KeyframeEffect
const effect = new KeyframeEffect(element, keyframes, {
duration: 500,
delay: 100,
easing: 'ease-out',
iterations: 3,
direction: 'alternate',
fill: 'both',
});
effect.getKeyframes();
effect.setKeyframes(newKeyframes);
effect.getTiming();
effect.updateTiming({ duration: 1000 });
Workers
Worker
const worker = new Worker('/worker.js', { type: 'module', name: 'my-worker' });
worker.onmessage = (event) => console.log(event.data);
worker.onerror = (event) => console.error(event.message);
worker.postMessage({ cmd: 'start' });
worker.terminate();
// Test helpers
worker.simulateMessage({ result: 42 });
worker.simulateError(new Error('worker failed'));
SharedWorker
const shared = new SharedWorker('/shared.js', { name: 'cache' });
shared.port.start();
shared.port.onmessage = (event) => console.log(event.data);
shared.port.postMessage('ping');
// Test helper
shared.port.simulateMessage('pong');
shared.port.close();
MessagePort
Properties: onmessage, onmessageerror.
Methods: postMessage(data), start(), close(), addEventListener(...), removeEventListener(...).
Test helper: simulateMessage(data).
ServiceWorker and ServiceWorkerContainer
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
registration.active; // ServiceWorker instance
registration.scope; // '/'
await registration.unregister();
await registration.update();
await navigator.serviceWorker.getRegistration('/');
await navigator.serviceWorker.getRegistrations();
await navigator.serviceWorker.ready; // resolves with first registration
Window
createWindow(options?) is the primary entry point for creating a full DOM environment.
import { createWindow } from '@asymmetric-effort/nogginlessdom';
const win = createWindow({
innerWidth: 1024,
innerHeight: 768,
screenWidth: 1920,
screenHeight: 1080,
devicePixelRatio: 2,
colorScheme: 'dark',
reducedMotion: false,
});
Window Properties
| Property | Type | Description |
document | Document | The document (with html/head/body structure) |
location | Location | Location stub |
history | History | History with push/pop/replace state |
navigator | Navigator | Navigator stub |
localStorage | Storage | In-memory storage (5 MB quota) |
sessionStorage | Storage | In-memory storage (5 MB quota) |
innerWidth / innerHeight | number | Viewport dimensions |
performance | Performance | Performance API |
screen | object | Screen stub with width, height, etc. |
devicePixelRatio | number | DPR |
scrollX / scrollY | number | Scroll position |
pageXOffset / pageYOffset | number | Scroll position aliases |
indexedDB | IDBFactory | IndexedDB factory |
CSS | object | CSS.supports(prop, val) |
console | Console | Re-exported globalThis.console |
Web API Properties on Window
FormData, Headers, Request, Response, TextEncoder, TextDecoder, Blob, atob, btoa, structuredClone, DOMParser, XMLSerializer, queueMicrotask, AbortController, AbortSignal, crypto, Worker, SharedWorker.
Storage
In-memory implementation with quota enforcement and StorageEvent dispatch.
win.localStorage.setItem('key', 'value');
win.localStorage.getItem('key'); // 'value'
win.localStorage.removeItem('key');
win.localStorage.clear();
win.localStorage.length; // 0
win.localStorage.key(0); // null
// StorageEvent fires on the Window when values change
win.addEventListener('storage', (e) => {
console.log(e.key, e.oldValue, e.newValue);
});
Quota: 5 MB (UTF-16). Throws a QuotaExceededError when exceeded.
Navigator
Properties: userAgent, language, languages, onLine, cookieEnabled, platform, vendor, hardwareConcurrency, maxTouchPoints, clipboard, permissions, serviceWorker.
Methods: sendBeacon(url, data?), vibrate(pattern).
Clipboard
await win.navigator.clipboard.writeText('hello');
await win.navigator.clipboard.readText(); // 'hello'
Permissions
const result = await win.navigator.permissions.query({ name: 'clipboard-read' });
result.state; // 'granted' (always)
History
win.history.pushState({ page: 1 }, '', '/page/1');
win.history.replaceState({ page: 2 }, '', '/page/2');
win.history.back(); // fires popstate
win.history.forward(); // fires popstate
win.history.go(-2); // fires popstate
win.history.length;
win.history.state;
Location
Properties: href, hash, pathname, search, origin, protocol, host, hostname, port.
Methods: assign(url), replace(url), reload().
matchMedia
const mql = win.matchMedia('(min-width: 768px)');
mql.matches; // true if innerWidth >= 768
mql.media; // '(min-width: 768px)'
mql.addEventListener('change', (e) => {
console.log(e.matches, e.media);
});
// Changing dimensions fires change events on affected MediaQueryLists
win.setDimensions(500, 400);
Supported media features: min-width, max-width, min-height, max-height, prefers-color-scheme, prefers-reduced-motion, media types (all, screen, print).
Performance API
win.performance.now(); // ms since creation
win.performance.timeOrigin; // creation timestamp
win.performance.mark('start');
win.performance.mark('end');
win.performance.measure('duration', 'start', 'end');
win.performance.getEntries();
win.performance.getEntriesByName('start');
win.performance.getEntriesByType('mark');
win.performance.clearMarks('start');
win.performance.clearMeasures();
PerformanceObserver
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
console.log(entries);
});
observer.observe({ entryTypes: ['mark', 'measure'] });
observer.disconnect();
observer.takeRecords();
requestAnimationFrame
const id = win.requestAnimationFrame((timestamp) => {
console.log('frame at', timestamp);
});
win.cancelAnimationFrame(id);
// Test helper: synchronously flush all queued callbacks
win.flushAnimationFrames();
requestIdleCallback
const id = win.requestIdleCallback((deadline) => {
console.log(deadline.timeRemaining(), deadline.didTimeout);
});
win.cancelIdleCallback(id);
// Test helper: synchronously flush all queued callbacks
win.flushIdleCallbacks();
postMessage
win.addEventListener('message', (event) => {
console.log(event.data, event.origin);
});
win.postMessage({ type: 'init' }, '*');
// Delivered asynchronously via queueMicrotask
Page Visibility
win.document.visibilityState; // 'visible'
win.document.hidden; // false
win.document.setVisibility('hidden');
// Fires 'visibilitychange' event on document
Timer Methods
setTimeout, setInterval, clearTimeout, clearInterval delegate to globalThis. Use fake timers from the mocking module to control them.
Configuration Methods
win.configureFetch(handler);
win.configureXHR(handler);
win.configureWebSocket(handler);
win.configureStylesheetLoader(loader);
ARIA and Accessibility
Property Accessors
All standard ARIA property accessors are implemented as getters/setters that map to the corresponding aria-* attributes:
element.ariaLabel = 'Close dialog';
element.getAttribute('aria-label'); // 'Close dialog'
element.ariaHidden = 'true';
element.ariaDisabled = 'true';
element.ariaExpanded = 'false';
element.ariaSelected = 'true';
element.ariaChecked = 'mixed';
element.ariaRequired = 'true';
element.ariaPressed = 'true';
element.ariaLive = 'polite';
element.ariaDescribedBy = 'desc-id';
element.ariaLabelledBy = 'label-id';
element.ariaControls = 'panel-id';
element.ariaValueNow = '50';
element.ariaValueMin = '0';
element.ariaValueMax = '100';
element.ariaValueText = 'fifty percent';
Role
element.role = 'button';
element.getAttribute('role'); // 'button'
Implicit Roles
getImplicitRole(element) returns the implicit ARIA role based on the tag name and attributes. An explicit role attribute always takes precedence.
import { getImplicitRole } from '@asymmetric-effort/nogginlessdom';
getImplicitRole(button); // 'button'
getImplicitRole(nav); // 'navigation'
getImplicitRole(header); // 'banner'
getImplicitRole(footer); // 'contentinfo'
getImplicitRole(main); // 'main'
// Context-dependent:
getImplicitRole(anchor); // null (no href)
anchor.setAttribute('href', '/');
getImplicitRole(anchor); // 'link'
input.setAttribute('type', 'checkbox');
getImplicitRole(input); // 'checkbox'
Pointer Capture
element.setPointerCapture(1);
element.hasPointerCapture(1); // true
element.releasePointerCapture(1);
element.hasPointerCapture(1); // false
Drag and Drop
DataTransfer
const dt = new DataTransfer();
dt.setData('text/plain', 'Hello');
dt.setData('text/html', '<b>Hello</b>');
dt.getData('text/plain'); // 'Hello'
dt.types; // ['text/plain', 'text/html']
dt.clearData('text/html');
dt.clearData(); // clears all
dt.dropEffect = 'copy';
dt.effectAllowed = 'copyMove';
dt.setDragImage(element, 0, 0); // no-op
DataTransferItem
dt.items.length; // number of items
dt.items.add('data', 'text/plain');
const item = dt.items.get(0);
item.kind; // 'string'
item.type; // 'text/plain'
item.getAsString((data) => console.log(data));
item.getAsFile(); // null (files not supported)
dt.items.remove(0);
dt.items.clear();
DragEvent
const dragEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer: dt,
});
element.dispatchEvent(dragEvent);
HTML Parser
parseHTML
Parses HTML strings into DOM nodes. Used internally by innerHTML setter.
import { parseHTML } from '@asymmetric-effort/nogginlessdom';
const nodes = parseHTML('<div class="box"><p>Hello</p></div>');
Supported features:
- Standard HTML elements and attributes (quoted and unquoted values)
- Void elements (br, img, input, etc.)
- Self-closing tags
- Raw text elements (script, style, textarea) -- content is not parsed as HTML
- HTML entities:
&, <, >, ", ', , ©, ®, ™, —, –, «, », •, …, €, £, ¥, ¢ - Numeric entities:
{, 💩 - DOCTYPE declarations (skipped)
- HTML comments (skipped)
- Maximum nesting depth of 5000 to prevent stack overflow
Shadow DOM and Custom Elements
Shadow DOM
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot></slot>';
shadow.getElementById('id');
shadow.querySelector('.cls');
shadow.querySelectorAll('*');
host.shadowRoot; // shadow (open mode)
// Closed mode: host.shadowRoot returns null
ShadowRoot extends Node and supports innerHTML, getElementById, querySelector, and querySelectorAll.
Events with composed: true cross shadow boundaries during dispatch. Non-composed events stop at the shadow root.
Custom Elements
class MyButton extends Element {
static observedAttributes = ['variant'];
connectedCallback() { /* called on appendChild */ }
disconnectedCallback() { /* called on removeChild */ }
attributeChangedCallback(name, oldVal, newVal) { /* called on setAttribute */ }
constructor() {
super('my-button');
}
}
document.customElements.define('my-button', MyButton);
document.customElements.get('my-button'); // MyButton
document.customElements.getName(MyButton); // 'my-button'
await document.customElements.whenDefined('my-button'); // resolves immediately
const btn = document.createElement('my-button'); // uses registered constructor
Slots
const slot = document.createElement('slot');
slot.name = 'header';
slot.assignedNodes(); // nodes distributed to this slot
slot.assignedElements(); // elements only
Collections and Utilities
NodeList
Returned by querySelectorAll. Supports length, [index], and forEach.
HTMLCollection
Returned by getElementsByTagName, getElementsByClassName, and children. Live collection: re-evaluates on each access.
DOMTokenList
Returned by element.classList. Methods: add(token), remove(token), toggle(token, force?), contains(token), replace(old, new), item(index), toString().
element.classList.add('active', 'visible');
element.classList.remove('hidden');
element.classList.toggle('selected');
element.classList.contains('active'); // true
TreeWalker and NodeIterator
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
(node) => NodeFilter.FILTER_ACCEPT,
);
while (walker.nextNode()) {
console.log(walker.currentNode);
}
walker.previousNode();
walker.firstChild();
walker.lastChild();
walker.nextSibling();
walker.previousSibling();
walker.parentNode();
NodeFilter Constants
| Constant | Value |
NodeFilter.SHOW_ALL | 0xFFFFFFFF |
NodeFilter.SHOW_ELEMENT | 0x1 |
NodeFilter.SHOW_TEXT | 0x4 |
NodeFilter.SHOW_COMMENT | 0x80 |
NodeFilter.FILTER_ACCEPT | 1 |
NodeFilter.FILTER_REJECT | 2 |
NodeFilter.FILTER_SKIP | 3 |