Add DOMContentLoaded, load, and readystatechange events with correct readyState transitions (loading→interactive→complete). Includes Window as a first-class event target, body onload spec quirk, and idempotency guards to prevent double-firing. Code review hardened the API surface by enforcing forward-only state transitions, eliminating a redundant wrapper function, and requesting a redraw after load handler DOM mutations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
218 lines
14 KiB
Markdown
218 lines
14 KiB
Markdown
# Story 2.7: Document Lifecycle
|
|
|
|
Status: done
|
|
|
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
## Story
|
|
|
|
As a web developer using JavaScript,
|
|
I want document lifecycle events to fire at the correct times,
|
|
So that scripts can initialize at the right point in page loading.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **DOMContentLoaded fires after parsing:** A page with a `DOMContentLoaded` event listener on `document` fires the event after the HTML is fully parsed (but before external resources like images are fully loaded). (WHATWG HTML §8.4)
|
|
|
|
2. **Window load event fires after all resources:** A page with a `load` event listener on `window` fires the event after all resources (images, stylesheets, scripts) have finished loading.
|
|
|
|
3. **document.readyState transitions correctly:** JavaScript reading `document.readyState` gets `"loading"` during parsing, `"interactive"` after parsing completes (before load), and `"complete"` after all resources load.
|
|
|
|
4. **readystatechange event fires on transitions:** A `readystatechange` event listener on `document` receives an event each time `document.readyState` changes (loading→interactive and interactive→complete).
|
|
|
|
5. **Integration tests verify event timing and readyState transitions**, checklist is updated, and `just ci` passes.
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Task 1: Add readyState property to Document (AC: #3)
|
|
- [x] 1.1 Add a `ready_state` field to `Document` in `crates/dom/src/document.rs`
|
|
- [x] 1.2 Initialize `ready_state: ReadyState::Loading` in `Document::new()`
|
|
- [x] 1.3 Add `pub fn ready_state(&self) -> ReadyState` getter and `pub fn set_ready_state(&mut self, state: ReadyState)` setter
|
|
- [x] 1.4 Expose `document.readyState` in JavaScript via `DomHost::get_property()` for `"Document"` type
|
|
- [x] 1.5 Unit tests: readyState default is "loading", setter works, JS property access returns correct string
|
|
|
|
- [x] Task 2: Add dispatch_document_event to WebApiFacade (AC: #1, #2, #4)
|
|
- [x] 2.1 Add `dispatch_lifecycle_event()` method to `WebApiFacade` — fires listeners directly on target without DOM propagation
|
|
- [x] 2.2 DOMContentLoaded fires on `EventTargetId::Document` (bubbles: true, cancelable: false)
|
|
- [x] 2.3 Load fires on `EventTargetId::Window` (bubbles: false, cancelable: false)
|
|
- [x] 2.4 Readystatechange fires on `EventTargetId::Document` on each transition
|
|
- [x] 2.5 Added `invoke_listeners_simple()` public wrapper in `event_dispatch.rs` for direct-target events
|
|
- [x] 2.6 Unit tests: verify DOMContentLoaded fires on document, load fires on window, readystatechange fires on document
|
|
|
|
- [x] Task 3: Wire lifecycle events into the page-load pipeline (AC: #1, #2, #3, #4)
|
|
- [x] 3.1 Updated pipeline in `event_handler.rs` with Phase 4 (interactive) and Phase 6 (complete) lifecycle events
|
|
- [x] 3.2 Used swap_document pattern for load event after rendering
|
|
- [x] 3.3 Added `fire_dom_content_loaded()` and `fire_load_event()` to `BrowserRuntime`
|
|
- [x] 3.4 `fire_dom_content_loaded()` called after scripts + inline handlers, before rendering
|
|
- [x] 3.5 `fire_load_event()` called after rendering completes
|
|
|
|
- [x] Task 4: Handle Window as an event target (AC: #2)
|
|
- [x] 4.1 Added `Window` variant to `EventTargetId` enum
|
|
- [x] 4.2 `addEventListener`/`removeEventListener` on Window works via `event_target_id_for()` mapping
|
|
- [x] 4.3 `<body onload="...">` now registers on `EventTargetId::Window` instead of body node
|
|
- [x] 4.4 Unit tests: addEventListener on window for "load" event registers correctly
|
|
|
|
- [x] Task 5: Tests and documentation (AC: #5)
|
|
- [x] 5.1 Integration test: DOMContentLoaded fires after scripts (verifies state=["script:loading","dcl:interactive"])
|
|
- [x] 5.2 Integration test: load fires after DOMContentLoaded (verifies order and readyState values)
|
|
- [x] 5.3 Integration test: readystatechange fires on each transition (verifies ["interactive","complete"])
|
|
- [x] 5.4 Integration test: inline `<body onload="...">` fires as window load event
|
|
- [x] 5.5 Updated `docs/HTML5_Implementation_Checklist.md` — Phase 4 items checked off
|
|
- [x] 5.6 Updated `docs/DOM_Implementation_Checklist.md` — readyState, DOMContentLoaded, readystatechange checked off
|
|
- [x] 5.7 `just ci` passes with all tests green
|
|
|
|
## Dev Notes
|
|
|
|
### Event System Infrastructure (What Exists)
|
|
|
|
The event system has solid foundations:
|
|
|
|
- **`DomEvent`** struct in `crates/web_api/src/event.rs` — event_type, target, phase, bubbles, cancelable, defaultPrevented, propagationStopped
|
|
- **`EventTargetId`** enum in `crates/web_api/src/event_target.rs` — currently `Node(NodeId)` and `Document` variants
|
|
- **`dispatch_event()`** in `crates/web_api/src/event_dispatch.rs` — full propagation (target → bubble) through DOM tree. Takes a `NodeId` target and builds path to Document
|
|
- **`EventListenerRegistry`** — addEventListener/removeEventListener with deduplication and snapshot-based dispatch
|
|
- **`install_inline_event_handlers()`** — scans `on*` attributes and registers listeners
|
|
- **`dispatch_click()`** on `WebApiFacade` — uses `dispatch_event()` for node-targeted events
|
|
|
|
### Key Design Decision: Direct-Target Events vs DOM Propagation
|
|
|
|
DOMContentLoaded, load, and readystatechange are NOT standard DOM propagation events. They do NOT bubble through DOM element nodes:
|
|
|
|
- **DOMContentLoaded** fires on `document` (bubbles: true, but only within document, not through element tree)
|
|
- **load** fires on `window` (bubbles: false)
|
|
- **readystatechange** fires on `document` (bubbles: false)
|
|
|
|
The existing `dispatch_event()` function expects a `NodeId` target and builds a propagation path through DOM elements. For lifecycle events, you need a **simpler dispatch** that fires listeners directly on a specific `EventTargetId` without building a DOM propagation path.
|
|
|
|
**Recommended approach:** Add a `dispatch_simple_event()` function that:
|
|
1. Takes `EventTargetId` + event type + bubbles/cancelable flags
|
|
2. Gets listeners from the registry for that target
|
|
3. Invokes them directly (no propagation path needed)
|
|
4. Drains microtask queue after dispatch
|
|
|
|
### What NOT to Change
|
|
|
|
- **Do NOT modify the existing `dispatch_event()` propagation logic** — it works correctly for node-targeted events like click
|
|
- **Do NOT add resource loading tracking** — for now, `load` fires after rendering is complete. True resource-complete tracking (waiting for all images/stylesheets) is a future optimization. The key behavior is that load fires AFTER DOMContentLoaded.
|
|
- **Do NOT implement `beforeunload` or `unload` events** — those are navigation events, not document lifecycle
|
|
- **Do NOT implement `document.DOMContentLoaded` as a property** — it's an event, not a property
|
|
- **Do NOT modify the HTML parser or tree builder**
|
|
|
|
### Architecture Constraints
|
|
|
|
- **Layer 0 (`dom`):** ReadyState type and Document field addition
|
|
- **Layer 1 (`web_api`):** Event dispatch for lifecycle events, JS property binding for readyState
|
|
- **Layer 2 (`browser_runtime`):** Public API methods for lifecycle events
|
|
- **Layer 3 (`app_browser`):** Wire lifecycle events into page-load pipeline
|
|
- **No unsafe** — enforced by CI
|
|
- **No upward dependencies** — `dom` does not import `web_api`; readyState is a DOM concept, not a web_api concept
|
|
|
|
### `<body onload>` Spec Quirk
|
|
|
|
Per HTML spec, the `onload` attribute on `<body>` is a **window event handler**, not a body event handler. The current `install_inline_event_handlers()` registers it on `EventTargetId::Node(body_id)`, which is incorrect. It should register `load` from `<body onload="...">` on `EventTargetId::Window` (or the equivalent). This is a known browser quirk that's important for compatibility.
|
|
|
|
Similarly, `onerror`, `onfocus`, `onblur`, `onscroll`, and `onresize` on `<body>` are also window event handlers per spec. For this story, only handle `onload` on `<body>` — the others can be addressed later.
|
|
|
|
### Key Files to Modify
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `crates/dom/src/document.rs` | Add `ReadyState` enum, `ready_state` field, getter/setter |
|
|
| `crates/web_api/src/event_target.rs` | Add `Window` variant to `EventTargetId` if needed |
|
|
| `crates/web_api/src/lib.rs` | Add `dispatch_document_event()` / `dispatch_window_event()`, expose readyState JS property |
|
|
| `crates/web_api/src/dom_host/host_environment.rs` | Add `readyState` property for Document type |
|
|
| `crates/browser_runtime/src/lib.rs` | Add `fire_dom_content_loaded()`, `fire_load_event()` |
|
|
| `crates/app_browser/src/event_handler.rs` | Wire lifecycle events into pipeline (lines 143-178) |
|
|
| `docs/HTML5_Implementation_Checklist.md` | Check off Phase 4 items |
|
|
| `docs/DOM_Implementation_Checklist.md` | Check off readyState, DOMContentLoaded, readystatechange |
|
|
|
|
### Key Files to Read (Reference)
|
|
|
|
| File | Why |
|
|
|------|-----|
|
|
| `crates/web_api/src/event.rs` | DomEvent struct, EventPhase |
|
|
| `crates/web_api/src/event_dispatch.rs` | Existing dispatch_event() — reference for how listeners are invoked |
|
|
| `crates/web_api/src/event_target.rs` | EventTargetId, EventListenerRegistry, EventListener |
|
|
| `crates/web_api/src/lib.rs` | WebApiFacade — dispatch_click pattern, install_inline_event_handlers |
|
|
| `crates/browser_runtime/src/lib.rs` | BrowserRuntime public API (dispatch_click pattern to follow) |
|
|
| `crates/app_browser/src/event_handler.rs` | Page-load pipeline (lines 143-178) |
|
|
|
|
### Previous Story Intelligence
|
|
|
|
**From Story 2.6 (Script Loading):**
|
|
- The page-load pipeline in `event_handler.rs` (lines 143-178) is the critical integration point
|
|
- Script execution happens after parsing, before rendering — lifecycle events slot between these phases
|
|
- The `runtime.prepare_for_navigation()` → `set_document()` → execute scripts → `take_document()` pattern must be preserved
|
|
|
|
**From Story 2.5 (DOM Query APIs):**
|
|
- JS property binding follows established pattern in `host_environment.rs` `get_property()` dispatch on `obj_type`
|
|
- Adding `"readyState"` follows the same pattern as other Document properties
|
|
|
|
**From Epic 1:**
|
|
- Always update both checklists (HTML5 and DOM) when applicable
|
|
- Code review catches edge cases — be thorough with event ordering
|
|
|
|
### Testing Strategy
|
|
|
|
- **Unit tests** in `crates/dom/src/document.rs` for ReadyState type and Document property
|
|
- **Unit tests** in `crates/web_api/src/lib.rs` for lifecycle event dispatch
|
|
- **Integration tests** using JS scripts that register event listeners and record execution order via array pushes
|
|
- **Key test pattern:** Scripts register listeners that push strings to a global array. After page load, verify the array contains the expected sequence.
|
|
|
|
### Spec References
|
|
|
|
- [WHATWG HTML §8.4 — Document lifecycle](https://html.spec.whatwg.org/multipage/parsing.html#the-end)
|
|
- [WHATWG HTML §7.1 — Document readiness states](https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness)
|
|
- [DOM §2.7 — Event dispatch](https://dom.spec.whatwg.org/#dispatching-events)
|
|
|
|
### References
|
|
|
|
- [Source: crates/web_api/src/event.rs] — DomEvent struct, EventPhase enum
|
|
- [Source: crates/web_api/src/event_dispatch.rs] — dispatch_event(), build_propagation_path()
|
|
- [Source: crates/web_api/src/event_target.rs] — EventTargetId enum, EventListenerRegistry
|
|
- [Source: crates/web_api/src/lib.rs] — WebApiFacade (dispatch_click, install_inline_event_handlers)
|
|
- [Source: crates/browser_runtime/src/lib.rs] — BrowserRuntime public API
|
|
- [Source: crates/app_browser/src/event_handler.rs] — Page-load pipeline (lines 143-178)
|
|
- [Source: crates/dom/src/document.rs] — Document type
|
|
- [Source: docs/HTML5_Implementation_Checklist.md] — Phase 4: Core Document Lifecycle
|
|
- [Source: docs/DOM_Implementation_Checklist.md] — §3.4 Document Lifecycle Events
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
Claude Opus 4.6 (1M context)
|
|
|
|
### Debug Log References
|
|
No errors or blocking issues encountered.
|
|
|
|
### Completion Notes List
|
|
- Implemented `ReadyState` enum (Loading/Interactive/Complete) in the `dom` crate with `as_str()` method
|
|
- Added `Window` variant to `EventTargetId` enum, enabling window as a first-class event target
|
|
- Created `dispatch_lifecycle_event()` on `WebApiFacade` for direct-target events (no DOM propagation)
|
|
- Added `invoke_listeners_simple()` public wrapper in `event_dispatch.rs`
|
|
- Wired `fire_dom_content_loaded()` and `fire_load_event()` through `BrowserRuntime` into the page-load pipeline
|
|
- Fixed `<body onload>` spec quirk: now registers on `EventTargetId::Window` per HTML spec
|
|
- Exposed `document.readyState` as a readable JS property
|
|
- All acceptance criteria satisfied with 5 unit tests + 4 integration tests + 8 lifecycle unit tests
|
|
|
|
### File List
|
|
- `crates/dom/src/document.rs` — Added `ReadyState` enum, `ready_state` field, getter/setter
|
|
- `crates/dom/src/lib.rs` — Re-exported `ReadyState`
|
|
- `crates/dom/src/tests/mod.rs` — Added `ready_state_tests` module
|
|
- `crates/dom/src/tests/ready_state_tests.rs` — NEW: 5 unit tests for ReadyState
|
|
- `crates/web_api/src/event_target.rs` — Added `Window` variant to `EventTargetId`
|
|
- `crates/web_api/src/event_dispatch.rs` — Made `invoke_listeners()` `pub(crate)` for lifecycle event dispatch
|
|
- `crates/web_api/src/lib.rs` — Added `dispatch_lifecycle_event()`, `fire_dom_content_loaded()`, `fire_load_event()`, body onload Window fix, 8 lifecycle unit tests
|
|
- `crates/web_api/src/dom_host/mod.rs` — Updated `event_target_id_for()` and `event_target_to_host_object()` for Window
|
|
- `crates/web_api/src/dom_host/host_environment.rs` — Added `document.readyState` JS property
|
|
- `crates/browser_runtime/src/lib.rs` — Added `fire_dom_content_loaded()` and `fire_load_event()` forwarding methods
|
|
- `crates/app_browser/src/event_handler.rs` — Wired lifecycle events into pipeline (Phase 4 + Phase 6)
|
|
- `tests/js_lifecycle.rs` — NEW: 4 integration tests for document lifecycle
|
|
- `Cargo.toml` — Added `[[test]]` entry for `js_lifecycle`
|
|
- `docs/HTML5_Implementation_Checklist.md` — Checked off Phase 4 readiness items
|
|
- `docs/DOM_Implementation_Checklist.md` — Checked off readyState, DOMContentLoaded, readystatechange
|
|
|
|
## Change Log
|
|
- 2026-03-15: Implemented document lifecycle events (DOMContentLoaded, load, readystatechange) with readyState transitions and Window event target support
|
|
- 2026-03-15: Code review fixes — removed invoke_listeners_simple wrapper (M1), changed dispatch_lifecycle_event to return () (M2), added window.request_redraw() after load handler DOM mutations (M3), added idempotency guards to lifecycle methods (M4), enforced forward-only readyState transitions (L1), removed no-change file list entry (L2), added TODO for other body-forwarded events (L3), clarified phase numbering comments (L4)
|