Use the call arena only for the AbortSignal dependent list that must survive re-entrant abort handlers, and move scratch-only DOM reads to the local arena. Store BeforeUnloadEvent.returnValue in the event-owned arena so it remains valid without accumulating in the longer-lived frame arena.
These allocator changes bound temporary memory to the shortest safe lifetime while preserving existing Web API behavior. Add coverage for replacing BeforeUnloadEvent.returnValue.
Keep one slab-allocated scheduler task per active element scroll sequence and recycle it on completion or scheduler cancellation. This prevents repeated programmatic scrolling from growing the frame arena and task queue without bound.
Advance the throttle state before dispatching scroll and scrollend so nested JavaScript callbacks cannot be overwritten, and avoid scheduling events for clamped no-op scrollBy calls. Add coverage for coalescing, both re-entrant event paths, and no-op scrolling.
After rebasing onto main, WPT
/dom/events/Event-dispatch-single-activation-behavior.html went from
3/132 to 0/0 (harness never reported): the test repeatedly clicks
`<a href="#link">` anchors and resets with `location.hash = ""`, and
both patterns ended up scheduling full same-URL reloads, putting the
page in a reload loop. As with the previous commit, the pre-rebase
code masked this because queued navigations rarely executed; the
HttpClient refactor made them reliable.
Two fixes, per the HTML navigate steps (a navigation is a fragment
navigation when the URL equals the document's URL excluding fragments
and its fragment is non-null) and the Location hash setter (if the
fragment doesn't change, no navigation happens):
- Frame.scheduleNavigationWithArena: re-navigating to the exact
current URL is only a reload when the URL has no fragment. With a
fragment (e.g. re-clicking the same `#link` anchor) it's now a
no-op fragment navigation: no reload, and since the fragment didn't
change, no hashchange and no history entry. Upstream's "identical
URL reloads" behavior (iframe.src = "about:blank" twice) is
untouched — those URLs carry no fragment.
- Location.setHash: skip the navigation entirely when the normalized
fragment equals the current one, so `location.hash = ""` on a
fragment-less URL (and re-assigning the current fragment) no longer
schedules a same-URL reload. Assigning "" still strips the fragment
from the URL, matching the existing tests/window/location.html
expectations.
Unit test added in tests/window/location.html: href stays stable
across repeated `hash = ""` and same-fragment re-assignments.
Coverage: /dom/events/Event-dispatch-single-activation-behavior.html
0/0 -> 83/132 (3/132 before the rebase; the gain past 3 comes from
the handler-return-value fix in the previous commit).
After rebasing onto main (HttpClient refactor: queued navigations now
reliably execute), WPT /dom/events/Event-dispatch-click.html went from
33/33 to 0/0: its form subtests cancel submission with
`form.onsubmit = () => { ...; return false }`, but the inline-handler
invocation discarded the return value, so the form really submitted,
the scheduled form navigation reloaded the page in a loop and the
harness never reported. Before the refactor the queued navigation
never ran, which masked the missing behavior.
Per the HTML "event handler processing algorithm" (§8.1.8.1, "Process
the return value"), an event handler attached via an on* property or
attribute that returns exactly false cancels the event — unlike
addEventListener listeners, whose return value is ignored. The
inverted onerror special case only applies to the global's onerror,
which is invoked via Window.reportError, not through node dispatch.
Implementation: the two inline-handler invocation sites in
EventManager.dispatchNode (at-target and bubble phase) now call the
handler with a js.Value return type and route it through a new
processHandlerReturnValue helper, which calls event.preventDefault()
when the returned value is strictly false (skipping "error" events).
Unit test added in tests/events.html covering: at-target handler,
ancestor (bubble-phase) handler, other falsy return values not
canceling, and addEventListener return values staying ignored.
Coverage: /dom/events/Event-dispatch-click.html 0/0 -> 33/33
(restores the previously claimed 33/33); also a prerequisite for
/dom/events/Event-dispatch-single-activation-behavior.html (see next
commit).
Fixes the failing subtest of WPT
/dom/nodes/moveBefore/custom-element-move-reactions.html (5/6 -> 6/6):
when a moved custom element defines no connectedMoveCallback, the
fallback is disconnectedCallback + connectedCallback - and either may
be undefined. The error from invoking the missing disconnectedCallback
returned early and skipped connectedCallback entirely.
Both fallback invocations now tolerate a missing method and let the
other one run.
Coverage: /dom/nodes/moveBefore/custom-element-move-reactions.html
5/6 -> 6/6 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 10 failing files under /dom/nodes/insertion-removing-steps/
(5/20 -> 15/20 files passing), which pin down when dynamically inserted
scripts execute:
- Scripts (and iframes/links/styles) appended to a DETACHED parent no
longer activate; the ready work is gated on the node being connected,
and runs later if the subtree gets inserted into the document.
- Inserting a subtree runs the ready work for its descendant elements
too, in tree order (nodeIsReadySubtree), after the node is in place.
- Multi-node insertions (fragments, and ParentNode.append/prepend with
several arguments, which now convert to a fragment per spec) run all
the ready work only after every node is inserted: an earlier script
observes its later siblings connected, and can remove one to prevent
it from running.
- Inserting into a not-yet-started connected script runs the script's
children-changed steps (once, with the full content) before the
inserted nodes' own ready work, per whatwg/html#10188: the outer
script executes before an inner one.
- Mutation records queue before any script runs, so an inserted script
observes its own insertion record via takeRecords. The script
manager's pre-execution microtask drain now only happens at a real
checkpoint (empty JS stack): a script executed synchronously from a
running script must not flush the queue mid-task.
The remaining files in the directory need meta-referrer /
meta-default-style / media <source> insertion steps and style/iframe
load interleavings.
Coverage: /dom/nodes/insertion-removing-steps/: 15/20 files passing
(10 newly); /dom/nodes and /dom/events regressions clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes two WPT files:
- /html/dom/documents/resource-metadata-management/document-cookie.html
(4/5 -> 5/5, fully green): a cookie-averse document (one without a
browsing context, e.g. createHTMLDocument) must read document.cookie
as the empty string and silently ignore writes. Both accessors now
no-op for documents that aren't a frame's active document.
- /html/dom/access-key-label.html (1/2 -> 2/2, fully green):
HTMLElement.accessKeyLabel was missing. It reports an Alt+ chord for
a valid single-character accesskey, like Chromium, and the empty
string otherwise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the last failing subtest of WPT /html/dom/elements/
the-innertext-and-outertext-properties/outertext-setter.html
(42/43 -> 43/43): outerText is an HTMLElement property, so assigning it
on a MathML <math> element must not replace the element.
MathML elements currently share the HTML wrapper types (giving them a
plain Element wrapper is part of the pending element-interface work),
so they expose the outerText setter; it now stays inert for non-HTML
namespaces.
Coverage: outertext-setter.html 42/43 -> 43/43 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /html/dom/documents/resource-metadata-management/
document-lastModified.html (0/1 -> 1/1) and document-lastModified-01.html
(0/3 -> 3/3): document.lastModified was missing entirely.
The getter reports the navigation response's Last-Modified header
(already captured on the frame), parsed as an RFC 822 date and
formatted in the user's local time zone as "MM/DD/YYYY hh:mm:ss" via
libc localtime_r, matching what scripts derive from `new Date(...)`.
Documents without such a header - including script-created ones, which
don't inherit the frame's headers - report the current time, as the
spec requires.
Coverage: both files above fully green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes two WPT files (1 subtest each, both fully green):
- /html/dom/documents/dom-tree-accessors/document.head-02.html:
document.head is the first html-namespace child of the document
element whose LOCAL name is head. A createElementNS(HTML_NS,
"blah:head") element is an HTMLUnknownElement in our type system, so
the old is(Html.Head) walk skipped it; getHead now matches on
namespace + local name (and returns *Element accordingly).
- .../document.getElementsByName/document.getElementsByName-namespace.html:
getElementsByName must only consider HTML-namespace elements; the
live collection's name filter now rejects foreign elements, matching
the earlier fix for name-based access on HTMLCollections.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/nodes/querySelector-mixed-case.html: per the Selectors
spec, attribute names in selectors match ASCII case-insensitively
against HTML elements (in an HTML document) but case-sensitively
against foreign (SVG/MathML) elements. We lowercased the selector's
attribute name at parse time for everyone, so [viewBox] never matched
the case-preserved viewBox attribute on an SVG element.
The parsed attribute selector now also keeps the name as written;
matching picks the lowercased name for HTML elements (whose stored
attributes are normalized) and the original for foreign elements
(whose attributes are stored as written).
Coverage: /dom/nodes/querySelector-mixed-case.html 0/1 -> 1/1 (fully
green); Element-matches.html stays 669/669.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the last failing subtest of WPT
/dom/lists/DOMTokenList-coverage-for-attributes.html (174/175 ->
175/175): an <a> element in the SVG namespace must expose relList as a
DOMTokenList (while SVG <area>/<link> have none).
Main now provides the full SVGAElement type (SVGGraphicsElement
hierarchy, href as SVGAnimatedString); this commit only adds the
relList accessor, reflecting the rel attribute through the shared
Element.getRelList.
(As originally written, this commit also created the SVGAElement type
and its factory/tag wiring; the rebase keeps main's implementation.)
Coverage: /dom/lists/DOMTokenList-coverage-for-attributes.html 174/175
-> 175/175 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 7 failing files under /html/dom/elements/global-attributes/
(the-translate-attribute-007 through -012 and
translate-enumerated-ascii-case-insensitive, 1 subtest each): the
HTMLElement.translate property was missing entirely.
The getter computes the element's translation mode per the HTML spec:
translate="yes" or "" enables it, "no" disables it (both ASCII
case-insensitively), anything else inherits from the closest ancestor
with a valid value, defaulting to enabled. The setter reflects back as
"yes"/"no".
Coverage: the 7 files above each 0/1 -> 1/1 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 16 failing files under /dom/events/scrolling/: all 8 variants of
scrollend-event-fired-for-programmatic-scroll.html and all 8 variants
of scrollend-event-fired-for-scroll-attr-change.html.
Window scrolls already dispatched throttled scroll + scrollend events,
but Element.scrollTo/scrollBy and the scrollTop/scrollLeft setters only
updated the stored position. They now schedule the same
scroll-then-scrollend pair (10ms/20ms, throttled through a per-element
state on the scroll position entry) when the position actually changes:
- events fired at the element don't bubble, per the UI Events
cancelability rules the tests assert ("Event targeting element does
not bubble");
- scrolling the document's scrolling element dispatches at the
document instead, where the events do bubble.
The remaining files in the directory need real layout
(scrollIntoView offsets, scroll snapping) or gesture scrolling.
Coverage: /dom/events/scrolling/: 20 files passing (16 newly), no
regressions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 9 failing files under /dom/nodes/Document-contentType/contentType/
(bmp, css, gif, jpg, mimeheader_01, png, txt, xml, and keeps the rest
green): a document created from a non-HTML response (the synthesized
image/text/raw documents) reported the text/html default from
document.contentType instead of the response's MIME type.
The frame records the response's MIME essence (type/subtype, lowercased,
parameters stripped) when the first chunk arrives, and applies it to
the new document's _content_type override once the navigation commits.
text/html responses keep the default.
The remaining file in the directory (contenttype_javascripturi) needs
iframes to navigate to javascript: URLs and document their string
result, which is a different mechanism.
Coverage: /dom/nodes/Document-contentType/: 14/15 files passing (9
newly passing, 1 subtest each).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/events/focus-event-document-move.html: testdriver's
Actions().send() promise resolved immediately (the vendor glue returned
Promise.resolve()) while the action sequence runs on the next scheduler
tick, so tests asserting right after `await ...send()` observed the
pre-action state.
actionSequence now returns a promise that a persisted resolver settles
once the input sources have been performed; the testdriver vendor glue
returns it. The persisted resolver handle is page-managed (persist
tracks it on the context), so the task does not reset it itself - doing
so double-freed the v8 global at page teardown.
Coverage: /dom/events/focus-event-document-move.html 0/1 -> 1/1 (fully
green); /dom/events regression clean (201 files).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/events/Event-timestamp-high-resolution.https.html
("window[eventType] is not a constructor"): the test constructs a
GamepadEvent and checks its timeStamp against performance.now().
GamepadEvent is a plain Event subclass with a gamepad member that is
always null - there are no gamepads in a headless browser - following
the DeviceMotionEvent pattern.
Coverage: /dom/events/Event-timestamp-high-resolution.https.html
0/1 -> 1/1 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the last failing subtest of WPT /dom/events/Event-dispatch-click.html
("pick the first with activation behavior <a href>", 32/33 -> 33/33):
following a link whose href is a javascript: URL must evaluate the URL
body as script in the link's frame. We explicitly ignored such hrefs,
so the test's completion callback never ran.
Anchor activation now schedules the script on the frame's JS scheduler
(navigation is a queued task) and compiles/runs it in the frame's
context, ignoring the completion value (a string result would replace
the document, which nothing relies on here). The URL body should be
percent-decoded per spec; markup hrefs rarely are, so that's left as a
TODO.
Coverage: /dom/events/Event-dispatch-click.html 32/33 -> 33/33 (fully
green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes all 29 failing files under /dom/events/non-cancelable-when-passive/
(13/42 -> 42/42 files passing):
- WebDriver wheel actions only worked with an element origin;
testdriver's Actions().scroll() uses the viewport origin, so nothing
was dispatched. Viewport origins now resolve their target through the
faux layout (a new vertical-axis-only elementFromPoint variant, since
the faux layout gives elements no useful horizontal extent), falling
back to the document element. The faux dimensions also learn
vh/vw units (resolved against the page viewport), which the tests'
200vh scroll containers rely on.
- Wheel dispatch also fires the legacy mousewheel event, like Blink.
- Touch pointer sources (Actions().addPointer("touch")) now dispatch
touchstart/touchmove/touchend (with pointer events, without mouse
events) instead of being treated as a mouse.
- Per the cancelability rules for scroll-blocking events, wheel,
mousewheel and touch events are dispatched cancelable only when some
listener on the propagation path (target chain plus window) is
non-passive: the UA knows preventDefault can't be called otherwise.
Coverage: /dom/events/non-cancelable-when-passive/: all 42 files pass
(29 newly passing, 1 subtest each); no regressions across /dom/events/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 21 failing subtests of WPT
/dom/ranges/Range-mutations-replaceChild.html (39/60 -> 60/60): live
range boundary points anchored on the parent were off by one because we
inserted the new child before removing the old one. Per the DOM
"replace" algorithm, the reference points are captured first (child's
next sibling, or node's own next sibling when they're adjacent), the
old child is removed, and only then is the new node inserted before the
reference - each step running the live-range update rules in that
order.
Self-replacement (replaceChild(c, c)) now also performs the actual
remove-and-reinsert instead of only queueing records, so ranges
anchored inside the node move to its old position, as the remove step
requires.
Node-replaceChild.html and MutationObserver-childList.html stay fully
green.
Coverage: /dom/ranges/Range-mutations-replaceChild.html 39/60 -> 60/60
(fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes all remaining failures of WPT /dom/ranges/Range-cloneContents.html
(171/187 -> 187/187) and Range-extractContents.html (153/187 ->
187/187):
- cloneContents only handled same-container and sibling-boundary
ranges. It now implements the DOM "clone the contents" algorithm on
explicit boundary points: partially contained CharacterData
boundaries are cloned with the substring, partially contained
elements are cloned shallow with the subrange recursed into the
clone, contained nodes are cloned deep, and a contained doctype
throws HierarchyRequestError.
- extractContents was cloneContents + deleteContents, which recreated
the extracted nodes ("you created or detached nodes when you weren't
supposed to"). It now implements the "extract" algorithm: contained
nodes MOVE into the fragment preserving identity; only the boundary
CharacterData substrings and the partially contained element shells
are cloned, with the originals truncated via replaceData. Children
are classified before anything moves, since moving a contained child
shifts the indices the boundary comparisons rely on. The range then
collapses to the same new node/new offset rule as deleteContents.
Coverage:
- /dom/ranges/Range-cloneContents.html 171/187 -> 187/187 (fully green)
- /dom/ranges/Range-extractContents.html 153/187 -> 187/187 (fully green)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 20 failing subtests of WPT /dom/ranges/Range-deleteContents.html
(105/125 -> 125/125) and 12 of Range-extractContents.html (which
delegates its deletion half): deleteContents only handled same-container
ranges and the "boundary containers are siblings" case, and always
collapsed to the start point.
It now follows the DOM algorithm:
- same-CharacterData ranges replace the data in place (replaceData, so
live ranges update);
- the top-most contained nodes (contained per boundary-point comparison,
with no contained parent) are collected before any mutation and
removed wherever they live in the tree, not just among siblings;
- partially contained CharacterData boundaries are truncated with
replaceData;
- the range collapses to the spec's "new node/new offset": the start
point when the start node is an inclusive ancestor of the end node,
otherwise just after the highest partially-contained ancestor of the
start node.
Coverage:
- /dom/ranges/Range-deleteContents.html 105/125 -> 125/125 (fully green)
- /dom/ranges/Range-extractContents.html 141/187 -> 153/187
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 44 failing subtests of WPT /dom/ranges/Range-collapse.html
(142/186 -> 186/186): per the DOM IDL, collapse(optional boolean
toStart = false) collapses the range to its END point when the argument
is omitted; we defaulted to the start.
Internal callers (deleteContents) already pass an explicit true and are
unaffected.
Coverage: /dom/ranges/Range-collapse.html 142/186 -> 186/186 (fully
green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 22 failing subtests of WPT /dom/ranges/Range-cloneRange.html
(40/62 -> 62/62): per the DOM spec, document.createRange() returns a
range whose start and end are (document, 0) - the document the method
was called on. The range factory hardcoded the frame's main document,
so foreignDoc.createRange() produced a range rooted in the main
document.
Factory.abstractRangeIn takes the initial container; createRange passes
its own document, and Range.init keeps the main-document default for
internal callers.
Coverage: /dom/ranges/Range-cloneRange.html 40/62 -> 62/62 (fully
green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/nodes/getElementsByClassName-14.htm: in quirks mode
getElementsByClassName must match class names ASCII
case-insensitively (class="a A" matches "a" twice), while Unicode case
stays significant.
The document had no notion of quirks mode at all (compatMode was
hardcoded to "CSS1Compat"). Document.isQuirksMode approximates the
HTML parser's mode: an HTML document without a doctype child is in
quirks mode (legacy doctypes that also trigger quirks are not
detected). compatMode now reports BackCompat accordingly, and
getElementsByClassName bakes the mode into its live-collection filter
(ClassNameFilter), with classAttributeContainsCase doing the
ASCII-case-insensitive token scan.
Coverage: /dom/nodes/getElementsByClassName-14.htm 1/2 -> 2/2 (fully
green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes "parser insertion mutations" in WPT
/dom/nodes/MutationObserver-document.html (1/4 -> 2/4): a script running
during the initial document parse can observe the document with
{subtree, childList} and must receive records for the nodes the parser
inserts after it. We suppressed all notifications for main-document
parser insertions.
Parser insertions now notify per inserted node. Fragment parses
(innerHTML et al.) stay silent, since Node.setHTML queues a single
combined "replace all" record. There is no overhead in the common case:
notifyChildInserted is gated on any observer existing, which is never
true during a parse unless an earlier script registered one.
The remaining two subtests need synchronous execution of scripts
inserted by other scripts during parsing (record batching across the
two) and parser insertions into a parent removed mid-parse; both are
script-scheduling semantics, not observer plumbing.
Coverage: /dom/nodes/MutationObserver-document.html 1/4 -> 2/4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the remaining 8 failing subtests of WPT
/dom/nodes/MutationObserver-childList.html (30/38 -> 38/38, fully
green):
- Inserting a DocumentFragment emitted one record per child for both
the removals from the fragment and the insertions into the parent.
Per the DOM insert algorithm observers get exactly two records: one
on the fragment with all removedNodes and one on the parent with all
addedNodes. appendAllChildren/insertAllChildrenBefore now share
moveAllChildren, which suppresses per-node notification and queues
the two combined records (or none, in the silent mode replaceChild
uses).
- replaceChild emitted separate insertion and removal records; the
"replace" algorithm queues one combined record {addedNodes,
removedNodes}. Removing the new child from its previous position
still notifies separately (internal replacement), and replacing a
node with itself reports a removal record followed by an addition
record with no tree change, matching browsers.
- Range.insertNode into a Text container rebuilt the text as new
before/after nodes (replaceChild + two insertBefores, three records).
It now uses splitText and inserts before the second half: two
records, and live ranges are updated by the split as a bonus
(Range-mutations-replaceChild 37/60 -> 39/60).
The unit test mutation_observer/childlist.html asserted the old
two-record replaceChild; updated to the spec's single combined record.
Coverage:
- /dom/nodes/MutationObserver-childList.html 30/38 -> 38/38 (fully green)
- /dom/ranges/Range-mutations-replaceChild.html 37/60 -> 39/60
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the failing subtest of WPT /dom/nodes/Node-mutation-adoptNode.html
("Adopting an element ... owner docs of it's attributes"): after
adopting an element into another document, its attribute nodes'
ownerDocument must report the new document.
Attribute nodes are parent-less, so ownerDocument fell through to the
detached-node fallback (the per-frame owner map / main document), which
never changes on adoption. An attribute node with an owning element now
delegates to that element's ownerDocument; detached Attr nodes keep the
old resolution.
Coverage:
- /dom/nodes/Node-mutation-adoptNode.html 1/2 -> 2/2 (fully green)
- /dom/nodes/attributes-namednodemap-cross-document.window.html 0/2 -> 1/2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the remaining failures of WPT /dom/nodes/Node-insertBefore.html
(16/40), Node-replaceChild.html (15/29) and Node-appendChild.html
(9/11) - all three are now fully green.
The insertion validity checks were incomplete and ran in the wrong
order:
- The reference-child NotFoundError check ran before the parent-type
and cycle checks, but per "ensure pre-insert validity" the
HierarchyRequestError checks for the parent kind and for node being
an inclusive ancestor of parent come first.
- Inserting a Document node was not rejected.
- DocumentFragment insertion into a document skipped validation
entirely: fragments with multiple elements, with a Text child, or
whose element joins an existing document element now throw.
- The doctype rules were missing: a second doctype, a doctype inserted
after an element, and an element inserted before a doctype all throw,
with replaceChild's variants (the replaced child doesn't count).
- replaceChild threw HierarchyRequestError for a child with the wrong
parent; the spec requires NotFoundError (unit test updated
accordingly).
- insertBefore's reference-child argument is required-but-nullable per
WebIDL: omitting it is now a TypeError (js.Nullable), while passing
null still appends.
appendChild/insertBefore/replaceChild now share ensurePreInsertValidity
(insert and replace modes); replaceChildren keeps its replace-all
validation.
Coverage:
- /dom/nodes/Node-insertBefore.html 16/40 -> 40/40 (fully green)
- /dom/nodes/Node-replaceChild.html 15/29 -> 29/29 (fully green)
- /dom/nodes/Node-appendChild.html 9/11 -> 11/11 (fully green)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/traversal/NodeIterator-removal.html (0/23 -> 23/23) and
/dom/nodes/moveBefore/moveBefore-nodeiterator.html: removing a node
that is an inclusive ancestor of a live NodeIterator's reference node
must move the reference per the DOM pre-removing steps. We never
adjusted iterators, so the reference kept pointing into the detached
subtree.
The frame keeps an intrusive list of live NodeIterators (mirroring
_live_ranges; iterators are slab-allocated for the frame lifetime, so
they are never unlinked) and Frame.removeNode runs the steps while the
tree is still intact:
- removing the root or an ancestor of the root leaves the iterator
untouched (matching browsers and the WPT model);
- with the pointer before the reference, the reference moves to the
first node following the removed subtree, if any;
- otherwise (or when there is no such node) it moves to the node
immediately preceding the removed node in tree order, clearing the
before-pointer in the fallthrough case.
Coverage:
- /dom/traversal/NodeIterator-removal.html 0/23 -> 23/23 (fully green)
- /dom/nodes/moveBefore/moveBefore-nodeiterator.html 0/1 -> 1/1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 5 failing subtests of WPT
/dom/traversal/TreeWalker-acceptNode-filter.html (7/12 -> 12/12):
- A filter object was converted eagerly at createTreeWalker time, so an
object without a callable acceptNode threw "invalid argument" at
creation. Per WebIDL any object converts to the NodeFilter callback
interface; the TypeError belongs at invocation time.
- The acceptNode member was cached at conversion. Per "call a user
object's operation" it must be looked up with a fresh Get on every
traversal, rethrowing errors from a throwing getter.
- The callback was invoked with the default this; the spec requires the
filter object itself as the this value.
NodeFilter now stores the raw function or object (js.Object.Global) and
performs the per-invocation lookup, callability check and this-binding
in acceptNode.
Coverage: /dom/traversal/TreeWalker-acceptNode-filter.html 7/12 ->
12/12 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the last failing subtest of WPT /dom/traversal/TreeWalker.html
("Recursive filters need to throw"): per the DOM traversal "filter"
algorithm, a NodeFilter that re-enters the walker (calling parentNode()
etc. from inside the callback) must get an InvalidStateError.
DOMTreeWalker tracks an active flag around the filter invocation and
throws InvalidStateError when a traversal starts while it is set.
Coverage: /dom/traversal/TreeWalker.html 760/761 -> 761/761 (fully
green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/traversal/TreeWalker-previousSiblingLastChildSkip.html
and 59 subtests of TreeWalker.html (701/761 -> 760/761):
TreeWalker.previousSibling()/nextSibling() only scanned the current
node's direct siblings, but the spec's "traverse siblings" algorithm
also:
- descends into a skipped (FILTER_SKIP) sibling's children - only a
rejected sibling excludes its whole subtree - so from B2 with B1
skipped, previousSibling() must return B1's last child;
- climbs to the parent when the siblings are exhausted and continues
from the parent's siblings, stopping at the root or at an accepted
parent.
Both directions now share the spec's traverseSiblings implementation.
Coverage:
- /dom/traversal/TreeWalker-previousSiblingLastChildSkip.html 0/1 -> 1/1
(fully green)
- /dom/traversal/TreeWalker.html 701/761 -> 760/761
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the remaining failures of WPT /dom/ranges/Range-selectNode.html
(288/292) and improves several other /dom/ranges files: for
CharacterData, the DOM "node length" is the data length in UTF-16 code
units, but Node.getLength returned the UTF-8 byte length, so
selectNodeContents() on text containing non-ASCII set endOffset to the
byte count (25 instead of 17 in the test's fixture).
Node.getLength now defers to CData.getLength (already UTF-16). Since
Range/Selection offsets are therefore UTF-16 units, the Range routines
that slice the UTF-8 data by offset (insertNode's text split,
deleteContents, cloneContents, toString) convert them with
utf16OffsetToUtf8 (clamped to the end) instead of using them as byte
indices, which would cut surrogate pairs and multi-byte sequences
apart.
Coverage:
- /dom/ranges/Range-selectNode.html 288/292 -> 292/292 (fully green)
- /dom/ranges/Range-deleteContents.html 95/125 -> 105/125
- /dom/ranges/Range-extractContents.html 128/187 -> 141/187
- /dom/ranges/Range-cloneContents.html 168/187 -> 171/187
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/nodes/Text-wholeText.html: wholeText must return the
concatenated data of the contiguous exclusive Text nodes (adjacent Text
siblings on both sides of the node) in tree order; we returned only the
node's own data.
The old own-data behavior is what every internal caller (semantic tree,
markdown dump, AX names, textarea default value, textContent) actually
wants, since they iterate the text nodes themselves; those now use the
new Text.ownData helper, and getWholeText implements the spec walk
(single-node case stays allocation-free).
Coverage: /dom/nodes/Text-wholeText.html 0/1 -> 1/1 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 7 failing WPT /dom/nodes/getElementsByClassName-*.htm files:
- Class attribute matching (getElementsByClassName and .class
selectors) treated only the space character as a token separator, so
class="a\nb" or class="a\tb" never matched "a". Per the spec the
separators are ASCII whitespace (tab, LF, FF, CR, space);
classAttributeContains now accepts all of them at token boundaries
(deliberately not std.ascii.isWhitespace, which would also accept
vertical tab). Fixes getElementsByClassName-02/04/15/22/25.
- getElementsByClassName-20 exercised table.tBodies[0].rows[0].cells[0]
and -21 table.deleteRow(1); the table interfaces were bare stubs.
Added HTMLTableElement.tBodies (child tbody collection),
HTMLTableSectionElement.rows (child tr collection),
HTMLTableRowElement.cells (child td/th collection, a new `cells`
NodeLive mode since child_tag filters a single tag), and
HTMLTableElement.deleteRow with the spec's row ordering (thead rows,
then table/tbody rows in tree order, then tfoot rows), -1 meaning the
last row, and IndexSizeError for out-of-range indices.
Coverage: getElementsByClassName-02/04/15/20/21/22/25.htm each
0/1 -> 1/1 (fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/nodes/MutationObserver-inner-outer.html (0/3 -> 3/3):
setting innerHTML must queue a single "replace all" mutation record
(all removed children + all parsed children), and setting outerHTML a
single record replacing the element with the parsed nodes. We emitted
one record per removed child plus one per inserted child.
- Node.setHTML suppresses per-node records (removals via the new
notify_observers opt, insertions by making fragment parsing never
notify) and queues the combined record itself. Parsing still targets
the element directly so html5ever keeps the right fragment context
(e.g. raw-text content of <script>).
- Fragment parsing no longer notifies observers per inserted child:
every other fragment-parse target is a detached DocumentFragment
where notification is a no-op.
- Element.setOuterHTML moves the parsed nodes in with notification
suppressed and queues one record with removedNodes=[the element],
addedNodes=parsed children and the element's siblings.
- The unit test mutation_observer/childlist.html expected the old
6-record innerHTML behavior; updated to the spec's single record
(matches Chrome).
Coverage: /dom/nodes/MutationObserver-inner-outer.html 0/3 -> 3/3
(fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/nodes/MutationObserver-textContent.html (1/4) and the
remaining failures of ParentNode-replaceChildren.html (25/29): the DOM
"replace all" algorithm (Element.textContent setter,
ParentNode.replaceChildren) must queue a single tree mutation record
with all removedNodes and addedNodes (and null previous/next sibling).
We emitted one record per removed child plus one per added child, so
observers saw 2+ records where the spec requires exactly one.
Frame.removeNode/appendNode gain a notify_observers opt (default true);
Node.replaceChildren suppresses the per-node records and queues the
combined record itself. Removing an added child from its previous
parent still notifies that parent's observers separately, as the spec
requires.
Coverage:
- /dom/nodes/MutationObserver-textContent.html 1/4 -> 4/4 (fully green)
- /dom/nodes/ParentNode-replaceChildren.html 25/29 -> 29/29 (fully green)
- /dom/nodes/MutationObserver-childList.html 29/38 -> 30/38
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes WPT /dom/nodes/MutationObserver-callback-arguments.html: per the
DOM spec ("invoke callback with a list of MutationRecord objects and mo,
and mo as callback this value"), the mutation callback's this value must
be the MutationObserver itself. We invoked it with the default receiver
(undefined -> globalThis), so `this === mo` failed.
deliverRecords now uses tryCallWithThis with the observer as receiver.
Coverage: /dom/nodes/MutationObserver-callback-arguments.html 0/1 -> 1/1
(fully green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes all failing subtests of WPT /dom/nodes/Element-insertAdjacentElement.html
(1/6), Element-insertAdjacentText.html (5/6) and insert-adjacent.html (11/14):
- insertAdjacentElement/Text wrongly reused insertAdjacentHTML's rule of
throwing NoModificationAllowedError for a null or document parent. Per
DOM's insert-adjacent algorithm they instead return null (no-op) for a
null parent and otherwise rely on pre-insert validity, so inserting
before/after the document element reports HierarchyRequestError.
findAdjacentNodes takes the variant; insertAdjacentHTML keeps its
HTML-spec behavior.
- insertAdjacentElement now returns the inserted element (was void, so
scripts using the return value got undefined).
- The pre-insert validity checks did not implement the document-parent
rules: a document can't contain Text children and has at most one
element child. validateDocumentInsertion enforces both in
appendChild/insertBefore/replaceChild; replaceChild excludes the child
being replaced from the single-element rule, and replaceChildren
(which replaces every child) is intentionally unaffected.
Coverage:
- /dom/nodes/Element-insertAdjacentElement.html 1/6 -> 6/6 (fully green)
- /dom/nodes/Element-insertAdjacentText.html 5/6 -> 6/6 (fully green)
- /dom/nodes/insert-adjacent.html 11/14 -> 14/14 (fully green)
- /dom/nodes/Node-insertBefore.html 13/40 -> 16/40
- /dom/nodes/Node-replaceChild.html 14/29 -> 15/29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the failing subtest of WPT /dom/nodes/Element-children.html
("HTMLCollection edge cases 1"): iterating an HTMLCollection with
for-in must yield only the supported indices; the supported names are
[LegacyUnenumerableNamedProperties] and must be skipped, while
Object.getOwnPropertyNames still returns indices + names.
v8 filters for-in through the named query interceptor, which
HTMLCollection did not register (only a descriptor callback), so every
name reported by the enumerator was treated as enumerable.
HTMLCollection now registers a named query reporting DontEnum for
supported names. The query deliberately does not report ReadOnly: v8
also consults it when a [[Set]] walks the prototype chain, and a
read-only property on the prototype would block the shadowing expando
that the spec's ignore-named-props rule requires
(HTMLCollection-as-prototype.html). Writability as observed through
getOwnPropertyDescriptor still comes from the descriptor callback, and
direct assignments to supported names still fail through the definer.
Coverage: /dom/nodes/Element-children.html 1/2 -> 2/2 (fully green);
/dom/collections stays fully green (53/53).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the last failing subtest of WPT
/dom/nodes/DOMImplementation-createHTMLDocument.html ("URL parsing"):
resolving `a.href = "http://example.org/?ä"` on an anchor inside a
createHTMLDocument() document returned "?%E4" instead of "?%C3%A4".
Two spec violations combined:
- Per DOM, documents synthesized by script (createHTMLDocument,
createDocument, new Document()) have the UTF-8 encoding. We had no
per-document encoding at all: document.characterSet always reflected
the frame's charset, so the new document inherited windows-1252 from
the test page (which uses that encoding deliberately to catch this).
Document gains a _charset override (same pattern as _content_type),
set to UTF-8 in DOMImplementation.createHTMLDocument/createDocument
and the Document constructor.
- Node.resolveURL encoded the query string with the frame's charset.
Per the URL/HTML specs the query percent-encoding uses the encoding
of the element's node document, so it now resolves the owning
document and uses its encoding, falling back to the frame's.
Coverage:
- /dom/nodes/DOMImplementation-createHTMLDocument.html 12/13 -> 13/13
(fully green)
- /dom/nodes/Document-constructor.html 3/5 -> 4/5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the remaining 30 failing subtests of WPT
/dom/nodes/Element-matches.html, in five clusters:
Matching (selector/List.zig):
- :link now matches a and area elements with an href attribute (in a
headless browser no link is ever visited, so :visited stays
unmatched and :link covers every hyperlink). :any-link gains the
missing area case.
- :empty now ignores comment and processing-instruction children, per
Selectors Level 3 only elements and non-empty text/cdata affect
emptiness (<p><!-- comment --></p> is :empty, <p> </p> is not).
- :lang() is implemented: the element's language is the nearest
ancestor-or-self lang attribute, falling back to the UA default (en)
for elements in a document and to no language at all in detached
subtrees, which is what the WPT expects for the detached/fragment
contexts. Matching is ASCII case-insensitive on the exact tag or a
`-` separated prefix (:lang(en) matches lang="en-AU").
Parsing (selector/Parser.zig):
- An empty selector-list segment ("div," or ",div") is now a parse
error instead of being silently skipped.
- Unexpected EOF closes open attribute brackets per CSS Syntax:
'#attr-value [align="center"' parses and matches.
- Attribute selectors accept a namespace component: [*|TiTlE] (any
namespace) and [|title] (no namespace). Attributes are stored by
qualified name and almost never namespaced, so both forms match by
name; [*|*=test] stays invalid.
Coverage:
- /dom/nodes/Element-matches.html 639/669 -> 669/669 (fully green)
- /dom/nodes/ParentNode-querySelectors-namespaces.html 0/1 -> 1/1
- /dom/nodes/moveBefore/moveBefore-lang.html 0/1 -> 1/1
- /dom/nodes/moveBefore/Node-moveBefore.html 31/32 -> 32/32
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 34 "wrong global" failures in WPT /dom/nodes/Element-matches.html
(and the equivalent checks in the other selector test files): when a DOM
method is called on a node from another realm (the test roots live in an
iframe), the thrown exception must be created in the receiver's relevant
realm, per Web IDL's "create an exception" using the context object's
realm. The harness checks e.constructor === root.ownerDocument.defaultView
.DOMException, which failed because we always built exceptions in the
caller's context.
The creation context of the wrapper can't be used to find that realm:
wrappers are session-level (one JS wrapper per Zig object, shared across
same-origin contexts). Instead, handleError now resolves the receiver
DOM node's document frame: for functions whose receiver is a Node (or a
type with an asNode() cast), errorLocal maps this -> node -> ownerDocument
-> frame, and if that frame differs from the calling one, the error is
constructed inside that frame's v8 context (entered/exited around the
exception creation).
TaggedOpaque.fromJS also learns to reject objects whose internal field
does not hold an aligned TaggedOpaque pointer. Such objects exist: a
custom element whose constructor throws was created from our template
but never mapped to a Zig instance, and its internal field still holds a
v8 tagged value. errorLocal probes the receiver on every error path, so
without the check the @alignCast panics (caught by the unit test suite's
custom_elements constructor.html).
Coverage: /dom/nodes/Element-matches.html 605/669 -> 639/669.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the failing subtest "hasOwnProperty, getOwnPropertyDescriptor,
getOwnPropertyNames" in WPT /dom/nodes/Document-getElementsByTagName.html
(17/18 -> 18/18) and /dom/nodes/Element-getElementsByTagName.html
(18/19 -> 19/19).
Per the DOM spec's supported property names for HTMLCollection, the
name attribute only exposes elements in the HTML namespace (ids expose
any element). The live-collection getByName fallback matched any
element with a name attribute; it now skips non-HTML elements, matching
the enumerator which already had the guard.
Coverage: Document-getElementsByTagName.html 17/18 -> 18/18,
Element-getElementsByTagName.html 18/19 -> 19/19. /dom/collections
stays fully green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes all 21 failing tests in WPT /dom/nodes/Document-createEvent.https.html
(258/279 -> 279/279), Document-createEvent-touchevent.window.html
(0/3 -> 3/3) and, as a side effect, /dom/events/non-cancelable-when-
passive/synthetic-events-cancelable.html (8/12 -> 12/12).
- Five new event interfaces, following the existing patterns:
BeforeUnloadEvent (returnValue, no constructor per spec), StorageEvent
(key/oldValue/newValue/url + initStorageEvent; storageArea is always
null), DeviceMotionEvent and DeviceOrientationEvent (nullable sensor
values), and TouchEvent (UIEvent subclass with modifier keys and empty
touch lists — there is no touch input source).
- document.createEvent maps their aliases to real instances instead of
plain Events, adds "touchevent", and drops the non-spec pluralized
aliases CustomEvents/FocusEvents/TextEvents, which must throw
NotSupportedError (the unit test asserting the old behavior is
updated).
- The "expose legacy touch event APIs" precondition requires
ontouchstart & co. to exist: the four touch handlers are added to the
global event handler set, HTMLElement and Document.
Coverage: /dom/nodes/Document-createEvent.https.html 258/279 ->
279/279, Document-createEvent-touchevent.window.html 0/3 -> 3/3,
synthetic-events-cancelable.html 8/12 -> 12/12. No regressions across
/dom/events; 1002/1002 unit tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 158 failing tests in WPT
/dom/nodes/DOMImplementation-createDocument.html (276/434 -> 434/434)
and improves Document-createElementNS.html (82/596 -> 197/596),
Document-createElement.html (29/147 -> 34/147),
Document-contentType/createDocument.html (0/1 -> 1/1) and
name-validation.html (0/5 -> 2/5). The remaining failures in those
files need XML/XHTML document loading, which Lightpanda doesn't have.
- Document now implements the DOM spec's §1.4 name validation
productions (valid element/attribute local name, valid namespace
prefix) and the "validate and extract" algorithm, replacing the
older ad-hoc element name check: prefixes are validated, and the
prefix/namespace consistency rules throw NamespaceError (prefix
without namespace, xml/xmlns mismatches).
- createElementNS and DOMImplementation.createDocument route through
it. createDocument also now: treats its two first arguments as
required (namespace via the js.Nullable wrapper, qualifiedName as a
raw js.Value so [LegacyNullToEmptyString] null maps to "" while
undefined stringifies), sets the spec's namespace-dependent
contentType, and registers custom namespace URIs for the root
element.
- element.namespaceURI now returns the actual URI for namespaces
outside the built-in set (it returned a lightpanda.io placeholder),
and Element.clone copies the registration. The unit tests asserting
the placeholder are updated to the correct values.
Coverage: DOMImplementation-createDocument.html 276/434 -> 434/434 and
the improvements above. No regressions across /dom/nodes; 1002/1002
unit tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 81 failing tests in WPT
/dom/nodes/DOMImplementation-createDocumentType.html (1/82 -> 82/82):
- DOMImplementation was an empty singleton-style object with no link to
its document, so a doctype created through another document's
implementation (e.g. createHTMLDocument().implementation) reported
the frame's main document as its ownerDocument. The implementation
object now stores its associated document and registers created
doctypes in the frame's node-owner-document map, following the
createDocumentFragment pattern.
- createDocumentType now validates the qualified name against the
doctype name production (no ASCII whitespace or '>'), throwing
InvalidCharacterError.
Coverage: /dom/nodes/DOMImplementation-createDocumentType.html 1/82 ->
82/82.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 4 failing tests in WPT
/dom/lists/DOMTokenList-coverage-for-attributes.html (135/140 ->
139/140): area.relList, output.htmlFor, iframe.sandbox and link.sizes
must be DOMTokenList attributes reflecting their content attributes.
Element gains a generic getTokenList lookup (keyed by element and
attribute) alongside the existing class/rel dedicated ones, and the
four elements expose the accessors following the Anchor/Link relList
pattern (undefined outside the HTML namespace).
The remaining failure needs an SVGAElement interface (relList on SVG
<a> elements), which doesn't exist yet — SVG elements are all generic.
Coverage: /dom/lists/DOMTokenList-coverage-for-attributes.html
135/140 -> 139/140.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 6 failing tests in WPT /dom/idlharness.any.worker.html
(211/219 -> 217/219) and the same assertions in the other idlharness
variants:
- addEventListener/removeEventListener had .length 1 instead of 2 and
accepted being called with a single argument. Per Web IDL their
callback parameter is required but nullable ("EventListener?"), which
a plain Zig optional can't express (the bridge treats optionals as
omittable and stops counting length at the first one). The new
js.Nullable(T) wrapper marks a required argument that accepts null or
undefined: omitting it throws a TypeError and it counts towards the
function's length.
- new AbortSignal() didn't throw: the spec defines no constructor, so
the JsApi constructor is removed (the interface object now uses the
illegal-constructor callback; AbortSignal.init stays for internal
use).
- AbortSignal.any() with no argument didn't throw: the trailing slice
parameter was treated as variadic and defaulted to empty. The
parameter is now a required js.Value converted to the signal sequence
explicitly.
The 2 remaining failures need strict-mode receiver validation on the
flattened worker global and the DOMStringList interface.
Coverage: /dom/idlharness.any.worker.html 211/219 -> 217/219. No
regressions across /dom/events and /dom/abort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>