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};