mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
fix(iframe): centralize document load delivery
This commit is contained in:
+18
-4
@@ -1230,13 +1230,27 @@ async fn auxiliary_adopted_iframe_loads_once_after_native_connection() {
|
||||
navigate_dynamic_page_and_wait_for_load(&mut child, 2, &format!("http://{fixture_addr}/popup"))
|
||||
.await;
|
||||
evaluate_window_name_probe(&mut opener, 3, r#"window.adoptedIframeLoaded=false;window.adoptedLoadCount=0;
|
||||
window.adoptedLoadEvents=[];
|
||||
window.adoptedFrame=document.createElement('iframe');
|
||||
adoptedFrame.onload=function(){adoptedLoadCount++;adoptedIframeLoaded=this===adoptedFrame&&this.ownerDocument===p.document};
|
||||
p.document.body.append(adoptedFrame);true"#).await;
|
||||
adoptedFrame.onload=function(){adoptedLoadCount++;adoptedLoadEvents.push('load');adoptedIframeLoaded=this===adoptedFrame&&this.ownerDocument===p.document};
|
||||
p.document.body.append(adoptedFrame);
|
||||
adoptedLoadEvents.push('after-insert');
|
||||
true"#).await;
|
||||
wait_for_value(&mut opener, "adoptedIframeLoaded", json!(true)).await;
|
||||
// Cross two tasks on the destination Page before checking for duplicate
|
||||
// load delivery. Install the checkpoints in that Page's own evaluation.
|
||||
evaluate_window_name_probe(
|
||||
&mut child,
|
||||
3,
|
||||
r#"window.adoptedLoadCheckpoint=false;
|
||||
setTimeout(()=>setTimeout(()=>{opener.adoptedLoadEvents.push('checkpoint');adoptedLoadCheckpoint=true},0),0);
|
||||
true"#,
|
||||
)
|
||||
.await;
|
||||
wait_for_value(&mut child, "adoptedLoadCheckpoint", json!(true)).await;
|
||||
assert_eq!(
|
||||
evaluate_window_name_probe(&mut opener, 4, "adoptedLoadCount").await,
|
||||
json!(1)
|
||||
evaluate_window_name_probe(&mut opener, 4, "[adoptedLoadCount,adoptedLoadEvents]").await,
|
||||
json!([1, ["load", "after-insert", "checkpoint"]])
|
||||
);
|
||||
abort_test_cdp_server(server).await;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use super::policy::{TreeMutationSourceProfile, TreeNoncePolicy, TreeReactionDispatchPolicy};
|
||||
use crate::{
|
||||
document_runtime::{DocumentRuntime, DomHandle, EventTargetHandle},
|
||||
document_runtime::{DocumentRuntime, DomHandle},
|
||||
mutation_coordinator::ConnectedScriptMutationPolicy,
|
||||
native_bridge::JsContextHost,
|
||||
util::v8str,
|
||||
};
|
||||
|
||||
impl DocumentRuntime {
|
||||
@@ -52,10 +51,7 @@ impl DocumentRuntime {
|
||||
reference_child: Option<DomHandle>,
|
||||
reaction_policy: TreeReactionDispatchPolicy,
|
||||
) -> bool {
|
||||
let insertion_roots = self
|
||||
.fragment_insertion_children(child)
|
||||
.unwrap_or_else(|| vec![child]);
|
||||
let changed = self.insert_before_with_nonce_handling(
|
||||
self.insert_before_with_nonce_handling(
|
||||
scope,
|
||||
host_ptr,
|
||||
parent,
|
||||
@@ -68,15 +64,7 @@ impl DocumentRuntime {
|
||||
reaction_policy,
|
||||
TreeNoncePolicy::HideInsertedContentAttributes,
|
||||
),
|
||||
);
|
||||
if changed {
|
||||
self.dispatch_detached_native_iframe_load_after_insert(
|
||||
scope,
|
||||
host_ptr,
|
||||
&insertion_roots,
|
||||
);
|
||||
}
|
||||
changed
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_detached_native_child(
|
||||
@@ -104,35 +92,4 @@ impl DocumentRuntime {
|
||||
TreeMutationSourceProfile::js_dom_api_appending_to_current_reaction_queue(),
|
||||
)
|
||||
}
|
||||
|
||||
fn dispatch_detached_native_iframe_load_after_insert(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
roots: &[DomHandle],
|
||||
) {
|
||||
let Some(event_ctor) = scope
|
||||
.get_current_context()
|
||||
.global(scope)
|
||||
.get(scope, v8str(scope, "Event").into())
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for &root in roots {
|
||||
if !self.dom_host.is_html_element_named(root, "iframe") {
|
||||
continue;
|
||||
}
|
||||
let Some(event) = event_ctor.new_instance(scope, &[v8str(scope, "load").into()]) else {
|
||||
continue;
|
||||
};
|
||||
let _ = self.dispatch_public_event_best_effort(
|
||||
scope,
|
||||
host_ptr,
|
||||
EventTargetHandle::Node(root),
|
||||
event,
|
||||
"detached native iframe load after insert",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,10 @@ impl JsContextHost {
|
||||
);
|
||||
}
|
||||
|
||||
/// Delivers child Document loads exclusively through FrameOwnerStore's
|
||||
/// owner and phase admissions, including synchronous initial about:blank.
|
||||
/// DOM insertion only connects the frame; it must not synthesize load on
|
||||
/// native nodes or their paired foreign reflectors.
|
||||
fn run_child_browsing_context_load_delivery(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
|
||||
@@ -897,11 +897,6 @@ fn detached_insert_node_with_current_queue_policy<'s>(
|
||||
detached_replace_children_array(scope, parent, prospective_array);
|
||||
}
|
||||
detached_record_tree_mutation(scope, parent);
|
||||
if !parent_has_native_handle {
|
||||
for node in insert_nodes {
|
||||
dispatch_detached_iframe_load_after_insert(scope, node);
|
||||
}
|
||||
}
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
@@ -936,17 +931,3 @@ fn detached_pre_insert_contains<'s>(
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn dispatch_detached_iframe_load_after_insert<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
node: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
if detached_node_type(scope, node) == Some(1)
|
||||
&& detached_element_local_name(scope, node)
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("iframe"))
|
||||
{
|
||||
let _ = crate::detached_event_target::dispatch_detached_simple_event(
|
||||
scope, node, "load", false, false, false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use super::fragment::validate_document_sequence;
|
||||
use super::*;
|
||||
use crate::custom_elements;
|
||||
use crate::native_bridge::document::{
|
||||
detached_native_handle_for_runtime, is_attr_node_value,
|
||||
paired_detached_native_object_for_handle,
|
||||
};
|
||||
use crate::native_bridge::document::{detached_native_handle_for_runtime, is_attr_node_value};
|
||||
|
||||
fn required_tree_insertion_node_arg_handle(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -43,48 +40,6 @@ fn set_original_node_arg_or_wrapped_handle<'s>(
|
||||
set_wrapped_node_or_null(scope, rv, runtime_ptr, Some(handle));
|
||||
}
|
||||
|
||||
fn inserted_handles_for_post_insert_events(
|
||||
runtime: &JsContextHost,
|
||||
handle: DomHandle,
|
||||
) -> Vec<DomHandle> {
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.is_some_and(Node::is_document_fragment)
|
||||
{
|
||||
runtime.dom_host().child_handles(handle).collect()
|
||||
} else {
|
||||
vec![handle]
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_detached_post_insert_events(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handles: &[DomHandle],
|
||||
) {
|
||||
for handle in handles {
|
||||
// A connected native iframe already owns its initial load delivery.
|
||||
// A paired foreign reflector must not add a second synthetic event.
|
||||
if unsafe { &*runtime_ptr }.child_browsing_context_host_is_active(*handle) {
|
||||
continue;
|
||||
}
|
||||
if !unsafe { &*runtime_ptr }
|
||||
.dom_host()
|
||||
.is_html_element_named(*handle, "iframe")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(target) = paired_detached_native_object_for_handle(scope, runtime_ptr, *handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let _ = crate::detached_event_target::dispatch_detached_simple_event(
|
||||
scope, target, "load", false, false, false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn node_append_child_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -109,8 +64,6 @@ pub(in crate::native_bridge) fn node_append_child_callback<'s>(
|
||||
return;
|
||||
}
|
||||
let child_value = args.get(0);
|
||||
let post_insert_event_handles =
|
||||
inserted_handles_for_post_insert_events(unsafe { &*runtime_ptr }, child);
|
||||
let inserted =
|
||||
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
|
||||
unsafe { &mut *runtime_ptr }.append_child_appending_to_current_reaction_queue(
|
||||
@@ -124,7 +77,6 @@ pub(in crate::native_bridge) fn node_append_child_callback<'s>(
|
||||
throw_dom_exception(scope, "HierarchyRequestError", 3, "Hierarchy Error");
|
||||
return;
|
||||
}
|
||||
dispatch_detached_post_insert_events(scope, runtime_ptr, &post_insert_event_handles);
|
||||
set_original_node_arg_or_wrapped_handle(scope, &mut rv, runtime_ptr, child_value, child);
|
||||
}
|
||||
|
||||
@@ -210,8 +162,6 @@ pub(in crate::native_bridge) fn node_insert_before_callback<'s>(
|
||||
return;
|
||||
}
|
||||
let child_value = args.get(0);
|
||||
let post_insert_event_handles =
|
||||
inserted_handles_for_post_insert_events(unsafe { &*runtime_ptr }, child);
|
||||
let inserted =
|
||||
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
|
||||
unsafe { &mut *runtime_ptr }.insert_before_appending_to_current_reaction_queue(
|
||||
@@ -226,7 +176,6 @@ pub(in crate::native_bridge) fn node_insert_before_callback<'s>(
|
||||
throw_dom_exception(scope, "HierarchyRequestError", 3, "Hierarchy Error");
|
||||
return;
|
||||
}
|
||||
dispatch_detached_post_insert_events(scope, runtime_ptr, &post_insert_event_handles);
|
||||
set_original_node_arg_or_wrapped_handle(scope, &mut rv, runtime_ptr, child_value, child);
|
||||
}
|
||||
|
||||
@@ -658,8 +607,6 @@ pub(in crate::native_bridge) fn node_replace_child_callback<'s>(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let post_insert_event_handles =
|
||||
inserted_handles_for_post_insert_events(unsafe { &*runtime_ptr }, new_child);
|
||||
let replaced =
|
||||
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
|
||||
unsafe { &mut *runtime_ptr }.replace_child_appending_to_current_reaction_queue(
|
||||
@@ -674,6 +621,5 @@ pub(in crate::native_bridge) fn node_replace_child_callback<'s>(
|
||||
throw_dom_exception(scope, "HierarchyRequestError", 3, "Hierarchy Error");
|
||||
return;
|
||||
}
|
||||
dispatch_detached_post_insert_events(scope, runtime_ptr, &post_insert_event_handles);
|
||||
set_original_node_arg_or_wrapped_handle(scope, &mut rv, runtime_ptr, args.get(1), old_child);
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ fn dom_parser_detached_elements_dispatch_local_events() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dom_parser_detached_iframe_load_ignores_tampered_non_elements() {
|
||||
fn dom_parser_detached_insertion_does_not_dispatch_iframe_load() {
|
||||
let mut vm = new_storage_test_vm("https://dom-parser-detached-iframe-load-guard.test/");
|
||||
|
||||
let result = vm
|
||||
@@ -238,9 +238,9 @@ fn dom_parser_detached_iframe_load_ignores_tampered_non_elements() {
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("DOMParser detached iframe load should guard node type");
|
||||
.expect("a Document without a browsing context should not load inserted iframes");
|
||||
|
||||
assert_eq!(result, "0|1");
|
||||
assert_eq!(result, "0|0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -292,7 +292,7 @@ fn dom_parser_detached_iframe_window_declares_own_methods() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dom_parser_detached_iframe_adopted_node_loads_after_insert() {
|
||||
fn dom_parser_detached_iframe_adoption_does_not_dispatch_load() {
|
||||
let mut vm = new_storage_test_vm("https://dom-parser-detached-iframe-import-sync.test/");
|
||||
|
||||
let result = vm
|
||||
@@ -326,7 +326,7 @@ fn dom_parser_detached_iframe_adopted_node_loads_after_insert() {
|
||||
)
|
||||
.expect("DOMParser detached iframe import source sync should evaluate");
|
||||
|
||||
assert_eq!(result, "true|1|imported|true|true");
|
||||
assert_eq!(result, "true|0||true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use super::*;
|
||||
|
||||
const FIXTURE: &str = include_str!("../../../tests/fixtures/iframe-load-delivery.js");
|
||||
const CHROMIUM: &str = include_str!("../../../tests/fixtures/iframe-load-delivery.chromium.json");
|
||||
|
||||
fn snapshot(vm: &mut ScriptVm) -> serde_json::Value {
|
||||
serde_json::from_str(
|
||||
&vm.eval("JSON.stringify(iframeLoadDelivery.snapshot())")
|
||||
.expect("iframe load trace should evaluate"),
|
||||
)
|
||||
.expect("iframe load trace should be JSON")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn iframe_load_delivery_matches_chromium_before_and_after_owner_tasks() {
|
||||
let oracle: serde_json::Value = serde_json::from_str(CHROMIUM).unwrap();
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let mut mismatches = Vec::new();
|
||||
for (name, expected) in oracle["cases"].as_object().unwrap() {
|
||||
let mut vm = new_storage_test_vm_with_loader("https://iframe-load-delivery.test/", &loader);
|
||||
vm.eval(FIXTURE)
|
||||
.expect("shared load fixture should install");
|
||||
vm.eval(&format!(
|
||||
"iframeLoadDelivery.start({})",
|
||||
serde_json::to_string(name).unwrap()
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("{name}: setup failed: {error}"));
|
||||
let immediate = snapshot(&mut vm);
|
||||
if immediate != expected["immediate"] {
|
||||
mismatches.push(format!(
|
||||
"{name} immediate: expected {}, got {immediate}",
|
||||
expected["immediate"]
|
||||
));
|
||||
}
|
||||
// Exercise real owner sources through quiescence, including any
|
||||
// navigation created by the synchronous initial load callback.
|
||||
vm.drain_pending_child_frame_work_for_test_on_owner_sources()
|
||||
.await;
|
||||
let settled = snapshot(&mut vm);
|
||||
if settled != expected["settled"] {
|
||||
mismatches.push(format!(
|
||||
"{name} settled: expected {}, got {settled}",
|
||||
expected["settled"]
|
||||
));
|
||||
}
|
||||
assert!(
|
||||
vm.run_next_child_frame_semantic_turn_for_test()
|
||||
.await
|
||||
.is_none(),
|
||||
"{name}: load delivery must not leave a duplicate owner task"
|
||||
);
|
||||
if matches!(name.as_str(), "pending-replacement" | "navigate-in-onload") {
|
||||
assert_eq!(
|
||||
vm.eval("iframeLoadDelivery.bodyText()").unwrap(),
|
||||
"replacement",
|
||||
"{name}: only the replacement Document should finish"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(mismatches.is_empty(), "{}", mismatches.join("\n"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn iframe_load_delivery_discards_queued_load_for_replaced_document() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let mut vm = new_storage_test_vm_with_loader("https://iframe-load-replacement.test/", &loader);
|
||||
vm.eval(FIXTURE).unwrap();
|
||||
vm.eval("iframeLoadDelivery.start('srcdoc')").unwrap();
|
||||
run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test(
|
||||
&mut vm,
|
||||
ChildFrameSemanticTurnKind::NavigationCommit,
|
||||
"original Document should commit",
|
||||
)
|
||||
.await;
|
||||
for transition in ["interactive", "DOMContentLoaded", "complete"] {
|
||||
run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test(
|
||||
&mut vm,
|
||||
ChildFrameSemanticTurnKind::DocumentLifecycle,
|
||||
transition,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// Complete has reserved HostLoad, but it has not called author JS yet.
|
||||
vm.eval("iframeLoadDelivery.replace()").unwrap();
|
||||
assert_eq!(
|
||||
vm.run_child_host_load_body_for_test().unwrap(),
|
||||
Some(crate::script_vm::ChildHostLoadRunOutcome::ConsumedWithoutCallback),
|
||||
"the superseded HostLoad must consume its reservation without calling author JS"
|
||||
);
|
||||
assert_eq!(snapshot(&mut vm), serde_json::json!(["before", "after"]));
|
||||
assert_eq!(vm.completed_child_frame_navigation_load_count(), 0);
|
||||
|
||||
vm.drain_pending_child_frame_work_for_test_on_owner_sources()
|
||||
.await;
|
||||
assert_eq!(
|
||||
snapshot(&mut vm),
|
||||
serde_json::json!(["before", "after", "frame:handler", "frame:listener"])
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("iframeLoadDelivery.bodyText()").unwrap(),
|
||||
"replacement"
|
||||
);
|
||||
assert_eq!(vm.completed_child_frame_navigation_load_count(), 1);
|
||||
assert!(
|
||||
vm.run_next_child_frame_semantic_turn_for_test()
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
@@ -2162,6 +2162,7 @@ mod event_receivers;
|
||||
mod geometry_point_conversion_order;
|
||||
mod headers_list;
|
||||
mod http_fixture;
|
||||
mod iframe_load_delivery;
|
||||
mod media_owner_playback_interfaces;
|
||||
mod media_recorder_shell;
|
||||
mod performance_observer_contract;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"product": "Chrome/145.0.7632.116",
|
||||
"revision": "@7d28075c6a9ba147e6df449209001258bb82a122",
|
||||
"cases": {
|
||||
"inert-parser-append": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"inert-parser-insert": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"inert-parser-replace": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"inert-html-append": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"inert-html-insert": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"inert-html-replace": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"inert-adoption": {"immediate": ["before", "after"], "settled": ["before", "after"]},
|
||||
"disconnected-native": {"immediate": ["before", "staged", "frame:handler", "frame:listener", "after"], "settled": ["before", "staged", "frame:handler", "frame:listener", "after"]},
|
||||
"disconnected-foreign": {"immediate": ["before", "staged", "frame:handler", "frame:listener", "after"], "settled": ["before", "staged", "frame:handler", "frame:listener", "after"]},
|
||||
"active-parser-append": {"immediate": ["before", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "after"]},
|
||||
"active-parser-insert": {"immediate": ["before", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "after"]},
|
||||
"active-parser-replace": {"immediate": ["before", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "after"]},
|
||||
"active-fragment": {"immediate": ["before", "staged", "frame:handler", "frame:listener", "after"], "settled": ["before", "staged", "frame:handler", "frame:listener", "after"]},
|
||||
"active-cross-window": {"immediate": ["before", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "after"]},
|
||||
"reinsert": {"immediate": ["before", "frame:handler", "frame:listener", "removed", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "removed", "frame:handler", "frame:listener", "after"]},
|
||||
"remove-in-onload": {"immediate": ["before", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "after"]},
|
||||
"srcdoc": {"immediate": ["before", "after"], "settled": ["before", "after", "frame:handler", "frame:listener"]},
|
||||
"pending-replacement": {"immediate": ["before", "after"], "settled": ["before", "after", "frame:handler", "frame:listener"]},
|
||||
"navigate-in-onload": {"immediate": ["before", "frame:handler", "frame:listener", "after"], "settled": ["before", "frame:handler", "frame:listener", "after", "frame:handler", "frame:listener"]},
|
||||
"url-navigation": {"immediate": ["before", "after"], "settled": ["before", "after", "frame:handler", "frame:listener"]}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Shared by the Rust lifecycle regressions and the Chromium differential runner.
|
||||
(() => {
|
||||
const root = document.documentElement || document.appendChild(document.createElement('html'));
|
||||
if (!document.body) root.appendChild(document.createElement('body'));
|
||||
let events;
|
||||
let resolveDone;
|
||||
let remainingLoads;
|
||||
let frame;
|
||||
const parserDocument = () => new DOMParser().parseFromString(
|
||||
'<html><body><iframe></iframe></body></html>', 'text/html');
|
||||
const foreignFrame = () => parserDocument().querySelector('iframe');
|
||||
const watch = (target, owner, label = 'frame', afterLoad) => {
|
||||
target.onload = function(event) {
|
||||
if (this !== target || event.target !== target || event.currentTarget !== target
|
||||
|| this.ownerDocument !== owner || this.contentDocument.readyState !== 'complete') {
|
||||
events.push('invalid-handler-target-or-document');
|
||||
}
|
||||
events.push(`${label}:handler`);
|
||||
if (afterLoad) afterLoad();
|
||||
};
|
||||
target.addEventListener('load', function(event) {
|
||||
events.push(this === target && event.target === target && event.currentTarget === target
|
||||
? `${label}:listener` : 'invalid-listener-target');
|
||||
if (--remainingLoads === 0) resolveDone();
|
||||
});
|
||||
};
|
||||
const insert = (parent, target, method) => {
|
||||
if (method === 'append') parent.appendChild(target);
|
||||
else if (method === 'insert') parent.insertBefore(target, null);
|
||||
else if (method === 'replace') {
|
||||
const previous = parent.appendChild(parent.ownerDocument.createElement('p'));
|
||||
parent.replaceChild(target, previous);
|
||||
} else throw new Error(`Unknown insertion method: ${method}`);
|
||||
};
|
||||
globalThis.iframeLoadDelivery = {
|
||||
done: null,
|
||||
start(name) {
|
||||
events = [];
|
||||
this.done = new Promise(resolve => { resolveDone = resolve; });
|
||||
remainingLoads = name.startsWith('inert-') ? 0
|
||||
: (name === 'reinsert' || name === 'navigate-in-onload' ? 2 : 1);
|
||||
events.push('before');
|
||||
if (name.startsWith('inert-parser-') || name.startsWith('inert-html-')) {
|
||||
const [, kind, method] = name.split('-');
|
||||
const doc = kind === 'parser' ? parserDocument()
|
||||
: document.implementation.createHTMLDocument('inert');
|
||||
frame = doc.createElement('iframe');
|
||||
watch(frame, doc);
|
||||
insert(doc.body, frame, method);
|
||||
} else if (name === 'inert-adoption') {
|
||||
const doc = document.implementation.createHTMLDocument('target');
|
||||
frame = foreignFrame();
|
||||
watch(frame, doc);
|
||||
doc.body.appendChild(frame);
|
||||
} else if (name.startsWith('disconnected-')) {
|
||||
const parent = document.createElement('div');
|
||||
frame = name.endsWith('foreign') ? foreignFrame() : document.createElement('iframe');
|
||||
watch(frame, document);
|
||||
parent.appendChild(frame);
|
||||
events.push('staged');
|
||||
document.body.appendChild(parent);
|
||||
} else if (name.startsWith('active-parser-')) {
|
||||
frame = foreignFrame();
|
||||
watch(frame, document);
|
||||
insert(document.body, frame, name.split('-')[2]);
|
||||
} else if (name === 'active-fragment') {
|
||||
const doc = parserDocument();
|
||||
const fragment = doc.createDocumentFragment();
|
||||
const parent = doc.createElement('div');
|
||||
frame = doc.querySelector('iframe');
|
||||
watch(frame, document);
|
||||
parent.appendChild(frame);
|
||||
fragment.appendChild(parent);
|
||||
events.push('staged');
|
||||
document.body.appendChild(fragment);
|
||||
} else if (name === 'active-cross-window') {
|
||||
const host = document.body.appendChild(document.createElement('iframe'));
|
||||
frame = host.contentDocument.createElement('iframe');
|
||||
watch(frame, document);
|
||||
document.body.appendChild(frame);
|
||||
} else if (name === 'reinsert') {
|
||||
frame = foreignFrame();
|
||||
watch(frame, document);
|
||||
document.body.appendChild(frame);
|
||||
const previousDocument = frame.contentDocument;
|
||||
frame.remove();
|
||||
events.push('removed');
|
||||
document.body.appendChild(frame);
|
||||
if (frame.contentDocument === previousDocument) events.push('reused-detached-document');
|
||||
} else if (name === 'remove-in-onload') {
|
||||
frame = foreignFrame();
|
||||
watch(frame, document, 'frame', () => frame.remove());
|
||||
document.body.appendChild(frame);
|
||||
} else if (name === 'srcdoc' || name === 'pending-replacement') {
|
||||
frame = foreignFrame();
|
||||
frame.srcdoc = '<body>original</body>';
|
||||
watch(frame, document);
|
||||
document.body.appendChild(frame);
|
||||
if (name === 'pending-replacement') this.replace();
|
||||
} else if (name === 'navigate-in-onload') {
|
||||
frame = foreignFrame();
|
||||
let first = true;
|
||||
watch(frame, document, 'frame', () => {
|
||||
if (first) {
|
||||
first = false;
|
||||
frame.srcdoc = '<body>replacement</body>';
|
||||
}
|
||||
});
|
||||
document.body.appendChild(frame);
|
||||
} else if (name === 'url-navigation') {
|
||||
frame = foreignFrame();
|
||||
document.body.appendChild(frame);
|
||||
watch(frame, document);
|
||||
frame.src = 'about:blank?navigation';
|
||||
} else throw new Error(`Unknown case: ${name}`);
|
||||
events.push('after');
|
||||
if (remainingLoads === 0) resolveDone();
|
||||
return this.snapshot();
|
||||
},
|
||||
replace() { frame.srcdoc = '<body>replacement</body>'; },
|
||||
snapshot() { return events.slice(); },
|
||||
bodyText() { return frame.contentDocument.body.textContent; }
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify iframe load delivery using the same fixtures as the Rust regressions.
|
||||
|
||||
uv run --project moli-benchmark python scripts/iframe-load-chromium-differential.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "moli-benchmark"))
|
||||
# Reuse the existing CDP browser lifecycle (startup, target creation, teardown).
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"iframe_load_oracle", ROOT / "scripts/layout-phase4-chromium-differential.py"
|
||||
)
|
||||
oracle = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = oracle
|
||||
spec.loader.exec_module(oracle)
|
||||
FIXTURES = ROOT / "moli-renderer-v8/tests/fixtures"
|
||||
EXPECTED = FIXTURES / "iframe-load-delivery.chromium.json"
|
||||
CASES = [
|
||||
*(f"inert-{kind}-{method}" for kind in ("parser", "html")
|
||||
for method in ("append", "insert", "replace")),
|
||||
"inert-adoption", "disconnected-native", "disconnected-foreign",
|
||||
*(f"active-parser-{method}" for method in ("append", "insert", "replace")),
|
||||
"active-fragment", "active-cross-window", "reinsert", "remove-in-onload",
|
||||
"srcdoc", "pending-replacement", "navigate-in-onload", "url-navigation",
|
||||
]
|
||||
|
||||
|
||||
async def evaluate(client, session_id, expression):
|
||||
result, _ = await oracle.command(client, "Runtime.evaluate", {
|
||||
"expression": expression, "returnByValue": True, "awaitPromise": True,
|
||||
}, session_id=session_id)
|
||||
if result.get("exceptionDetails"):
|
||||
raise RuntimeError(result["exceptionDetails"])
|
||||
return result["result"].get("value")
|
||||
|
||||
|
||||
async def measure(client, session_id, case, *, record):
|
||||
_, messages = await oracle.command(client, "Page.navigate", {
|
||||
"url": "data:text/html,<!doctype html><body>iframe load probe</body>",
|
||||
}, session_id=session_id)
|
||||
await oracle.wait_for_load(client, session_id, messages)
|
||||
await evaluate(client, session_id, (FIXTURES / "iframe-load-delivery.js").read_text())
|
||||
immediate = await evaluate(
|
||||
client, session_id, f"iframeLoadDelivery.start({json.dumps(case.name)})")
|
||||
# Wait for the expected document loads, then cross task boundaries so a
|
||||
# duplicate queued after the first event is visible in the final trace.
|
||||
settled = await evaluate(client, session_id, """
|
||||
(async () => {
|
||||
await iframeLoadDelivery.done;
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
return iframeLoadDelivery.snapshot();
|
||||
})()
|
||||
""")
|
||||
if case.name in ("pending-replacement", "navigate-in-onload"):
|
||||
body = await evaluate(client, session_id, "iframeLoadDelivery.bodyText()")
|
||||
if body != "replacement":
|
||||
raise AssertionError(f"{case.name}: replacement Document did not finish: {body!r}")
|
||||
actual = {"immediate": immediate, "settled": settled}
|
||||
if not record:
|
||||
expected = json.loads(EXPECTED.read_text())["cases"][case.name]
|
||||
if actual != expected:
|
||||
raise AssertionError(f"{case.name}: expected {expected!r}, got {actual!r}")
|
||||
return actual
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--chromium", type=Path, default=oracle.DEFAULT_CHROMIUM)
|
||||
parser.add_argument("--record", action="store_true")
|
||||
args = parser.parse_args()
|
||||
oracle.fixture_cases = lambda: (
|
||||
oracle.Case(name, 800, 600, "", ()) for name in CASES
|
||||
)
|
||||
oracle.measure_case = measure
|
||||
report = asyncio.run(oracle.run(args.chromium.expanduser().resolve(), record=args.record))
|
||||
report.pop("tolerance_css_px")
|
||||
if args.record:
|
||||
lines = ["{", f' "product": {json.dumps(report["product"])},',
|
||||
f' "revision": {json.dumps(report["revision"])},', ' "cases": {']
|
||||
items = list(report["cases"].items())
|
||||
lines.extend(
|
||||
f" {json.dumps(name)}: {json.dumps(case)}" + ("," if i + 1 < len(items) else "")
|
||||
for i, (name, case) in enumerate(items)
|
||||
)
|
||||
EXPECTED.write_text("\n".join([*lines, " }", "}", ""]))
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user