- 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>
613 lines
16 KiB
Rust
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");
|
|
}
|