Files
rust_browser/tests/js_scheduling.rs
Zachary D. Rowitsch d691f56470 Add Phase 5 hardening: source locations, tracing, tests, and docs
- Add line:col source locations in runtime error messages (PR 5)
- Consolidate info! → debug! logging, add debug_span! for event loop,
  dispatch, and microtask drain (PR 6)
- Add 14 integration tests across js_events, js_dom_tests, js_tests,
  and js_scheduling for edge cases (PR 7)
- Add JS feature matrix and conformance report documentation (PR 8)
- Update CLAUDE.md with JS262 harness commands and doc references
- Mark Milestone 6 Phase 5 complete in plan doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 17:27:49 -05:00

613 lines
16 KiB
Rust

use shared::NodeId;
use web_api::WebApiFacade;
/// Helper: build a WebApiFacade with a DOM: html > body > div#target("original")
/// Uses virtual clock for deterministic testing.
fn setup_facade() -> (WebApiFacade, NodeId, NodeId) {
let mut facade = WebApiFacade::new_for_testing();
let doc = facade.document_mut();
let root = doc.root().unwrap();
let html = doc.create_element("html");
doc.append_child(root, html);
let body = doc.create_element("body");
doc.append_child(html, body);
let div = doc.create_element("div");
doc.set_attribute(div, "id", "target");
doc.append_child(body, div);
let text = doc.create_text("original");
doc.append_child(div, text);
facade.bootstrap().unwrap();
(facade, body, div)
}
// --- setTimeout(fn, 0) fires on next tick() ---
#[test]
fn set_timeout_delay_0_fires_on_next_tick() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
setTimeout(function() {
el.textContent = "timeout_fired";
}, 0);
"#,
)
.unwrap();
// Before tick: text unchanged
assert_eq!(facade.document().text_content(div), "original");
// After tick: timer fires
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "timeout_fired");
}
// --- setTimeout(fn, 100) doesn't fire until clock advances ---
#[test]
fn set_timeout_delayed_fires_after_clock_advance() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
setTimeout(function() {
el.textContent = "delayed";
}, 100);
"#,
)
.unwrap();
// tick at t=0 — not ready
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "original");
// Advance to t=99 — still not ready
facade.task_queue_mut().clock_mut().advance(99);
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "original");
// Advance to t=100 — fires
facade.task_queue_mut().clock_mut().advance(1);
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "delayed");
}
// --- queueMicrotask runs after script, before tick() ---
#[test]
fn queue_microtask_runs_after_script() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
queueMicrotask(function() {
el.textContent = "microtask_ran";
});
"#,
)
.unwrap();
// Microtasks drain after execute_script
assert_eq!(facade.document().text_content(div), "microtask_ran");
}
// --- Microtask runs before next task ---
#[test]
fn microtask_before_task_ordering() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
var order = "";
setTimeout(function() {
order = order + "task";
el.textContent = order;
}, 0);
queueMicrotask(function() {
order = order + "micro_";
});
"#,
)
.unwrap();
// After script: microtask has run, timer hasn't
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "micro_task");
}
// --- new Promise(resolve => resolve(42)).then(fn) ---
#[test]
fn promise_resolve_then_fires() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
new Promise(function(resolve) {
resolve(42);
}).then(function(value) {
el.textContent = "resolved_" + value;
});
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "resolved_42");
}
// --- Promise chain .then().then() ---
#[test]
fn promise_chain_propagates_values() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
new Promise(function(resolve) {
resolve(10);
}).then(function(value) {
return value + 5;
}).then(function(value) {
el.textContent = "chain_" + value;
});
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "chain_15");
}
// --- Promise.resolve(v).then(fn) ---
#[test]
fn promise_resolve_static() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
Promise.resolve("hello").then(function(v) {
el.textContent = "static_" + v;
});
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "static_hello");
}
// --- Promise.reject(r).catch(fn) ---
#[test]
fn promise_reject_catch() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
Promise.reject("oops").catch(function(r) {
el.textContent = "caught_" + r;
});
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "caught_oops");
}
// --- Nested microtasks drain completely ---
#[test]
fn nested_microtasks_drain() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
var order = "";
queueMicrotask(function() {
order = order + "1";
queueMicrotask(function() {
order = order + "2";
queueMicrotask(function() {
order = order + "3";
el.textContent = order;
});
});
});
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "123");
}
// --- Determinism: repeated runs produce identical results ---
#[test]
fn deterministic_execution() {
for _ in 0..10 {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
var order = "";
setTimeout(function() { order = order + "T"; el.textContent = order; }, 0);
queueMicrotask(function() { order = order + "M"; });
"#,
)
.unwrap();
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "MT");
}
}
// --- dispatch_click drains microtasks ---
#[test]
fn dispatch_click_drains_microtasks() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
el.addEventListener("click", function() {
queueMicrotask(function() {
el.textContent = "micro_from_handler";
});
});
"#,
)
.unwrap();
facade.dispatch_click(div).unwrap();
assert_eq!(facade.document().text_content(div), "micro_from_handler");
}
// --- Timer ordering: setTimeout(fn, 200) fires after setTimeout(fn, 100) ---
#[test]
fn timer_ordering() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
var order = "";
setTimeout(function() { order = order + "200_"; el.textContent = order; }, 200);
setTimeout(function() { order = order + "100_"; }, 100);
"#,
)
.unwrap();
facade.task_queue_mut().clock_mut().advance(100);
facade.tick().unwrap();
facade.task_queue_mut().clock_mut().advance(100);
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "100_200_");
}
// --- clearTimeout cancels a pending timer ---
#[test]
fn clear_timeout_cancels_timer() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
var id = setTimeout(function() {
el.textContent = "should_not_fire";
}, 0);
clearTimeout(id);
"#,
)
.unwrap();
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "original");
}
// --- clearTimeout with a non-existent ID is a no-op ---
#[test]
fn clear_timeout_nonexistent_id_is_noop() {
let (mut facade, _, div) = setup_facade();
// Cancelling a bogus timer ID must not error or panic.
facade
.execute_script(
r#"
clearTimeout(99999);
"#,
)
.unwrap();
// A real timer queued afterwards must still fire.
facade
.execute_script(
r#"
var el = document.getElementById("target");
setTimeout(function() {
el.textContent = "still_fires";
}, 0);
"#,
)
.unwrap();
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "still_fires");
}
// --- Promise double-resolve: second resolve is ignored ---
#[test]
fn promise_double_resolve_second_is_ignored() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
new Promise(function(resolve, reject) {
resolve("first");
resolve("second"); // must be ignored
}).then(function(v) {
el.textContent = "resolved_" + v;
});
"#,
)
.unwrap();
// Only the first resolve value should propagate.
assert_eq!(facade.document().text_content(div), "resolved_first");
}
// --- Promise reject-then-catch recovery chain ---
#[test]
fn promise_reject_then_catch_recovery() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
Promise.reject("initial_error")
.catch(function(reason) {
return "recovered_" + reason;
})
.then(function(v) {
el.textContent = v;
});
"#,
)
.unwrap();
assert_eq!(
facade.document().text_content(div),
"recovered_initial_error"
);
}
// --- .then on already-fulfilled promise fires synchronously (within drain) ---
#[test]
fn then_on_already_fulfilled_fires_in_same_drain() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
// Promise.resolve returns an already-fulfilled promise.
// .then should fire within the same microtask drain pass.
var p = Promise.resolve("already");
p.then(function(v) {
el.textContent = "then_" + v;
});
"#,
)
.unwrap();
// Microtasks drain after execute_script completes.
assert_eq!(facade.document().text_content(div), "then_already");
}
// --- .then forwarding: fulfilled promise with no on_fulfilled passes value along ---
#[test]
fn then_fulfilled_forwarding_without_on_fulfilled() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
// .then(null, onRejected) on a fulfilled promise: value should be forwarded
// to the next .then in the chain.
Promise.resolve("forwarded")
.then(undefined, function(reason) { return "rejected_path"; })
.then(function(v) {
el.textContent = "got_" + v;
});
"#,
)
.unwrap();
// The first .then has no on_fulfilled, so the value "forwarded" is forwarded
// to the second .then.
assert_eq!(facade.document().text_content(div), "got_forwarded");
}
// --- Promise error in then-handler rejects the child promise ---
#[test]
fn promise_then_handler_error_rejects_child() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
Promise.resolve("ok")
.then(function(v) {
return undeclaredVariable; // throws ReferenceError
})
.catch(function(err) {
el.textContent = "caught";
});
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "caught");
}
// --- Multiple timers: second timer fires after first ---
#[test]
fn multiple_timers_sequential_ticks() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
var log = "";
setTimeout(function() { log = log + "A_"; }, 0);
setTimeout(function() { log = log + "B_"; el.textContent = log; }, 0);
"#,
)
.unwrap();
// First tick fires timer A.
facade.tick().unwrap();
// Second tick fires timer B and updates textContent.
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "A_B_");
}
// --- Timer callback that enqueues a microtask: microtask runs in same drain ---
#[test]
fn timer_callback_enqueues_microtask_runs_in_same_drain() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
setTimeout(function() {
el.textContent = "timer";
queueMicrotask(function() {
el.textContent = el.textContent + "_micro";
});
}, 0);
"#,
)
.unwrap();
facade.tick().unwrap();
// Both timer callback and the microtask it enqueued should have run.
assert_eq!(facade.document().text_content(div), "timer_micro");
}
// --- Promise created in timer callback fires .then ---
#[test]
fn promise_created_in_timer_callback_fires() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
setTimeout(function() {
var p = new Promise(function(resolve) { resolve("from_timer"); });
p.then(function(v) { el.textContent = v; });
}, 0);
"#,
)
.unwrap();
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "from_timer");
}
// --- Nested setTimeout: second timer fires on subsequent tick ---
#[test]
fn nested_set_timeout_fires_on_subsequent_tick() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
el.textContent = "";
setTimeout(function() {
el.textContent = el.textContent + "outer";
setTimeout(function() {
el.textContent = el.textContent + "_inner";
}, 0);
}, 0);
"#,
)
.unwrap();
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "outer");
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "outer_inner");
}
// --- Microtask enqueues timer: timer fires on subsequent tick ---
#[test]
fn microtask_enqueues_timer_fires_on_subsequent_tick() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
el.textContent = "";
queueMicrotask(function() {
el.textContent = "micro";
setTimeout(function() {
el.textContent = el.textContent + "_timer";
}, 0);
});
"#,
)
.unwrap();
// Microtask already ran during execute_script drain.
assert_eq!(facade.document().text_content(div), "micro");
// Timer enqueued by microtask fires on next tick.
facade.tick().unwrap();
assert_eq!(facade.document().text_content(div), "micro_timer");
}