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

250 lines
16 KiB
Markdown

# 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
- [x] Task 1: Add DocumentFragment node type (AC: #1)
- [x] 1.1 Add `NodeKind::DocumentFragment` variant to `crates/dom/src/node.rs`
- [x] 1.2 Add `Document::create_document_fragment(&mut self) -> NodeId` method in `document.rs`
- [x] 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`)
- [x] 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
- [x] 1.5 Unit tests: create fragment, add children, insert into DOM → children transfer, fragment empty
- [x] Task 2: Expose insertBefore to JavaScript (AC: #2, #4)
- [x] 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
- [x] 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
- [x] 2.3 Handle DocumentFragment: if `new_child` is a fragment, insert each fragment child before `ref_child`
- [x] 2.4 Unit tests (Rust): insert before first child, middle child, last child; insert with null ref (append); move existing node; insert fragment
- [x] 2.5 JS binding tests: verify insertBefore works from JavaScript
- [x] Task 3: Implement and expose replaceChild (AC: #3, #4)
- [x] 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)`
- [x] 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)
- [x] 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)
- [x] 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)
- [x] 3.5 JS binding tests: verify replaceChild works from JavaScript
- [x] Task 4: Wire createDocumentFragment to JavaScript (AC: #1)
- [x] 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" }`
- [x] 4.2 Ensure DocumentFragment objects support `appendChild`, `insertBefore`, `removeChild`, `replaceChild` in the method dispatch (fragments act as parents)
- [x] 4.3 Ensure `textContent` getter/setter works on fragments
- [x] 4.4 JS binding tests: create fragment from JS, add children, insert into DOM
- [x] Task 5: Integration tests and documentation (AC: #5)
- [x] 5.1 Add integration test: JS script that creates elements, uses insertBefore to reorder children, verifies DOM structure
- [x] 5.2 Add integration test: JS script that creates fragment, adds elements, appends fragment, verifies children transferred
- [x] 5.3 Add integration test: JS script that uses replaceChild, verifies old child removed and new child at correct position
- [x] 5.4 Add integration test: JS script that moves a node (appendChild to different parent), verifies removed from old parent
- [x] 5.5 Add golden test if any mutation affects rendering output
- [x] 5.6 Update `docs/HTML5_Implementation_Checklist.md` — check off DocumentFragment, insertBefore, replaceChild items
- [x] 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`:
```rust
// 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:
```rust
// 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.rs``inner_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](https://dom.spec.whatwg.org/#interface-node)
- [DOM Living Standard §4.5 — DocumentFragment interface](https://dom.spec.whatwg.org/#interface-documentfragment)
- [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