From bd7055dc0232f2e984ca77657fa3355529e0afd5 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 27 Sep 2026 04:43:47 +0800 Subject: [PATCH] fix(runtime): reconcile WPT changes with native state on main Restore native History cursors from sparse serialized positions, preserve shared Window history across isolated realms, and keep entry identity and structured state through document.open and cross-realm settlement. Move event subclass and precommit state access onto shared native backing. Respect published iframe layout snapshots and align history, rendering, and rejection tests with their owning task boundaries. Remove duplicate upstream tests and obsolete helpers from the main rebase. Validation: cargo fmt --all; workspace/all-targets/all-features Clippy with -D warnings; full Nextest (20,319 passed, 16 skipped); fresh CLI HTTP/CDP fetch resource timing fixture (36/36). Build and tests used 32-way parallelism. --- moli-core/tests/web_apis/abort_signal.rs | 2 +- moli-page-types/src/navigation_history.rs | 2 - moli-parser/src/xml.rs | 5 +- .../nested_history.rs | 10 +- moli-renderer-v8/src/context_bootstrap.rs | 5 +- .../event_target_dispatch.rs | 4 +- .../src/context_bootstrap/events.rs | 19 +-- .../events/subclasses/data.rs | 23 ++-- .../src/context_bootstrap/events/wrappers.rs | 13 +++ .../exposed_interfaces/materialize.rs | 13 --- .../exposed_interfaces/mod.rs | 2 +- .../src/context_bootstrap/history_mutation.rs | 40 ++----- .../history_runtime/native.rs | 2 +- .../history_runtime/traversal.rs | 2 +- .../indexed_db/event_target/dispatch.rs | 3 +- .../events/simple_event_target/dispatch.rs | 28 +++-- .../context_bootstrap/navigation_bootstrap.rs | 11 +- .../navigation_callbacks/navigation.rs | 3 +- .../src/context_bootstrap/navigation_entry.rs | 16 +-- .../context_bootstrap/navigation_events.rs | 21 +++- .../context_bootstrap/navigation_mutation.rs | 4 +- .../navigation_projection.rs | 4 +- .../context_bootstrap/navigation_restore.rs | 6 +- .../src/context_bootstrap/navigation_seed.rs | 32 +++-- .../context_bootstrap/navigation_serialize.rs | 3 +- .../navigation_surface/runtime.rs | 4 +- .../navigation_traversal_coordinator.rs | 5 +- .../navigation_traversal_execution.rs | 9 +- .../session_history_traversal.rs | 47 +++++--- .../context_bootstrap/window_events/error.rs | 1 - .../src/document_runtime/document_write.rs | 18 +-- .../src/document_runtime/events.rs | 6 + moli-renderer-v8/src/host/events.rs | 3 +- moli-renderer-v8/src/host/timers.rs | 12 ++ .../context_host/child_events.rs | 12 +- .../context_host/document_parser_owner.rs | 6 +- .../context_host/host_environment.rs | 11 +- .../src/native_bridge/context_host/layout.rs | 36 ------ .../native_bridge/context_host/navigation.rs | 4 +- moli-renderer-v8/src/native_bridge/element.rs | 15 +-- .../runtime/page_vm/tests/user_interaction.rs | 4 +- .../tests/extracted/owner_lifecycle.rs | 4 +- .../extracted/page_lifecycle_isolation.rs | 36 ++++-- .../src/script_vm/page_task_enqueue.rs | 6 + .../tests/browser_api/history_worlds.rs | 110 ++++++++++++++++-- .../navigation/element_navigation.rs | 79 +------------ .../navigation/history_traversal.rs | 5 +- .../script_vm/tests/browser_api/traversal.rs | 10 ++ .../stylesheet_and_document_lifecycle.rs | 13 ++- .../script_vm/tests/dom_xhr/cors_preflight.rs | 2 +- .../script_vm/tests/dom_xhr/forms/dialog.rs | 4 +- moli-renderer-v8/src/script_vm/tests/mod.rs | 2 +- .../src/script_vm/tests/observer_documents.rs | 20 +++- moli-renderer-v8/src/util.rs | 2 +- .../webidl_callback_source_boundary_tests.rs | 4 +- moli-renderer-v8/src/window_host.rs | 6 +- .../fixtures/navigation-document-open.js | 2 + 57 files changed, 430 insertions(+), 341 deletions(-) diff --git a/moli-core/tests/web_apis/abort_signal.rs b/moli-core/tests/web_apis/abort_signal.rs index 87929a54c7..786318393e 100644 --- a/moli-core/tests/web_apis/abort_signal.rs +++ b/moli-core/tests/web_apis/abort_signal.rs @@ -11,7 +11,7 @@ async fn assert_abort_signal_dependencies(scenario: &str, events: &[&str]) -> Re for target in ["window", "child", "worker"] { let observed = tokio::time::timeout( Duration::from_secs(10), - super::pipe_disturbed::run_probe(&browser, &server, target, &source), + super::event_dispatch::run_probe(&browser, &server, target, &source), ) .await??; assert_eq!( diff --git a/moli-page-types/src/navigation_history.rs b/moli-page-types/src/navigation_history.rs index 8d85fc533a..d0aed1327e 100644 --- a/moli-page-types/src/navigation_history.rs +++ b/moli-page-types/src/navigation_history.rs @@ -1042,8 +1042,6 @@ mod tests { NavigationHistoryDocumentId::allocate(), NavigationHistoryEntryId::allocate(), NavigationHistoryEntryKey::allocate(), - None, - None, ) }) .collect(); diff --git a/moli-parser/src/xml.rs b/moli-parser/src/xml.rs index 88641ab531..3e49b2ae92 100644 --- a/moli-parser/src/xml.rs +++ b/moli-parser/src/xml.rs @@ -18,7 +18,10 @@ use xmlparser::{ ElementEnd as XmlElementEnd, Token as XmlTokenizerToken, Tokenizer as XmlTokenizer, }; -use super::{html_chunks, xml_preprocess::prepare_xml_for_xml5ever, xml_tree_viewer::transform_document_to_xml_tree_view}; +use super::{ + html_chunks, xml_preprocess::prepare_xml_for_xml5ever, + xml_tree_viewer::transform_document_to_xml_tree_view, +}; use moli_dom::native::{ Attribute as NativeAttribute, DomHost, NativeDom, NativeNodeId, Node, ParserConstruction, }; diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/nested_history.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/nested_history.rs index 2bf4323b1f..f11d686169 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/nested_history.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/nested_history.rs @@ -265,14 +265,20 @@ async fn check_interleaved_frames_after_parent_document_replacement() -> Result< .collect::>(); // The URL changes at commit, before the restored child document // finishes parsing. Read its body only after that load completes. - wait( + let loaded = wait( &mut page, &format!( "({list}).every(f=>f.contentDocument?.readyState === 'complete') && JSON.stringify(({list}).map(f=>f.contentWindow.location.href)) === {}", serde_json::to_string(&serde_json::to_string(&urls)?)? ), ) - .await?; + .await; + if let Err(error) = loaded { + let observed = snapshot(&mut page, root).await; + return Err(error.context(format!( + "popup={popup}, {layout}: traversing {urls:?}; observed={observed:?}" + ))); + } let current = snapshot(&mut page, root).await.with_context(|| { format!("popup={popup}, {layout}: traversed child entries {urls:?}") })?; diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index e70e3f0bdb..c4ce66bf3d 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -99,9 +99,9 @@ pub(crate) use navigation_window::{ mod navigator_runtime; mod platform_object_worlds; mod session_history; +mod session_history_traversal; mod shared_event_targets; mod world_wrappers; -mod session_history_traversal; #[cfg(test)] pub(crate) use navigator_runtime::{ materialized_navigator_subobject_keys, navigator_storage_wrapper_diagnostics, @@ -157,7 +157,8 @@ pub(crate) use self::css_runtime::{ css_supports_condition_text, install_css_runtime_state_for_document, }; pub(crate) use self::events::{ - construct_original_event, construct_original_page_transition_event, + construct_original_event, + construct_original_page_transition_event, construct_original_storage_event_utf16, }; pub(crate) use crypto::{ diff --git a/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs b/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs index c42e258d4d..749c500c21 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs @@ -58,7 +58,9 @@ pub(crate) fn begin_dispatch_with_original_target<'s>( .unwrap_or(target); let original_target = backing .get_creation_context(scope) - .map(|context| super::shared_event_targets::target_in_realm(scope, original_target, context)) + .map(|context| { + super::shared_event_targets::target_in_realm(scope, original_target, context) + }) .unwrap_or(original_target); // HTML's legacy target override changes Event.target, not the dispatch // path or the Window whose listeners are invoked. diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs index be540f913a..435de1ef6c 100644 --- a/moli-renderer-v8/src/context_bootstrap/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/events.rs @@ -112,14 +112,18 @@ fn new_before_unload_event<'s>( if event.set_prototype(scope, prototype.into()) != Some(true) { return None; } + let wrapper = event; + let event = new_event_state(scope); base::initialize_event_object(scope, event, event_type, false, cancelable); - set_private_value( + set_event_private_value( scope, event, BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT, v8str(scope, "").into(), ); - Some(event) + web_api_interfaces::initialize(scope, event, "BeforeUnloadEvent").ok()?; + initialize_event_wrapper(scope, wrapper, event)?; + Some(wrapper) } pub(crate) fn construct_original_before_unload_event<'s>( @@ -142,7 +146,7 @@ fn before_unload_event_return_value<'s>( scope: &mut v8::PinScope<'s, '_>, event: v8::Local<'s, v8::Object>, ) -> Option> { - get_private_value(scope, event, BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT) + event_private_value(scope, event, BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) } @@ -158,7 +162,7 @@ pub(crate) fn set_before_unload_event_return_value<'s>( event: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::String>, ) { - set_private_value( + set_event_private_value( scope, event, BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT, @@ -498,7 +502,7 @@ pub(super) fn clipboard_event_clipboard_data_getter_function<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let value = get_private_value(scope, args.this(), CLIPBOARD_EVENT_CLIPBOARD_DATA_SLOT) + let value = event_private_value(scope, args.this(), CLIPBOARD_EVENT_CLIPBOARD_DATA_SLOT) .unwrap_or_else(|| v8::null(scope).into()); rv.set(value); } @@ -512,7 +516,7 @@ pub(super) fn clipboard_change_event_types_getter_function<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let Some(value) = get_private_value(scope, args.this(), CLIPBOARD_CHANGE_EVENT_TYPES_SLOT) + let Some(value) = event_private_value(scope, args.this(), CLIPBOARD_CHANGE_EVENT_TYPES_SLOT) else { throw_type_error(scope, "Illegal invocation"); return; @@ -529,7 +533,8 @@ pub(super) fn clipboard_change_event_change_id_getter_function<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let Some(value) = get_private_value(scope, args.this(), CLIPBOARD_CHANGE_EVENT_CHANGE_ID_SLOT) + let Some(value) = + event_private_value(scope, args.this(), CLIPBOARD_CHANGE_EVENT_CHANGE_ID_SLOT) else { throw_type_error(scope, "Illegal invocation"); return; diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs index 0f5bb99020..02d6757169 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs @@ -1042,6 +1042,12 @@ pub(in crate::context_bootstrap) fn run_navigate_event_precommit_handlers<'s>( } install_navigate_event_precommit_transition(scope, event); let controller = create_precommit_controller(scope, event); + crate::context_bootstrap::set_event_private_value( + scope, + event, + NAVIGATE_EVENT_PRECOMMIT_CONTROLLER_SLOT, + controller.into(), + ); let arguments = [controller.into()]; run_navigation_handler_array( scope, @@ -1117,16 +1123,9 @@ fn create_precommit_controller<'s>( scope: &mut v8::PinScope<'s, '_>, event: v8::Local<'s, v8::Object>, ) -> v8::Local<'s, v8::Object> { - let controller = PrecommitControllerDeclaration::new(event, true) + PrecommitControllerDeclaration::new(event, true) .bind(scope) - .expect("precommit controller declaration should bind"); - set_private_value( - scope, - event, - NAVIGATE_EVENT_PRECOMMIT_CONTROLLER_SLOT, - controller.into(), - ); - controller + .expect("precommit controller declaration should bind") } fn precommit_controller_event<'s>( @@ -1145,7 +1144,7 @@ fn precommit_controller_event<'s>( } let event = get_private_value(scope, controller, PRECOMMIT_CONTROLLER_EVENT_SLOT) .and_then(|value| v8::Local::::try_from(value).ok())?; - let owner_is_active = get_private_value( + let owner_is_active = crate::context_bootstrap::event_private_value( scope, event, NAVIGATE_EVENT_PRECOMMIT_TRANSITION_NAVIGATION_SLOT, @@ -1157,7 +1156,7 @@ fn precommit_controller_event<'s>( }); if !owner_is_active || !navigate_event_target_is_connected(scope, event) - || object_bool_property(scope, event, "defaultPrevented").unwrap_or(false) + || crate::context_bootstrap::event_bool_attribute(scope, event, "defaultPrevented") { navigate_event_throw_invalid_state(scope); return None; @@ -1239,7 +1238,7 @@ fn precommit_controller_redirect_callback<'s>( navigate_event_throw_invalid_state(scope); return; }; - let Some(navigation) = get_private_value( + let Some(navigation) = crate::context_bootstrap::event_private_value( scope, event, NAVIGATE_EVENT_PRECOMMIT_TRANSITION_NAVIGATION_SLOT, diff --git a/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs b/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs index 3e57f6ea89..6d1480aeed 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs @@ -265,6 +265,19 @@ pub(crate) fn event_attribute<'s>( property: &str, ) -> Option> { let state = event_backing(scope, event); + // CommandEvent and ToggleEvent expose source through prototype accessors; + // their native state holds the value in a private slot, not an own field. + if property == "source" { + match event_subclass_kind(scope, state) { + Some(EventSubclassKind::CommandEvent) => { + return get_private_value(scope, state, COMMAND_EVENT_SOURCE_SLOT); + } + Some(EventSubclassKind::ToggleEvent) => { + return get_private_value(scope, state, TOGGLE_EVENT_SOURCE_SLOT); + } + _ => {} + } + } state.get(scope, v8_string(scope, property)?.into()) } diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs index 92cc5cd139..893909154a 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs @@ -172,19 +172,6 @@ pub(crate) fn ensure_intrinsic_interface_prototype<'s>( .ok_or_else(|| anyhow!("intrinsic prototype `{name}` is missing after materialization")) } -/// Reads an already registered prototype without materializing an interface. -/// This is useful while creating an exception for a bootstrap failure. -pub(crate) fn materialized_intrinsic_interface_prototype<'s>( - scope: &mut v8::PinScope<'s, '_>, - name: &str, -) -> Option> { - let registry = ExposedInterfaceTemplateRegistry::current(scope)?; - let id = registry.id_by_name(name)?; - IntrinsicInterfaceRegistry::for_current_context(scope, registry.len()) - .ok()? - .prototype(scope, id) -} - fn materialize_uninitialized_interface<'s>( scope: &mut v8::PinScope<'s, '_>, registry: &Rc, diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs index 888e685109..169c889575 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs @@ -22,7 +22,7 @@ pub(crate) use install::{ }; pub(crate) use materialize::{ build_intrinsic_interface_instance, ensure_intrinsic_interface_constructor, - ensure_intrinsic_interface_prototype, materialized_intrinsic_interface_prototype, + ensure_intrinsic_interface_prototype, }; pub(crate) use metadata::RealmKind; pub(in crate::context_bootstrap) use metadata::TemplateBuildProfile; diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index 3acec70fb7..3169f33d7e 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -46,38 +46,22 @@ pub(crate) fn update_history_for_document_open<'s>( let Some(history) = window_history_for_holder(scope, window) else { return; }; - let Some(entries) = history_entries(scope, history) else { + let Some(history_record) = super::history_runtime::native::history(scope, history) else { return; }; - let index = history_index(scope, history); - let Some(previous) = entries - .get_index(scope, index) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { + let Some(previous_record) = history_record.borrow().current_entry().cloned() else { return; }; - let navigation_index = navigation_current_entry_index(scope, window).unwrap_or(index); - let key = navigation_entry_key_value(scope, previous) - .unwrap_or_else(|| new_navigation_entry_key().as_str().to_owned()); - let state = - clone_history_entry_state(scope, previous).unwrap_or_else(|| v8::null(scope).into()); - let state_json = stringify_history_state(scope, state); - let entry = create_navigation_entry( - scope, - window, - url.as_str(), - state_json.as_deref(), - None, - None, - navigation_index, - &new_navigation_entry_id(), - &key, - ); - copy_navigation_entry_document_id(scope, previous, entry); - bind_navigation_entry_runtime_owner(scope, entry, window); - set_history_entry_state(scope, entry, state); - let _ = entries.set_index(scope, index, entry.into()); - set_history_entries(scope, history, entries); + let previous = + super::history_runtime::native::entry_wrapper(scope, window, previous_record.clone()); + let mut record = previous_record.borrow().clone(); + record.url = url.as_str().to_owned(); + record.id = + super::navigation_entry::navigation_entry_public_token(new_navigation_entry_id().as_str()); + let entry = super::history_runtime::native::entry_wrapper(scope, window, record.into_ref()); + let state = super::history_runtime::state::history_state_value(scope, history); + replace_history_entry(scope, history, entry); + cache_current_history_state(scope, history, state); sync_navigation_current_entry_from_history_entry(scope, window, entry); super::session_history::commit( scope, diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/native.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/native.rs index 4bcbdde51f..0d46359288 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/native.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/native.rs @@ -165,7 +165,7 @@ pub(in crate::context_bootstrap) fn find_entry_wrapper<'s>( }; for candidate in candidates { if let Some(wrapper) = candidate.to_local(scope) - && wrapper.get_creation_context(scope) == Some(context) + && super::super::world_wrappers::belongs_to_world(scope, wrapper, context) { return Some(wrapper); } diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs index 60fc842125..3fcd8ed537 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs @@ -2,7 +2,7 @@ use super::super::navigation_activation::{ install_navigation_transition, precommit_transition_resolver_from_event, resolve_navigation_transition_committed, }; -use super::super::navigation_entry::{history_entries}; +use super::super::navigation_entry::history_entries; use super::super::navigation_events::{ NavigationDispatchOutcome, dispatch_navigation_success, run_navigation_precommit_deferred_handlers, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/event_target/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/event_target/dispatch.rs index b704936f52..6202b2df1c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/event_target/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/event_target/dispatch.rs @@ -36,7 +36,8 @@ pub(in crate::context_bootstrap::indexed_db) fn dispatch_idb_named_event<'s>( event_type == "error", ); events::mark_event_trusted(scope, event); - extras(scope, events::event_backing(scope, event)); + let backing = events::event_backing(scope, event); + extras(scope, backing); dispatch_idb_event_object(scope, target, event, event_type) } diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs index 3c8810516c..6a1f136277 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs @@ -242,21 +242,27 @@ pub(crate) fn invoke_simple_event_target_listeners<'s>( continue; }; let callback_target = crate::context_bootstrap::shared_event_targets::target_in_realm( - scope, target, listener.relevant_context(), + scope, + target, + listener.relevant_context(), ); - let Some(callback_event) = crate::context_bootstrap::navigation_event_worlds::event_in_realm( - scope, target, event, listener.relevant_context(), - ) else { + let Some(callback_event) = + crate::context_bootstrap::navigation_event_worlds::event_in_realm( + scope, + target, + event, + listener.relevant_context(), + ) + else { continue; }; let ordinary_arguments = [callback_event.into()]; - let handler_arguments = if handler_arguments.len() == 1 - && handler_arguments[0].strict_equals(event.into()) - { - ordinary_arguments.as_slice() - } else { - handler_arguments - }; + let handler_arguments = + if handler_arguments.len() == 1 && handler_arguments[0].strict_equals(event.into()) { + ordinary_arguments.as_slice() + } else { + handler_arguments + }; set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, listener.passive); let outcome = invoke_simple_event_listener_collecting_errors( scope, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs index 5b5e726b93..3297ff7c56 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs @@ -113,6 +113,11 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>( window: v8::Local<'s, v8::Object>, href: &str, ) -> Result<()> { + // A child isolated realm shares the default Window's native records. + // Guard the entire reset, including History and Navigation ownership. + if window_has_shared_history(scope, window) { + return Ok(()); + } reset_window_location_runtime_state(scope, window, href)?; let initial_seed = initial_navigation_history_seed(scope, window, href); @@ -123,9 +128,9 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>( set_runtime_window_owner(scope, history, window); install_history_scroll_restoration_runtime_state(scope, history, "auto"); install_history_state_runtime_state(scope, history, v8::null(scope).into()); - let entries = build_history_entries_from_seed(scope, window, &initial_seed); + let (entries, current_index) = build_history_entries_from_seed(scope, window, &initial_seed); let current_entry = entries - .get(initial_seed.current_index as usize) + .get(current_index as usize) .map(|entry| entry_wrappers::for_window(scope, window, entry.clone())) .unwrap_or_else(|| { build_current_navigation_entry_from_seed( @@ -136,7 +141,7 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>( ) }); set_history_entries(scope, history, entries); - set_history_index(scope, history, initial_seed.current_index); + set_history_index(scope, history, current_index); set_private_value(scope, window, WINDOW_HISTORY_SLOT, history.into()); let navigation = match window_runtime_object(scope, window, WINDOW_NAVIGATION_SLOT) { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_callbacks/navigation.rs b/moli-renderer-v8/src/context_bootstrap/navigation_callbacks/navigation.rs index c0fa77c5f0..7bedc2089c 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_callbacks/navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_callbacks/navigation.rs @@ -1190,9 +1190,8 @@ fn precommit_commit_fulfilled_callback<'s>( Some("reload"), ); } - let receiver = v8::undefined(scope).into(); if let Some(resolve) = data.committed_resolve { - let _ = resolve.call(scope, receiver, &[resolved_value]); + settle_navigation_finished_resolved_immediately(scope, resolve, resolved_value); } resolve_navigation_transition_committed(scope, data.navigation); let outcome = NavigationDispatchOutcome { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs index c66ba2e17d..4b0e0dfd77 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs @@ -184,7 +184,11 @@ pub(super) fn navigation_document_origin<'s>( crate::util::context_host_ptr_from_global_bridge(scope) .and_then(|host| unsafe { &*host }.window_document_origin(dispatch_scope)) }) - .or_else(|| url::Url::parse(url).ok().map(|url| url.origin().ascii_serialization())) + .or_else(|| { + url::Url::parse(url) + .ok() + .map(|url| url.origin().ascii_serialization()) + }) .unwrap_or_else(|| "null".to_owned()) } @@ -195,16 +199,6 @@ pub(super) fn navigation_entry_origin<'s>( navigation_entry_stored_string(scope, entry, EntryStringField::Origin) } -pub(super) fn set_navigation_entry_origin<'s>( - scope: &mut v8::PinScope<'s, '_>, - entry: v8::Local<'s, v8::Object>, - origin: &str, -) { - if let Some(entry) = native::entry(scope, entry) { - entry.borrow_mut().document_origin = origin.to_owned(); - } -} - pub(super) fn navigation_entries_share_origin<'s>( scope: &mut v8::PinScope<'s, '_>, left: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs index fcf28bf4ab..2a14c7e005 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs @@ -1099,9 +1099,12 @@ pub(super) fn finish_navigation_precommit<'s>( scope: &mut v8::PinScope<'s, '_>, event: v8::Local<'s, v8::Object>, ) { - if let Some(controller) = - get_private_value(scope, event, NAVIGATE_EVENT_PRECOMMIT_CONTROLLER_SLOT) - .and_then(|value| v8::Local::::try_from(value).ok()) + if let Some(controller) = crate::context_bootstrap::event_private_value( + scope, + event, + NAVIGATE_EVENT_PRECOMMIT_CONTROLLER_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok()) { set_private_value( scope, @@ -1109,7 +1112,7 @@ pub(super) fn finish_navigation_precommit<'s>( "__lmPrecommitControllerActive", v8::Boolean::new(scope, false).into(), ); - set_private_value( + crate::context_bootstrap::set_event_private_value( scope, event, NAVIGATE_EVENT_PRECOMMIT_CONTROLLER_SLOT, @@ -1441,7 +1444,12 @@ pub(super) fn navigation_destination_for_realm<'s>( return Some(wrapper); } let wrapper = create_navigation_destination(scope, "", false, None); - set_private_value(scope, wrapper, NAVIGATION_DESTINATION_BACKING_SLOT, destination.into()); + set_private_value( + scope, + wrapper, + NAVIGATION_DESTINATION_BACKING_SLOT, + destination.into(), + ); super::world_wrappers::insert(scope, destination, wrapper); Some(wrapper) } @@ -1459,8 +1467,9 @@ fn create_navigation_destination_for_entry<'s>( .and_then(|history| history_entries(scope, history)) .and_then(|entries| { let current = navigation_current_entry(scope, owner); + let record = super::history_runtime::native::entry(scope, entry)?; super::navigation_projection::visible_navigation_index_for_entry( - scope, entries, current, entry, + scope, &entries, current, &record, ) }) .map(|_| entry); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs b/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs index 2f63c24842..19c7f8995a 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs @@ -10,7 +10,9 @@ use super::navigation_entry::{ new_navigation_entry_key, set_navigation_entry_document_id, sync_navigation_current_entry_from_history_entry, }; -use super::navigation_entry_state::{copy_navigation_entry_serialized_state, set_navigation_entry_state}; +use super::navigation_entry_state::{ + copy_navigation_entry_serialized_state, set_navigation_entry_state, +}; use super::navigation_events::{ dispatch_navigation_currententrychange, dispatch_navigation_entry_dispose, }; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs index 3f2b97e05d..6e33c6183a 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs @@ -74,7 +74,9 @@ fn visible_navigation_entries<'s>( let same_origin = |entry: &HistoryEntryRef| { let current = current_entry.borrow(); let candidate = entry.borrow(); - current.document == candidate.document || (current.document_origin != "null" && current.document_origin == candidate.document_origin) + current.document == candidate.document + || (current.document_origin != "null" + && current.document_origin == candidate.document_origin) }; let mut start = current_index; while start > 0 { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs b/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs index 0b010badc7..5455ebd65d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs @@ -139,9 +139,9 @@ pub(crate) fn install_navigation_entry_view_for_holder<'s>( let Some(navigation) = window_navigation_for_holder(scope, owner) else { return; }; - let entries = build_history_entries_from_seed(scope, owner, entry_seed); + let (entries, current_index) = build_history_entries_from_seed(scope, owner, entry_seed); let current_entry = entries - .get(entry_seed.current_index as usize) + .get(current_index as usize) .map(|entry| entry_wrappers::for_window(scope, owner, entry.clone())) .unwrap_or_else(|| { build_current_navigation_entry_from_seed( @@ -154,7 +154,7 @@ pub(crate) fn install_navigation_entry_view_for_holder<'s>( let current_state = clone_history_entry_state(scope, current_entry).unwrap_or_else(|| v8::null(scope).into()); set_history_entries(scope, history, entries); - set_history_index(scope, history, entry_seed.current_index); + set_history_index(scope, history, current_index); cache_current_history_state(scope, history, current_state); set_navigation_current_entry(scope, navigation, current_entry); if let Some(snapshot) = entry_seed diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs index 54e1cdb9ab..4e7a036d4b 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs @@ -56,21 +56,34 @@ pub(super) fn build_history_entries_from_seed<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, seed: &NavigationHistoryEntrySeed, -) -> Vec { +) -> (Vec, u32) { let mut snapshots: Vec<_> = seed.entries.iter().collect(); snapshots.sort_by_key(|snapshot| snapshot.history_index); - snapshots - .into_iter() + // Serialized history positions can have gaps after pruning. Native History + // stores a dense vector, so restore its cursor from the sorted position. + let current_index = snapshots + .iter() + .position(|entry| entry.history_index == seed.current_index) + .map_or(seed.current_index, |index| index as u32); + let current_document = snapshots + .get(current_index as usize) + .map(|entry| &entry.document_id); + let entries = snapshots + .iter() .map(|snapshot| { let entry = history_entry_from_snapshot(snapshot); - if seed.entries.iter().find(|entry| entry.history_index == seed.current_index) - .is_some_and(|current| current.document_id == snapshot.document_id) - { - entry.borrow_mut().document_origin = super::navigation_entry::navigation_document_origin(scope, owner, &snapshot.url); + if current_document.is_some_and(|document| document == &snapshot.document_id) { + entry.borrow_mut().document_origin = + super::navigation_entry::navigation_document_origin( + scope, + owner, + &snapshot.url, + ); } entry }) - .collect() + .collect(); + (entries, current_index) } pub(super) fn build_current_navigation_entry_from_seed<'s>( @@ -85,7 +98,8 @@ pub(super) fn build_current_navigation_entry_from_seed<'s>( .find(|entry| entry.history_index == seed.current_index) .map(|snapshot| { let entry = history_entry_from_snapshot(snapshot); - entry.borrow_mut().document_origin = super::navigation_entry::navigation_document_origin(scope, owner, &snapshot.url); + entry.borrow_mut().document_origin = + super::navigation_entry::navigation_document_origin(scope, owner, &snapshot.url); entry }) .unwrap_or_else(|| { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs b/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs index c122b70432..91cd240adc 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs @@ -1,6 +1,7 @@ use super::navigation_activation::navigation_activation_value; use super::navigation_entry::{ - history_index, navigation_current_entry, navigation_entry_referrer_policy_value, + history_index, navigation_current_entry, navigation_entry_document_id, + navigation_entry_referrer_policy_value, }; use super::navigation_window::{ child_browsing_context_handle_for_runtime_owner, runtime_window_is_global, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs b/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs index 84de4ba218..52006c6fa8 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs @@ -54,10 +54,10 @@ pub(in crate::context_bootstrap) fn build_history_runtime_state<'s>( anyhow::anyhow!("isolated History is missing its native Window history") })? } else { - let records = build_history_entries_from_seed(scope, window, initial_seed); + let (records, current_index) = build_history_entries_from_seed(scope, window, initial_seed); std::rc::Rc::new(std::cell::RefCell::new(moli_history::WindowHistory::new( records, - initial_seed.current_index, + current_index, ))) }; let history = HistoryRuntimeObjectDeclaration::new() diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_coordinator.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_coordinator.rs index f5d70e664a..155190b378 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_coordinator.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_coordinator.rs @@ -16,8 +16,8 @@ use super::navigation_traversal_execution::TraversalTarget; use super::navigation_traversal_plan::JointTraversalPlan; use super::navigation_window::{ child_browsing_context_handle_for_runtime_owner, navigation_document_has_opaque_origin, - navigation_document_is_active, - window_navigation_for_holder, window_task_target_for_runtime_owner, + navigation_document_is_active, window_navigation_for_holder, + window_task_target_for_runtime_owner, }; use crate::document_runtime::DomHandle; use crate::native_bridge::history_traversal::{ @@ -380,7 +380,6 @@ fn settle_aborted_admission<'s>( admission: &PendingHistoryTraversalAdmission, error: Option>, ) { - let canceled = error.is_none(); let _execution = crate::script_cleanup::ScriptExecutionScope::enter(scope); let error = error.unwrap_or_else(|| { navigation_dom_exception(scope, "History traversal was canceled", "AbortError") diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs index 528fa3385b..a54fc3d06c 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs @@ -1,8 +1,6 @@ use super::history_runtime::results::reject_pending_navigation_results; -use super::history_runtime::{ - history_traversal_target_window, route_history_traversal_task, -}; -use super::navigation_entry::{history_entries}; +use super::history_runtime::{history_traversal_target_window, route_history_traversal_task}; +use super::navigation_entry::history_entries; use super::navigation_events::{ NavigationDispatchOutcome, dispatch_beforeunload_for_runtime_owner, dispatch_navigation_traverse_event_with_outcome, @@ -218,7 +216,8 @@ fn traversal_target_key_still_available<'s>( return true; }; history_entries(scope, history).is_some_and(|entries| { - entries.get(target_index as usize) + entries + .get(target_index as usize) .is_some_and(|entry| entry.borrow().key.as_str() == expected_key) }) } diff --git a/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs b/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs index 0ae81e1325..00499d6b8b 100644 --- a/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs @@ -99,18 +99,26 @@ pub(super) fn apply_step( .and_then(|entries| entries.get(target.target_index as usize).cloned()) { let key = entry.borrow().key.as_str().to_owned(); - host.top_level_navigation_history().select_joint_traversal(key, step); + host.top_level_navigation_history() + .select_joint_traversal(key, step); } host.record_pending_top_level_history_traversal(delta); return; } } - let targets = plan.targets.iter().map(|target| { - let entries = history_entries(scope, target.history)?; - let entry = entries.get(target.target_index as usize)?; - let key = entry.borrow().key.clone(); - Some((session_history::binding(scope, host, target.owner).context, key)) - }).collect::>>(); + let targets = plan + .targets + .iter() + .map(|target| { + let entries = history_entries(scope, target.history)?; + let entry = entries.get(target.target_index as usize)?; + let key = entry.borrow().key.clone(); + Some(( + session_history::binding(scope, host, target.owner).context, + key, + )) + }) + .collect::>>(); let Some(targets) = targets else { if let Some(method) = method { reject_canceled_history_traversal_results(scope, &method.results); @@ -135,15 +143,22 @@ pub(super) fn apply_step( let key = history_entries(scope, target.history) .and_then(|entries| entries.get(target.target_index as usize).cloned()) .map(|entry| entry.borrow().key.as_str().to_owned()); - let (info, results) = method.map_or_else(|| (None, Vec::new()), |method| (method.info, method.results)); - apply_pending_cross_document_traversal(scope, host, PendingCrossDocumentTraversal { - target: exact, - target_index: target.target_index, - target_key: key, - seed, - info, - results, - }) + let (info, results) = method.map_or_else( + || (None, Vec::new()), + |method| (method.info, method.results), + ); + apply_pending_cross_document_traversal( + scope, + host, + PendingCrossDocumentTraversal { + target: exact, + target_index: target.target_index, + target_key: key, + seed, + info, + results, + }, + ) } else { false } diff --git a/moli-renderer-v8/src/context_bootstrap/window_events/error.rs b/moli-renderer-v8/src/context_bootstrap/window_events/error.rs index 2219c26d94..edfe65cfdb 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events/error.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events/error.rs @@ -20,7 +20,6 @@ struct ErrorEventInitDeclaration<'scope> { error: v8::Local<'scope, v8::Value>, } - pub(super) fn ensure_window_reflecting_body_onerror_handler(scope: &mut v8::PinScope<'_, '_>) { let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; diff --git a/moli-renderer-v8/src/document_runtime/document_write.rs b/moli-renderer-v8/src/document_runtime/document_write.rs index c8a864906e..e3dc4b8baf 100644 --- a/moli-renderer-v8/src/document_runtime/document_write.rs +++ b/moli-renderer-v8/src/document_runtime/document_write.rs @@ -53,20 +53,10 @@ struct DocumentWriteParserMutationOwner<'a, 'scope, 'pin> { enum DocumentWriteParserMutationTarget { LiveDocument, WindowlessDocument { owner_document: DomHandle }, - DetachedFragment { owner_document: DomHandle }, + DetachedFragment, } impl DocumentWriteParserMutationOwner<'_, '_, '_> { - fn owner_document_handle(&self) -> DomHandle { - match self.target { - DocumentWriteParserMutationTarget::LiveDocument => self.runtime.document_handle(), - DocumentWriteParserMutationTarget::WindowlessDocument { owner_document } - | DocumentWriteParserMutationTarget::DetachedFragment { owner_document } => { - owner_document - } - } - } - fn targets_live_document(&self) -> bool { matches!(self.target, DocumentWriteParserMutationTarget::LiveDocument) } @@ -78,7 +68,7 @@ impl ParserMutationEffectConsumer for DocumentWriteParserMutationOwner<'_, '_, ' fn consume_parser_mutation_effects(&mut self, effects: DomMutationEffects) { if matches!( self.target, - DocumentWriteParserMutationTarget::DetachedFragment { .. } + DocumentWriteParserMutationTarget::DetachedFragment ) { return; } @@ -193,7 +183,7 @@ impl ParserDomMutationConsumer for DocumentWriteParserMutationOwner<'_, '_, '_> fn apply_parser_dom_mutation(&mut self, mutation: ParserDomMutation) { if matches!( self.target, - DocumentWriteParserMutationTarget::DetachedFragment { .. } + DocumentWriteParserMutationTarget::DetachedFragment ) { let _ = mutation .apply_to_detached_dom_host(self.runtime.dom_host_mut_for_active_parser_step()); @@ -250,7 +240,7 @@ impl ParserDomMutationConsumer for DocumentWriteParserMutationOwner<'_, '_, '_> // attributes also need registration without waiting for connection. let window_handlers = if matches!( self.target, - DocumentWriteParserMutationTarget::DetachedFragment { .. } + DocumentWriteParserMutationTarget::DetachedFragment ) || self.runtime.dom_host().is_connected(node_id) { crate::native_bridge::element::ParserAddedBodyWindowHandlers::capture( diff --git a/moli-renderer-v8/src/document_runtime/events.rs b/moli-renderer-v8/src/document_runtime/events.rs index 15094ed189..b1d78fe0d7 100644 --- a/moli-renderer-v8/src/document_runtime/events.rs +++ b/moli-renderer-v8/src/document_runtime/events.rs @@ -356,6 +356,12 @@ impl DocumentRuntime { self.timeouts.has_ready_timer() } + // Rendering wakes are independent of callbacks queued by an API. + #[cfg(test)] + pub(crate) fn has_ready_callback_timer(&self) -> bool { + self.timeouts.has_ready_callback_timer() + } + pub(crate) fn next_ready_timeout_deadline( &self, selection: crate::page_task_queue::RendererPageTimerSelection, diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index c69218bdfe..a542cc23a6 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -2476,8 +2476,7 @@ fn event_source_target_handle( host_ptr: *mut JsContextHost, event: v8::Local<'_, v8::Object>, ) -> Option { - let value = crate::context_bootstrap::event_backing(scope, event) - .get(scope, v8str(scope, "source").into())?; + let value = crate::context_bootstrap::event_attribute(scope, event, "source")?; if value.is_null_or_undefined() || !value.is_object() { return None; } diff --git a/moli-renderer-v8/src/host/timers.rs b/moli-renderer-v8/src/host/timers.rs index 367029535f..3190d58422 100644 --- a/moli-renderer-v8/src/host/timers.rs +++ b/moli-renderer-v8/src/host/timers.rs @@ -835,6 +835,18 @@ impl HostTimeoutScheduler { self.scheduler.has_ready_timer(Instant::now()) } + #[cfg(test)] + pub(crate) fn has_ready_callback_timer(&self) -> bool { + self.scheduler + .next_ready_deadline_matching(Instant::now(), min_delay_ready_allowance(), |task| { + !matches!( + task.callback, + ScheduledTimerCallback::AnimationFrameWake { .. } + ) + }) + .is_some() + } + pub(crate) fn has_ready_from_schedule_ranges(&self, ranges: &[TimerScheduleRange]) -> bool { self.scheduler .has_ready_from_schedule_ranges(ranges, Instant::now()) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs index ad13f02fd0..0f7a75a040 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs @@ -580,8 +580,16 @@ impl JsContextHost { } else { window.into() }; - let _ = crate::context_bootstrap::event_backing(scope, event).set(scope, v8str(scope, "target").into(), target); - let _ = crate::context_bootstrap::event_backing(scope, event).set(scope, v8str(scope, "currentTarget").into(), window.into()); + let _ = crate::context_bootstrap::event_backing(scope, event).set( + scope, + v8str(scope, "target").into(), + target, + ); + let _ = crate::context_bootstrap::event_backing(scope, event).set( + scope, + v8str(scope, "currentTarget").into(), + window.into(), + ); let _ = crate::context_bootstrap::event_backing(scope, event).set( scope, v8str(scope, "eventPhase").into(), diff --git a/moli-renderer-v8/src/native_bridge/context_host/document_parser_owner.rs b/moli-renderer-v8/src/native_bridge/context_host/document_parser_owner.rs index e8eac75798..d0488baad9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/document_parser_owner.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/document_parser_owner.rs @@ -566,7 +566,11 @@ impl ParserElementCreationConsumer for ContextDocumentParserOwner<'_, '_, '_> { request.intended_parent, |document_handle, local_name, namespace, prefix| { request.construction.create_element( - self.host.dom_host_mut(), document_handle, local_name, namespace, prefix, + self.host.dom_host_mut(), + document_handle, + local_name, + namespace, + prefix, ) }, ) diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs index fb75ece63b..1ee09daa08 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs @@ -2338,15 +2338,8 @@ impl JsContextHost { if style_mutation_effects_affect_layout_metric(effects) { self.clear_layout_rect_cache(); } - let changed_iframe_attributes = effects.iter().filter_map(|effect| { - let StyleMutationEffect::Attribute { element, name, .. } = effect else { - return None; - }; - (StyleAttributeImpact::for_attribute_name(name).affects_layout_metric() - && self.dom_host().is_html_element_named(*element, "iframe")) - .then_some(*element) - }); - self.invalidate_published_frame_viewports(changed_iframe_attributes); + // Frame viewports belong to the last published layout. Style mutations + // invalidate style inputs; the next layout publishes their new sizes. let validity_containers = self.validity_containers_for_child_list_mutations(effects); let dom_host = self.dom_host() as *const _; let emulated_media = self.emulated_media().clone(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout.rs b/moli-renderer-v8/src/native_bridge/context_host/layout.rs index 4c041b87d8..c7d040d1e5 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs @@ -185,42 +185,6 @@ impl JsContextHost { .frame_viewport(frame) } - pub(crate) fn invalidate_published_frame_viewports( - &self, - frames: impl IntoIterator, - ) { - let frames = frames.into_iter().collect::>(); - if frames.is_empty() { - return; - } - let documents = frames - .iter() - .filter_map(|frame| { - let document = self.dom_host().owner_document_handle(*frame)?; - self.top_level_document_for_document(document) - }) - .collect::>(); - let changed = { - let mut state = self.document_layout_state.borrow_mut(); - let changed = - state.update_frame_viewports(frames.into_iter().map(|frame| (frame, None))); - if documents.contains(&self.document_handle()) { - state.clear_latest_layout(); - } - changed - }; - for document in documents { - if let Some(cache) = self.lightweight_popup_layout_cache(document) { - cache.borrow_mut().clear(); - } - } - if changed { - self.style_viewport_generation - .set(self.style_viewport_generation.get().saturating_add(1)); - } - } - - #[cfg(debug_assertions)] pub(crate) fn style_viewport_generation(&self) -> u64 { self.style_viewport_generation.get() } diff --git a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs index 3b3adddf5b..e43cb30db6 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs @@ -8,9 +8,7 @@ use crate::runtime::{ }; use crate::service_worker_runtime::{ServiceWorkerClientId, ServiceWorkerClientNavigateError}; use moli_fetch::BrowserNavigationRequestKind; -use moli_page_types::{ - NavigationHistoryEntrySeed, NavigationHistoryMutation, -}; +use moli_page_types::{NavigationHistoryEntrySeed, NavigationHistoryMutation}; use url::Url; pub(crate) struct PendingReservedServiceWorkerClient { diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 4d7b69da47..8e7717eecc 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -425,13 +425,14 @@ pub use geometry::ClientRect; #[cfg(test)] pub(crate) use geometry::observable_scrollbar_hit_test; pub(crate) use geometry::{ - element_is_inert_for_hit_testing, InputSurfaceHit, apply_scroll_observable_effects, input_surface_hit_test, - observable_caret_position, observable_deep_hit_test, observable_document_metrics, - observable_event_offset, observable_geometry_batch, observable_geometry_query, - observable_hit_test, observable_hit_test_all, observable_input_hit_test, - observable_sources_with_fragments, perform_scrollbar_scroll_default_action, - perform_wheel_scroll_default_action, published_geometry_batch, queue_scroll_observable_effects, - scroll_node_into_view_at_center, scroll_node_into_view_at_start, + InputSurfaceHit, apply_scroll_observable_effects, element_is_inert_for_hit_testing, + input_surface_hit_test, observable_caret_position, observable_deep_hit_test, + observable_document_metrics, observable_event_offset, observable_geometry_batch, + observable_geometry_query, observable_hit_test, observable_hit_test_all, + observable_input_hit_test, observable_sources_with_fragments, + perform_scrollbar_scroll_default_action, perform_wheel_scroll_default_action, + published_geometry_batch, queue_scroll_observable_effects, scroll_node_into_view_at_center, + scroll_node_into_view_at_start, }; pub(super) use geometry::{ node_client_height_getter_function, node_client_left_getter_function, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/user_interaction.rs b/moli-renderer-v8/src/runtime/page_vm/tests/user_interaction.rs index 2f94d34a5b..7406bbdac2 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/user_interaction.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/user_interaction.rs @@ -545,8 +545,8 @@ getSelection().collapse(text, 1); )?; assert!( - !page_vm.vm().has_ready_timeout(), - "user-interaction tasks must not acquire PageTimer descriptors" + !page_vm.vm().has_ready_callback_timer(), + "user-interaction callbacks must stay on their task source; rendering wakes are separate" ); // The value setter already queued selectionchange before the range // setter queues select; later selection changes coalesce with it. diff --git a/moli-renderer-v8/src/runtime/tests/extracted/owner_lifecycle.rs b/moli-renderer-v8/src/runtime/tests/extracted/owner_lifecycle.rs index 77ae593ae1..567a7bd061 100644 --- a/moli-renderer-v8/src/runtime/tests/extracted/owner_lifecycle.rs +++ b/moli-renderer-v8/src/runtime/tests/extracted/owner_lifecycle.rs @@ -426,7 +426,9 @@ document.body.appendChild(frame); } let (observed, _) = page .run_async_command(RendererPageCommand::EvaluateExpression { - expression: "globalThis.__childSelfNavigateEvents.join('|')".to_owned(), + // Each owner-driven completion is required exactly once. Posted + // messages and load events use separate task sources. + expression: "globalThis.__childSelfNavigateEvents.slice().sort().join('|')".to_owned(), await_promise: false, }) .await diff --git a/moli-renderer-v8/src/runtime/tests/extracted/page_lifecycle_isolation.rs b/moli-renderer-v8/src/runtime/tests/extracted/page_lifecycle_isolation.rs index 609b847868..6ba3026963 100644 --- a/moli-renderer-v8/src/runtime/tests/extracted/page_lifecycle_isolation.rs +++ b/moli-renderer-v8/src/runtime/tests/extracted/page_lifecycle_isolation.rs @@ -319,9 +319,16 @@ async fn per_page_isolate_policy_routes_unhandled_rejections_to_originating_page expression: r#"(() => { globalThis.__lm_shared_isolate_unhandled_rejections = []; globalThis.__lm_shared_isolate_rejectionhandled = []; + globalThis.__lm_shared_isolate_unhandled_done = new Promise(resolve => { + globalThis.__lm_shared_isolate_resolve_unhandled = resolve; + }); + globalThis.__lm_shared_isolate_handled_done = new Promise(resolve => { + globalThis.__lm_shared_isolate_resolve_handled = resolve; + }); addEventListener("unhandledrejection", event => { event.preventDefault(); globalThis.__lm_shared_isolate_unhandled_rejections.push(String(event.reason)); + globalThis.__lm_shared_isolate_resolve_unhandled(); }); addEventListener("rejectionhandled", event => { globalThis.__lm_shared_isolate_rejectionhandled.push( @@ -329,6 +336,7 @@ async fn per_page_isolate_policy_routes_unhandled_rejections_to_originating_page ? "same-promise" : "wrong-promise" ); + globalThis.__lm_shared_isolate_resolve_handled(); }); return "installed"; })()"# @@ -359,13 +367,18 @@ async fn per_page_isolate_policy_routes_unhandled_rejections_to_originating_page Some(serde_json::json!("scheduled")) ); - let (second_rejections, _) = second_page - .run_async_command(RendererPageCommand::EvaluateExpression { - expression: r#"JSON.stringify(globalThis.__lm_shared_isolate_unhandled_rejections)"# + // Rejection notifications are queued tasks. A later Evaluate command is + // not a barrier for their delivery; await the originating page's event. + let (second_rejections, _) = tokio::time::timeout( + Duration::from_secs(2), + second_page.run_async_command(RendererPageCommand::EvaluateExpression { + expression: r#"__lm_shared_isolate_unhandled_done.then(() => JSON.stringify(__lm_shared_isolate_unhandled_rejections))"# .to_owned(), - await_promise: false, - }) + await_promise: true, + }), + ) .await + .expect("second page must receive its unhandled rejection task") .expect("second page rejection list should evaluate"); assert_eq!( renderer_json_value(second_rejections), @@ -403,13 +416,16 @@ async fn per_page_isolate_policy_routes_unhandled_rejections_to_originating_page Some(serde_json::json!("handler-added")) ); - let (second_rejectionhandled, _) = second_page - .run_async_command(RendererPageCommand::EvaluateExpression { - expression: r#"JSON.stringify(globalThis.__lm_shared_isolate_rejectionhandled)"# + let (second_rejectionhandled, _) = tokio::time::timeout( + Duration::from_secs(2), + second_page.run_async_command(RendererPageCommand::EvaluateExpression { + expression: r#"__lm_shared_isolate_handled_done.then(() => JSON.stringify(__lm_shared_isolate_rejectionhandled))"# .to_owned(), - await_promise: false, - }) + await_promise: true, + }), + ) .await + .expect("second page must receive its rejectionhandled task") .expect("second page rejectionhandled list should evaluate"); assert_eq!( renderer_json_value(second_rejectionhandled), diff --git a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs index f2597930d2..e0efe95298 100644 --- a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs +++ b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs @@ -857,6 +857,12 @@ impl ScriptVm { self.document_runtime.has_ready_timeout() } + // Rendering wakes are independent of callbacks queued by an API. + #[cfg(test)] + pub(crate) fn has_ready_callback_timer(&self) -> bool { + self.document_runtime.has_ready_callback_timer() + } + pub(crate) fn next_ready_timeout_deadline( &self, selection: crate::page_task_queue::RendererPageTimerSelection, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs index 6b1af1a37d..079d81c1f2 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs @@ -1210,6 +1210,7 @@ async fn history_worlds_child_mutations_share_only_the_child_window_history() { r##" history.replaceState({owner: 'top'}, '', '#top'); const frame = document.createElement('iframe'); + frame.srcdoc = '

History owner

'; (document.body || document.documentElement || document).appendChild(frame); void frame.contentWindow; 'created' @@ -1224,6 +1225,11 @@ async fn history_worlds_child_mutations_share_only_the_child_window_history() { .await .unwrap() ); + // Traversal needs a committed Document: pushState replaces the entry of + // the initial empty Document rather than appending one. + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); let child = vm .live_child_default_runtime_realm_inventory() .into_iter() @@ -1239,7 +1245,7 @@ async fn history_worlds_child_mutations_share_only_the_child_window_history() { vm.eval_in_child_default_context( child, r##" - history.replaceState({owner: 'child'}, '', '#child'); + history.replaceState({owner: 'child'}, '', 'about:srcdoc#child'); history.scrollRestoration = 'manual'; 'ready' "##, @@ -1259,7 +1265,7 @@ async fn history_worlds_child_mutations_share_only_the_child_window_history() { vm.eval_in_isolated_context( isolated, r##" - history.pushState({owner: 'isolated-child'}, '', '#next'); + history.pushState({owner: 'isolated-child'}, '', 'about:srcdoc#next'); history.scrollRestoration = 'auto'; 'pushed' "##, @@ -1353,6 +1359,8 @@ async fn history_worlds_preserve_the_shared_backing_when_initial_child_window_is vm.eval( r#" const frame = document.createElement('iframe'); + // Keep the initial empty Window pending until the srcdoc commit. + frame.srcdoc = '

pending initial load

'; (document.body || document.documentElement || document).appendChild(frame); void frame.contentWindow; "#, @@ -1376,7 +1384,7 @@ async fn history_worlds_preserve_the_shared_backing_when_initial_child_window_is vm.eval_in_isolated_context( isolated, r##" - originalHistory.replaceState({rebound: true}, '', '#rebound'); + originalHistory.replaceState({rebound: true}, '', 'about:srcdoc#rebound'); 'replaced' "##, ) @@ -1432,7 +1440,7 @@ fn history_native_snapshot_survives_isolate_replacement_with_structured_values() history.scrollRestoration = 'manual'; navigation.updateCurrentEntry({state: new Set([7n])}); history.state.map.set('answer', 99n); - location.href = '/next'; + navigation.navigate('/next', {history: 'push'}); 'queued' "##).unwrap(); let pending = vm.take_pending_location_navigation_with_seed().unwrap(); @@ -1476,7 +1484,7 @@ fn history_native_snapshot_survives_isolate_replacement_with_structured_values() } #[test] -fn history_restored_entries_keep_world_identity_and_refresh_reused_ids() { +fn history_restored_sparse_entries_keep_world_identity_and_refresh_reused_ids() { const ENTRIES: &str = r#"JSON.stringify(navigation.entries() .filter(entry => entry.url.includes('#')) .map(entry => [entry.url, entry.id, entry.key, entry.index, entry.sameDocument, @@ -1496,7 +1504,8 @@ fn history_restored_entries_keep_world_identity_and_refresh_reused_ids() { ) .unwrap(); let expected = vm.eval(ENTRIES).unwrap(); - vm.eval("location.href = '/next'; 'queued'").unwrap(); + vm.eval("navigation.navigate('/next', {history: 'push'}); 'queued'") + .unwrap(); let mut seed = vm .take_pending_location_navigation_with_seed() .unwrap() @@ -1509,7 +1518,13 @@ fn history_restored_entries_keep_world_identity_and_refresh_reused_ids() { .unwrap() .history_index; seed.activation = None; - // Snapshot order is not the history position stored in each snapshot. + // Snapshot positions may be sparse and do not follow serialization order. + // The current serialized position still fits in the dense vector, where + // using it as an offset would silently select the wrong entry. + seed.current_index *= 3; + for entry in &mut seed.entries { + entry.history_index *= 3; + } seed.entries.reverse(); (seed, expected) }; @@ -1595,5 +1610,84 @@ fn history_native_fragment_navigation_preserves_structured_navigation_state() { "##, ) .unwrap(); - assert_eq!(vm.eval("String(history.state.value === 5n && navigation.currentEntry.getState().get('value') === 9n && retained.getState().get('value') === 9n)").unwrap(), "true"); + assert_eq!(vm.eval("String(history.state === null && navigation.currentEntry.getState().get('value') === 9n && retained.getState().get('value') === 9n)").unwrap(), "true"); +} + +#[test] +fn event_worlds_beforeunload_return_value_uses_shared_backing() { + let mut vm = new_storage_test_vm("https://example.com/base"); + vm.eval( + r#" + globalThis.event = document.createEvent('BeforeUnloadEvent'); + event.initEvent('beforeunload', false, true); + event.returnValue = 'original'; + event.pageOnly = true; + 'ready' + "#, + ) + .unwrap(); + let isolated = vm + .create_isolated_world("beforeunload-state", false) + .unwrap(); + vm.eval_in_isolated_context( + isolated, + r#" + globalThis.facts = []; + navigation.addEventListener('beforeunload', event => { + facts.push(event instanceof BeforeUnloadEvent, event.returnValue, + event.pageOnly === undefined); + event.returnValue = 'updated'; + event.preventDefault(); + }, {once: true}); + 'ready' + "#, + ) + .unwrap(); + assert_eq!(vm.eval("JSON.stringify([navigation.dispatchEvent(event), event.returnValue, event.defaultPrevented])").unwrap(), + r#"[false,"updated",true]"#); + assert_eq!( + vm.eval_in_isolated_context(isolated, "JSON.stringify(facts)") + .unwrap(), + r#"[true,"original",true]"# + ); +} + +#[tokio::test] +async fn event_worlds_precommit_controllers_expire_after_navigation_settles() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let mut vm = new_storage_test_vm_with_loader("https://example.com/base", &loader); + let isolated = vm.create_isolated_world("precommit-state", false).unwrap(); + vm.eval_in_isolated_context( + isolated, + r##" + globalThis.handlers = 0; + navigation.addEventListener('navigate', event => { + event.intercept({precommitHandler(controller) { + globalThis.savedController = controller; + controller.addHandler(() => { handlers++; }); + }}); + }, {once: true}); + 'ready' + "##, + ) + .unwrap(); + vm.eval("navigation.navigate('#next'); 'started'").unwrap(); + vm.advance_timers_until_deadline_for_test(&loader) + .await + .unwrap(); + assert_eq!( + vm.eval_in_isolated_context( + isolated, + r#" + (() => { + let errorName = ''; + try { savedController.addHandler(() => {}); } + catch (error) { errorName = error.name; } + return JSON.stringify([location.hash, handlers, errorName]); + })() + "# + ) + .unwrap(), + r##"["#next",1,"InvalidStateError"]"## + ); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs index 303e0f036c..7e8b1960fc 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs @@ -1246,83 +1246,6 @@ async fn navigation_destination_uses_native_webidl_receivers_and_live_entry_stat } } -#[tokio::test] -async fn named_element_navigation_prefers_its_source_frame_over_duplicate_names() { - for action in ["anchor", "submit", "requestSubmit"] { - for name in ["initial", "renamed", "shadowed"] { - let server = - StaticHttpServer::spawn_with_bodies(vec!["selected".into()]) - .await; - let base = server.base_url().origin().ascii_serialization(); - let loader = static_http_loader([]); - let mut vm = new_storage_page_task_executor_test_vm_with_loader( - &format!("{base}/parent"), - &loader, - ); - vm.eval(&format!( - r#" - const earlier = document.createElement('iframe'); earlier.name = 'same'; - const source = document.createElement('iframe'); - source.name = {name:?} === 'renamed' ? 'old-name' : 'same'; - document.body.append(earlier, source); - const childDocument = source.contentDocument; - const nested = childDocument.createElement('iframe'); nested.name = 'same'; - childDocument.body.append(nested); - globalThis.originalDocument = childDocument; - globalThis.nameReads = 0; - if ({name:?} === 'renamed') source.contentWindow.name = 'same'; - if ({name:?} === 'shadowed') Object.defineProperty(source.contentWindow, 'name', {{ - get() {{ ++nameReads; throw new Error('script-visible name was read'); }} - }}); - globalThis.namedAccessIsChild = source.contentWindow.same === nested.contentWindow; - "# - )) - .unwrap(); - vm.drain_ready_page_task_executor_turns_for_setup(&loader, 100) - .await - .unwrap(); - vm.eval(&format!( - r#" - const element = childDocument.createElement({action:?} === 'anchor' ? 'a' : 'form'); - element.target = 'same'; - if ({action:?} === 'anchor') element.href = '/selected?from=anchor'; - else {{ - element.action = '/selected'; - element.innerHTML = ''; - }} - childDocument.body.append(element); - if ({action:?} === 'anchor') element.click(); - else element[{action:?}](); - "# - )) - .unwrap(); - advance_page_task_executor_until_eval_equals( - &mut vm, - &loader, - "String(source.contentDocument.body?.textContent === 'selected')", - "true", - &format!("{action}, {name}"), - ) - .await; - assert_eq!( - vm.eval( - "JSON.stringify([source.contentDocument !== originalDocument, \ - source.contentWindow.length, earlier.contentWindow.location.href, \ - namedAccessIsChild, nameReads])", - ) - .unwrap(), - r#"[true,0,"about:blank",true,0]"#, - "{action}, {name}", - ); - assert_eq!( - server.finish_targets().await, - [format!("/selected?from={action}")], - "{action}, {name}", - ); - } - } -} - #[tokio::test] async fn document_open_preserves_navigation_initialization() { for kind in ["iframe", "popup"] { @@ -1362,7 +1285,7 @@ async fn document_open_preserves_navigation_initialization() { "snapshots": snapshots, "navigated": { "current": "about:blank", "state": 4 }, "loadedOpen": { - "sameNavigation": true, "sameEntryKey": true, "state": 5, + "sameNavigation": true, "sameEntryKey": true, "newPublicId": true, "state": 5, }, }), "{kind}" diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/history_traversal.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/history_traversal.rs index 35b2c86774..4ff821a253 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/history_traversal.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/history_traversal.rs @@ -386,12 +386,11 @@ else frame.setAttribute('sandbox', {changed}); "history.replaceState(0, ''); history.pushState(1, ''); frame.contentWindow.eval('history.back()');", ) .unwrap(); - assert_eq!( + assert!( vm.run_one_history_traversal_executor_turn(&loader) .await .unwrap(), - allowed, - "initial={initial}, changed={changed}, navigated={navigate}" + "the traversal task evaluates the active sandbox: initial={initial}, changed={changed}, navigated={navigate}" ); assert_eq!( vm.eval("String(history.state)").unwrap(), diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs index 4d1614fa3b..25bd90f71e 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs @@ -346,6 +346,11 @@ frame.contentWindow; ] { vm.eval(&format!("frame.contentWindow.history.{call}")) .unwrap(); + assert!( + vm.run_one_history_traversal_executor_turn(&loader) + .await + .unwrap() + ); let pending = vm .take_pending_top_level_history_traversal() .map(|pending| pending.delta); @@ -358,6 +363,11 @@ frame.contentWindow; // A method borrowed from the child still uses its top-level History receiver. vm.eval("frame.contentWindow.history.back.call(history)") .unwrap(); + assert!( + vm.run_one_history_traversal_executor_turn(&loader) + .await + .unwrap() + ); assert_eq!( vm.take_pending_top_level_history_traversal().unwrap().delta, -1 diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/stylesheet_and_document_lifecycle.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/stylesheet_and_document_lifecycle.rs index 0b1071433c..dfeac6be48 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/stylesheet_and_document_lifecycle.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/stylesheet_and_document_lifecycle.rs @@ -1075,10 +1075,10 @@ fn computed_used_size_clamps_infinite_negative_math_to_zero() { fn computed_style_child_document_media_queries_use_iframe_viewport() { let mut vm = new_storage_test_vm("https://child-document-media-query.test/"); - let result = vm - .eval( - r#" -(() => { + let result = eval_with_layout_publications( + &mut vm, + r#" +(function* () { if (!document.documentElement) { document.appendChild(document.createElement('html')); } @@ -1096,12 +1096,13 @@ fn computed_style_child_document_media_queries_use_iframe_viewport() { document.body.offsetTop; const before = getComputedStyle(childDocument.body).color; frame.style.width = '200px'; + yield; // Publish the new viewport before evaluating the child's media query. const after = getComputedStyle(childDocument.body).color; return `${before}|${after}`; })() "#, - ) - .expect("child document media queries should use iframe viewport"); + ) + .expect("child document media queries should use iframe viewport"); assert_eq!(result, "rgb(255, 0, 0)|rgb(0, 128, 0)"); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/cors_preflight.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/cors_preflight.rs index 11b550255f..000adc6886 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/cors_preflight.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/cors_preflight.rs @@ -424,5 +424,5 @@ async fn cors_preflight_permissions_reach_window_fetch_and_xhr() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn cors_preflight_permissions_reach_worker_fetch_and_xhr() { - check_preflight_permissions(true, &["fetch", "xhr"]).await; + check_preflight_permissions(true, &["fetch", "xhr", "sync-xhr"]).await; } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs index df61853753..0c3bfaf7d3 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs @@ -558,8 +558,8 @@ async fn dialog_form_submission_closes_with_submitter_result_and_queues_reentran ); assert!( - !vm.has_ready_timeout(), - "dialog close must not create a synthetic Page timer" + !vm.has_ready_callback_timer(), + "dialog close must not create a callback timer; focus rendering wakes are separate" ); assert!( vm.run_one_dom_manipulation_task_executor_turn( diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 2e9e475cd3..5d3d37c7cb 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2246,6 +2246,7 @@ mod geometry_point_conversion_order; mod headers_list; mod history_document_identity; mod history_referrer; +mod history_replace_forward; mod http_fixture; mod media_owner_playback_interfaces; mod media_recorder_shell; @@ -2310,7 +2311,6 @@ mod webidl_collections; mod webidl_fetch; mod webidl_receivers; mod webidl_trusted_types; -mod history_replace_forward; mod websocket; mod window_cross_origin_getters; mod window_event_receivers; diff --git a/moli-renderer-v8/src/script_vm/tests/observer_documents.rs b/moli-renderer-v8/src/script_vm/tests/observer_documents.rs index 417d64aa1d..7f870bf282 100644 --- a/moli-renderer-v8/src/script_vm/tests/observer_documents.rs +++ b/moli-renderer-v8/src/script_vm/tests/observer_documents.rs @@ -151,7 +151,7 @@ observer.observe(popupTarget); } #[tokio::test(flavor = "current_thread")] -async fn popup_iframe_invalidation_preserves_the_opener_layout() { +async fn popup_iframe_resize_preserves_the_opener_layout() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); let mut vm = new_storage_page_task_executor_test_vm("https://popup-frame-layout.test/"); vm.eval( @@ -162,6 +162,8 @@ mainTarget.style.cssText = 'width:80px;height:20px'; globalThis.popupTarget = popup.document.body.appendChild(popup.document.createElement('div')); popupTarget.style.cssText = 'width:40px;height:20px'; globalThis.frame = popup.document.body.appendChild(popup.document.createElement('iframe')); +frame.style.border = '0'; +frame.setAttribute('width', '300'); frame.contentWindow.document.body.innerHTML = '
child
'; globalThis.observer = new ResizeObserver(() => {}); observer.observe(mainTarget); @@ -173,7 +175,15 @@ observer.observe(popupTarget); .await .unwrap(); assert_eq!(vm.eval("mainTarget.getBoundingClientRect().width + ':' + popupTarget.getBoundingClientRect().width").unwrap(), "80:40"); + assert_eq!( + vm.eval("frame.getBoundingClientRect().width").unwrap(), + "300" + ); vm.eval("frame.setAttribute('width', '200');").unwrap(); + assert_eq!( + vm.eval("frame.getBoundingClientRect().width").unwrap(), + "300" + ); vm.with_default_context_scope_and_checkpoint_for_test(|scope, host_ptr| { let global = scope.get_current_context().global(scope); let key = v8::String::new(scope, "popupTarget").unwrap(); @@ -188,8 +198,8 @@ observer.observe(popupTarget); let document = host.dom_host().owner_document_handle(handle).unwrap(); assert!( host.with_latest_layout_tree_for_document(document, |_| ()) - .is_none(), - "changing iframe width must invalidate its popup snapshot" + .is_some(), + "changing iframe width must retain the published popup snapshot until rendering" ); assert!( host.with_latest_layout_tree_for_document(host.document_handle(), |_| ()) @@ -203,6 +213,10 @@ observer.observe(popupTarget); .await .unwrap(); assert_eq!(vm.eval("mainTarget.getBoundingClientRect().width + ':' + popupTarget.getBoundingClientRect().width").unwrap(), "80:40"); + assert_eq!( + vm.eval("frame.getBoundingClientRect().width").unwrap(), + "200" + ); vm.eval("frame.remove();").unwrap(); vm.with_default_context_scope_and_checkpoint_for_test(|_scope, host_ptr| { let host = unsafe { &*host_ptr }; diff --git a/moli-renderer-v8/src/util.rs b/moli-renderer-v8/src/util.rs index 8562665633..6f5d58d1c6 100644 --- a/moli-renderer-v8/src/util.rs +++ b/moli-renderer-v8/src/util.rs @@ -9,7 +9,7 @@ pub use moli_v8_util::{ global_constructor_object, global_constructor_prototype, initialize_ecmascript_intrinsic_registry, new_null_prototype_object, object_bool_property, object_chain_contains, object_defined_string_property, object_non_empty_string_property, - object_number_property, object_own_static_bool_property, object_own_static_property_as_array, + object_number_property, object_own_static_bool_property, object_own_static_string_property, object_property_as_array, object_property_as_object, object_string_property, private_key, register_ecmascript_intrinsic, registered_ecmascript_constructor, registered_ecmascript_prototype, set_null_prototype, diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index 5c9a737967..9bde98d251 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -109,8 +109,8 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[ ), allowed( "context_bootstrap/navigation_callbacks/navigation.rs", - // Three settlement calls now go through the existing shared helpers. - 8, + // Fulfillment paths share the realm-aware lifecycle settlement helper. + 7, DirectCallOwner::BrowserAlgorithm, ), allowed( diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index 4705a0b099..f54ce2df68 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -476,7 +476,11 @@ pub(super) fn event_target_dispatch_event_callback<'s>( event_type.as_deref().unwrap_or_default(), event, ); - Ok(!crate::context_bootstrap::event_bool_attribute(scope, event, "defaultPrevented")) + Ok(!crate::context_bootstrap::event_bool_attribute( + scope, + event, + "defaultPrevented", + )) } else { host.dispatch_public_event(scope, host_ptr, EventTargetHandle::Window, event) .map(|dispatch| { diff --git a/moli-renderer-v8/tests/fixtures/navigation-document-open.js b/moli-renderer-v8/tests/fixtures/navigation-document-open.js index 91604220ed..83116b1e9b 100644 --- a/moli-renderer-v8/tests/fixtures/navigation-document-open.js +++ b/moli-renderer-v8/tests/fixtures/navigation-document-open.js @@ -48,6 +48,7 @@ globalThis.navigationDocumentOpenProbe = async function(kind) { state: loadedNav.currentEntry.getState().value }; const key = loadedNav.currentEntry.key; + const id = loadedNav.currentEntry.id; child.document.open(); child.document.write('

loaded stream

'); child.document.close(); @@ -55,6 +56,7 @@ globalThis.navigationDocumentOpenProbe = async function(kind) { const loadedOpen = { sameNavigation: child.navigation === loadedNav, sameEntryKey: loadedNav.currentEntry.key === key, + newPublicId: loadedNav.currentEntry.id !== id && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(loadedNav.currentEntry.id), state: loadedNav.currentEntry.getState().value }; return {snapshots, navigated, loadedOpen};