Files
rust_browser/_bmad-output/implementation-artifacts/2-4-dom-tree-mutation-apis.md
Zachary D. Rowitsch 22f9fffc1b Implement DOM tree mutation APIs with code review fixes (§4.4, §4.5)
Add DocumentFragment, insertBefore, replaceChild, and createDocumentFragment
with full JS binding support. Code review fixes: replace_child self-replacement
now no-op per DOM spec, addEventListener validates DocumentFragment IDs,
removed redundant dirty marking and double validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 22:58:41 -04:00

16 KiB

Story 2.4: DOM Tree Mutation APIs

Status: done

Story

As a web developer using JavaScript, I want to create, insert, replace, and move DOM nodes programmatically, So that dynamic web pages can modify their structure at runtime.

Acceptance Criteria

  1. document.createDocumentFragment() creates a lightweight container. When the fragment is inserted into the DOM via appendChild, insertBefore, or replaceChild, all child nodes transfer to the insertion point and the fragment becomes empty per DOM §4.5.

  2. parentNode.insertBefore(newNode, referenceNode) inserts newNode before referenceNode in the parent's child list. If referenceNode is null, appends to the end. If newNode already exists in the DOM, it is moved.

  3. parentNode.replaceChild(newChild, oldChild) replaces oldChild with newChild at the same position in the parent's child list. The old child is removed and returned. If newChild already exists in the DOM, it is moved.

  4. Node movement works correctly: inserting a node that already has a parent removes it from its current parent first (already works for appendChild, must also work for insertBefore and replaceChild).

  5. Integration tests verify each mutation API from JavaScript. docs/HTML5_Implementation_Checklist.md updated. just ci passes.

Tasks / Subtasks

  • Task 1: Add DocumentFragment node type (AC: #1)

    • 1.1 Add NodeKind::DocumentFragment variant to crates/dom/src/node.rs
    • 1.2 Add Document::create_document_fragment(&mut self) -> NodeId method in document.rs
    • 1.3 Update all match on NodeKind across the codebase to handle the new variant (search for exhaustive matches in dom, html, style, layout, display_list, render, web_api)
    • 1.4 Implement fragment insertion semantics: when append_child, insert_before, or replace_child receives a DocumentFragment as the child, iterate its children and insert each one individually, then clear the fragment's children
    • 1.5 Unit tests: create fragment, add children, insert into DOM → children transfer, fragment empty
  • Task 2: Expose insertBefore to JavaScript (AC: #2, #4)

    • 2.1 Verify Document::insert_before(parent, new_child, ref_child) exists from Story 2.3 — if not, implement it now in crates/dom/src/document.rs:
      • Remove new_child from old parent if it has one
      • Find index of ref_child in parent.children
      • Insert new_child at that index
      • If ref_child is None, append to end
      • Set parent link, mark dirty
    • 2.2 Add JS binding in crates/web_api/src/dom_host/host_environment.rs: wire "insertBefore" in the call_method match for "Element" type — extract parent_id, new_child_id, ref_child_id (handle JsValue::Null for ref_child), call document.insert_before(), return new_child as HostObject
    • 2.3 Handle DocumentFragment: if new_child is a fragment, insert each fragment child before ref_child
    • 2.4 Unit tests (Rust): insert before first child, middle child, last child; insert with null ref (append); move existing node; insert fragment
    • 2.5 JS binding tests: verify insertBefore works from JavaScript
  • Task 3: Implement and expose replaceChild (AC: #3, #4)

    • 3.1 Add Document::replace_child(&mut self, parent: NodeId, new_child: NodeId, old_child: NodeId) -> Option<NodeId> in document.rs:
      • Verify old_child is a child of parent, return None if not
      • Remove new_child from its current parent if it has one
      • Find index of old_child in parent.children
      • Replace old_child at that index with new_child
      • Clear old_child.parent, set new_child.parent = Some(parent)
      • Mark dirty, return Some(old_child)
    • 3.2 Add JS binding in host_environment.rs: wire "replaceChild" in call_method for "Element" — extract parent_id, new_child_id, old_child_id, call document.replace_child(), return old_child as HostObject (or throw if old_child not found)
    • 3.3 Handle DocumentFragment: if new_child is a fragment, replace old_child with all fragment children (first fragment child takes old_child's position, rest inserted after)
    • 3.4 Unit tests (Rust): replace first/middle/last child; replace with node from elsewhere; replace with fragment; error case (old_child not a child)
    • 3.5 JS binding tests: verify replaceChild works from JavaScript
  • Task 4: Wire createDocumentFragment to JavaScript (AC: #1)

    • 4.1 Add JS binding in host_environment.rs: wire "createDocumentFragment" in call_method for "Document" type — call document.create_document_fragment(), return as JsValue::HostObject { id, type_name: "DocumentFragment" }
    • 4.2 Ensure DocumentFragment objects support appendChild, insertBefore, removeChild, replaceChild in the method dispatch (fragments act as parents)
    • 4.3 Ensure textContent getter/setter works on fragments
    • 4.4 JS binding tests: create fragment from JS, add children, insert into DOM
  • Task 5: Integration tests and documentation (AC: #5)

    • 5.1 Add integration test: JS script that creates elements, uses insertBefore to reorder children, verifies DOM structure
    • 5.2 Add integration test: JS script that creates fragment, adds elements, appends fragment, verifies children transferred
    • 5.3 Add integration test: JS script that uses replaceChild, verifies old child removed and new child at correct position
    • 5.4 Add integration test: JS script that moves a node (appendChild to different parent), verifies removed from old parent
    • 5.5 Add golden test if any mutation affects rendering output
    • 5.6 Update docs/HTML5_Implementation_Checklist.md — check off DocumentFragment, insertBefore, replaceChild items
    • 5.7 Run just ci and ensure all tests pass

Dev Notes

Dependencies on Previous Stories

Story 2.3 plans to add Document::insert_before() and Document::insert_at() as internal helpers for the parser's adoption agency algorithm. If Story 2.3 is complete:

  • insert_before already exists — just wire it to JS and add DocumentFragment handling
  • insert_at may also exist — useful for replace_child implementation

If Story 2.3 is NOT complete, implement insert_before here (Task 2.1).

Architecture: JS Binding Pattern

The DOM-to-JS bridge uses HostEnvironment trait in crates/web_api/src/dom_host/host_environment.rs:

// JS calls: element.appendChild(child)
// VM dispatches to:
fn call_method(&mut self, obj_id: u64, obj_type: &str, method: &str, args: &[JsValue])
    -> Result<JsValue, RuntimeError>

// obj_id = NodeId.index() as u64
// obj_type = "Element", "Document", "DocumentFragment", etc.
// method = "appendChild", "insertBefore", etc.
// args = JS arguments as JsValue array

Existing method dispatch (in call_method):

  • "Document" + "getElementById"document.get_element_by_id()
  • "Document" + "createElement"document.create_element()
  • "Element" + "appendChild"document.append_child()
  • "Element" + "removeChild"document.remove_child()

Add new dispatch entries:

  • "Document" + "createDocumentFragment"document.create_document_fragment()
  • "Element" + "insertBefore"document.insert_before()
  • "Element" + "replaceChild"document.replace_child()
  • "DocumentFragment" + same methods as "Element" for tree mutation

DocumentFragment Semantics

Key behavior: fragment is a transparent container. When inserted:

// Pseudocode for append_child with fragment support:
fn append_child(&mut self, parent: NodeId, child: NodeId) {
    if self.is_document_fragment(child) {
        // Transfer each fragment child to parent
        let fragment_children: Vec<NodeId> = self.children(child).to_vec();
        for fc in fragment_children {
            self.append_child(parent, fc); // recursive, but fc is never a fragment
        }
        // Fragment is now empty (children moved)
    } else {
        // Existing behavior: remove from old parent, append to new parent
        // ...
    }
}

Important: The existing append_child must be updated to handle fragments, not just the new methods. This means modifying an existing method — be careful not to break existing behavior.

NodeKind Match Exhaustiveness

Adding NodeKind::DocumentFragment will break all exhaustive match statements. Search the codebase for:

match.*node\.kind
match.*kind
NodeKind::

Key locations likely to need updates:

  • crates/dom/src/document.rsinner_html(), text_content(), other methods
  • crates/html/src/tree_builder.rs — may match on NodeKind
  • crates/style/ — style computation skips non-element nodes
  • crates/layout/ — layout computation
  • crates/display_list/ — paint generation
  • crates/web_api/ — property getters

For most cases, DocumentFragment should be handled like Document (transparent container) or ignored (no styling, no layout, no painting).

Error Handling for JS APIs

Follow the DOM spec's exception patterns:

  • insertBefore with invalid ref_child → throw NotFoundError (return Err(RuntimeError))
  • replaceChild with old_child not a child of parent → throw NotFoundError
  • replaceChild/insertBefore with node that would create a cycle → throw HierarchyRequestError

In the Rust binding, these map to returning Err(RuntimeError::new("NotFoundError: ...")).

What NOT to Change (Scope Boundaries)

  • Do NOT add querySelector/querySelectorAll — that's Story 2.5
  • Do NOT add live collections (getElementsByTagName returning live HTMLCollection) — that's Story 2.5
  • Do NOT add cloneNode — not in this story's scope
  • Do NOT add innerHTML setter improvements — already works

Files to Modify

  • crates/dom/src/node.rs — add DocumentFragment to NodeKind
  • crates/dom/src/document.rs — add create_document_fragment(), replace_child(); verify/add insert_before(); update append_child() for fragment semantics
  • crates/dom/src/lib.rs — ensure new types are exported
  • crates/web_api/src/dom_host/host_environment.rs — wire JS bindings for all three new APIs + DocumentFragment method dispatch
  • crates/dom/src/tests/tree_ops_tests.rs — Rust-level unit tests
  • crates/web_api/src/dom_host/tests/dom_tests.rs — JS binding tests
  • Various crates for NodeKind match updates (style, layout, display_list, etc.)
  • docs/HTML5_Implementation_Checklist.md — update checked items

Previous Story Intelligence

From Story 2.3:

  • Document::insert_before() and Document::insert_at() may already be implemented as parser-internal helpers
  • These handle reparenting (remove from old parent first)
  • Build on these rather than reimplementing

From Epic 1:

  • Code review catches edge-case bugs — test fragment insertion thoroughly
  • Golden tests essential for rendering changes
  • Always update checklists

Testing Strategy

  • Unit tests in crates/dom/src/tests/tree_ops_tests.rs — Rust-level DOM operations
  • JS binding tests in crates/web_api/src/dom_host/tests/dom_tests.rs — verify JS calls work
  • Integration tests — full pipeline: JS script → DOM mutation → verify tree structure
  • Key test scenarios:
    • insertBefore(newNode, firstChild) — prepend
    • insertBefore(newNode, null) — append
    • insertBefore(existingNode, refNode) — move
    • replaceChild(newNode, oldNode) — swap
    • replaceChild(existingNode, oldNode) — move + swap
    • Fragment with 3 children → appendChild(fragment) → 3 children transferred
    • Fragment with children → insertBefore(fragment, refNode) → children inserted before ref
    • Empty fragment → appendChild(fragment) → no-op
    • Error: insertBefore(node, nonChildRef) → throws

References

  • DOM Living Standard §4.4 — Node interface
  • DOM Living Standard §4.5 — DocumentFragment interface
  • [Source: crates/dom/src/document.rs] — existing DOM operations
  • [Source: crates/dom/src/node.rs] — Node struct, NodeKind enum
  • [Source: crates/web_api/src/dom_host/host_environment.rs] — JS binding dispatch
  • [Source: crates/web_api/src/dom_host/tests/dom_tests.rs] — existing JS binding tests
  • [Source: crates/dom/src/tests/tree_ops_tests.rs] — existing DOM unit tests
  • [Source: docs/HTML5_Implementation_Checklist.md] — checklist to update

Dev Agent Record

Agent Model Used

Claude Opus 4.6 (1M context)

Debug Log References

No HALT conditions encountered. All tasks completed in a single implementation pass.

Completion Notes List

  • Task 1: Added NodeKind::DocumentFragment variant. Created create_document_fragment() and is_document_fragment() methods. Updated exhaustive matches in serialize_node (combined with Document arm) and dump_node (new arm). Updated set_text_content to handle fragments like elements. Refactored append_child and insert_before to delegate to internal _single methods, with fragment path that transfers children. Added replace_child with full fragment support. 7 unit tests added.
  • Task 2: insert_before already existed from Story 2.3. Wired JS binding with null ref_child handling (appends) and NotFoundError for invalid ref_child. Fragment support inherited from DOM layer. 3 unit tests + 3 integration tests added.
  • Task 3: Implemented Document::replace_child() with sibling-aware position tracking, including self-replacement no-op per DOM spec. Wired JS binding with NotFoundError on invalid old_child. Fragment support handles multi-child replacement at correct position. 4 unit tests + 2 integration tests added.
  • Task 4: Wired createDocumentFragment on Document type. Added DocumentFragment type dispatch in call_method (delegates to Element handlers), get_property, and set_property (textContent support). 4 unit tests + 2 integration tests added.
  • Task 5: Added 9 integration tests in tests/js_dom_tests.rs (including fragment replaceChild). No golden test needed (mutations don't affect rendering output since fragments are never rendered). Updated HTML5 checklist. just ci passes with 0 failures.

File List

  • crates/dom/src/node.rs — Added DocumentFragment variant to NodeKind
  • crates/dom/src/document.rs — Added create_document_fragment(), is_document_fragment(), replace_child(); refactored append_child/insert_before for fragment semantics; updated exhaustive matches in serialize_node, dump_node, set_text_content
  • crates/dom/src/tests/tree_ops_tests.rs — Added 12 unit tests for fragment, insertBefore, replaceChild (including self-replacement no-op)
  • crates/web_api/src/dom_host/host_environment.rs — Added createDocumentFragment, insertBefore, replaceChild bindings; added DocumentFragment type dispatch; updated get/set_property for DocumentFragment textContent
  • crates/web_api/src/dom_host/tests/dom_tests.rs — Added 9 unit tests for JS bindings
  • tests/js_dom_tests.rs — Added 9 integration tests for insertBefore, replaceChild, createDocumentFragment, node movement, fragment replaceChild
  • docs/HTML5_Implementation_Checklist.md — Checked off DOM tree node types and DOM mutation algorithms

Change Log

  • 2026-03-14: Implemented Story 2.4 — DOM Tree Mutation APIs (DocumentFragment, insertBefore, replaceChild, createDocumentFragment) with full JS binding support and comprehensive tests
  • 2026-03-14: Code review fixes — (1) replace_child self-replacement now no-op per DOM spec, (2) addEventListener validates DocumentFragment IDs, (3) removed redundant dirty marking in append_child fragment path, (4) removed double validate_element_id in DocumentFragment delegation, (5) added missing fragment replaceChild integration test, (6) corrected test counts in Dev Agent Record