diff --git a/moli-core/tests/fetch_behaviors/font_binary.rs b/moli-core/tests/fetch_behaviors/font_binary.rs index 93e3a37b40..607c4898f4 100644 --- a/moli-core/tests/fetch_behaviors/font_binary.rs +++ b/moli-core/tests/fetch_behaviors/font_binary.rs @@ -23,7 +23,7 @@ async fn font_face_binary_sources_validate_payloads_and_buffer_conversion_order( .context("binary FontFace results")?, )?; assert_eq!(value["failures"], serde_json::json!([]), "{value}"); - assert_eq!(value["rows"].as_array().unwrap().len(), 52, "{value}"); + assert_eq!(value["rows"].as_array().unwrap().len(), 58, "{value}"); server.shutdown().await; Ok(()) } diff --git a/moli-core/tests/web_apis/indexed_db_transaction.rs b/moli-core/tests/web_apis/indexed_db_transaction.rs index 3472600ac4..c1c4b48fdb 100644 --- a/moli-core/tests/web_apis/indexed_db_transaction.rs +++ b/moli-core/tests/web_apis/indexed_db_transaction.rs @@ -1,26 +1,5 @@ use super::event_dispatch::run_probe; use super::*; -#[tokio::test(flavor = "multi_thread")] -async fn indexed_db_key_ranges_protect_bounds_and_validate_receivers() -> Result<()> { - let server = FixtureServer::spawn().await?; - let browser = Browser::new(AppConfig::default())?; - let fixture = include_str!("fixtures/indexeddb-key-range.js"); - for target in ["window", "child", "worker"] { - let source = format!( - "{fixture}\nkeyRangeProbe('key-range-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: rangeChecks}}));" - ); - let result = run_probe(&browser, &server, target, &source).await?; - assert_eq!(result["state"], "pass", "{target}: {result}"); - assert_eq!( - result["checks"].as_array().unwrap().len(), - if target == "worker" { 301 } else { 313 }, - "{target}: {result}" - ); - } - server.shutdown().await; - Ok(()) -} - #[tokio::test(flavor = "multi_thread")] async fn indexed_db_database_names_preserve_utf16_identity_across_realms_and_queues() -> Result<()> { @@ -308,7 +287,7 @@ async fn indexed_db_key_ranges_protect_bounds_and_validate_receivers() -> Result assert_eq!(result["state"], "pass", "{target}: {result}"); assert_eq!( result["checks"].as_array().unwrap().len(), - if target == "worker" { 317 } else { 329 }, + if target == "worker" { 341 } else { 353 }, "{target}: {result}" ); } diff --git a/moli-protocol/src/domains/network/main_document_progress/tests.rs b/moli-protocol/src/domains/network/main_document_progress/tests.rs index f6d13df034..b1b4c6fb3e 100644 --- a/moli-protocol/src/domains/network/main_document_progress/tests.rs +++ b/moli-protocol/src/domains/network/main_document_progress/tests.rs @@ -1612,6 +1612,7 @@ fn no_content_navigation_retains_response_metadata_with_live_progress() { request_load_policy: NavigationRequestLoadPolicy::BrowserInitiated, timestamp: 12.5, source_document_security: Default::default(), + navigation_history: None, }; let (live_source, _receiver) = live_progress_source(); let source = MainDocumentBodyProgressSource::from_live_source( @@ -1625,14 +1626,12 @@ fn no_content_navigation_retains_response_metadata_with_live_progress() { final_url: Url::parse("http://example.test/final").unwrap(), network_events: source.completed_body_network_events(events), }; - let MaterializedNavigationLoadOutcome::Failed(mut failed) = - materialize_no_content_navigation_progress(&conn, &state, navigation) + let MaterializedNavigationLoadOutcome::NoDocument(mut progress_gate) = + materialize_no_content_navigation_progress(&mut conn, &state, navigation) else { - panic!("ignored responses must retain main's failed-navigation projection"); + panic!("ignored responses must preserve the committed Document"); }; - let out = drain_gate_until_body_finished_visible_into_protocol_messages( - &mut failed.progress_gate, - ); + let out = drain_gate_until_body_finished_visible_into_protocol_messages(&mut progress_gate); let response = out .iter() .find(|event| event["method"] == "Network.responseReceived") @@ -1651,9 +1650,9 @@ fn no_content_navigation_retains_response_metadata_with_live_progress() { failure["params"]["errorText"], json!(moli_fetch::NET_ERR_ABORTED_ERROR_TEXT) ); - assert_eq!( - failed.document_policy, - FailedNavigationDocumentPolicy::PreserveCommittedDocument + assert!( + !out.iter() + .any(|event| event["method"] == "Page.frameNavigated") ); assert!( !out.iter() diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index feabfee009..7129b55c09 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -105,7 +105,7 @@ mod navigation_traversal_plan; mod navigation_window; pub(crate) use navigation_window::{ navigation_unload_event_active, replace_navigation_unload_event_active, - runtime_window_dispatch_scope, window_location_for_holder, + window_location_for_holder, }; mod navigator_runtime; mod platform_object_worlds; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs index df4c1383ce..c17c9f2980 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs @@ -12,7 +12,7 @@ use super::{ parse_cursor_direction, parse_cursor_direction_with_context, parse_idb_key_path, parse_key_or_range, parse_key_range_from_value, parse_optional_count, prepare_object_store_write, remove_database_index_metadata, request_error_object, - require_idb_key, scan_index_entries, scan_object_store_entries, set_database_index_metadata, + scan_index_entries, scan_object_store_entries, set_database_index_metadata, store_request_error, store_request_success, submit_cursor_open_operation, transaction_handle_from_value, with_indexed_db_manager, }; diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index 7c62a7ca3d..309b081d2f 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -17,6 +17,7 @@ use super::navigation_entry_state::clone_navigation_entry_state; use super::navigation_events::{ NavigationDispatchOutcome, cancel_active_navigation_event, dispatch_cross_document_navigation_navigate_event_for_window_with_type_and_form_data, + dispatch_navigation_navigate_event_with_form_data_and_outcome, dispatch_navigation_navigate_event_with_outcome, dispatch_popstate_event, finish_navigation_precommit, queue_hash_change_for_runtime_owner, }; diff --git a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs index aaaa48a9ac..dc8f0b9aa7 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs @@ -36,8 +36,8 @@ use super::super::{ set_reflected_boolean_attribute, submit_form_with_submit_event, update_focus, }; use super::targets::{ - SpecialBrowsingContextTarget, named_iframe_target_handle_for_navigation, - navigate_element_target_browsing_context, + SpecialBrowsingContextTarget, browsing_context_window_for_dispatch_scope, + named_iframe_target_handle_for_navigation, navigate_element_target_browsing_context, }; fn array_like_length(scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>) -> u32 { diff --git a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs index dd4677766d..d14f6369b0 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs @@ -1,7 +1,6 @@ use crate::{ RendererPendingPopupActivation, RendererPendingWindowOpenEvent, RendererPopupDisposition, document_runtime::{DocumentPolicyContainer, DomHandle}, - util::v8str, }; use super::super::super::JsContextHost; @@ -35,37 +34,6 @@ impl SpecialBrowsingContextTarget { } } -fn navigate_target_window_location( - scope: &mut v8::PinScope<'_, '_>, - window: v8::Local<'_, v8::Object>, - resolved_url: &str, -) -> bool { - let Some(value) = crate::util::v8_string(scope, resolved_url) else { - return false; - }; - window - .set(scope, v8str(scope, "location").into(), value.into()) - .unwrap_or(false) -} - -fn queue_top_level_location_navigation( - scope: &mut v8::PinScope<'_, '_>, - runtime_ptr: *mut JsContextHost, - resolved_url: &str, - source: crate::native_bridge::OwnerDispatchScope, -) -> bool { - let Ok(url) = url::Url::parse(resolved_url) else { - return false; - }; - let runtime = unsafe { &mut *runtime_ptr }; - let about_document_state = runtime.capture_about_document_state(source, &url, None); - if url.scheme() != "javascript" && !runtime.dispatch_main_document_tree_beforeunload(scope) { - return false; - } - runtime.record_pending_location_navigation(url, None, about_document_state); - true -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct ElementPopupRelations { suppress_opener: bool, @@ -646,8 +614,17 @@ pub(crate) fn navigate_existing_browsing_context_target<'s>( let Some(resolved_url) = resolved_url else { return Some(target_window); }; - let navigated = navigate_target_window_location(scope, target_window, resolved_url); - if navigated { Some(target_window) } else { None } + let location = crate::context_bootstrap::window_location_for_holder(scope, target_window)?; + // Window.open navigates directly. The before-load replacement rule belongs + // to Location-object navigation and must not affect this history handling. + crate::context_bootstrap::navigate_location_object_with_source_element( + scope, + location, + crate::context_bootstrap::LocationNavigationKind::Assign, + Some(resolved_url.to_owned()), + None, + ); + Some(target_window) } #[derive(Clone, Copy)] 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 f854dd01fc..578e703820 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 @@ -355,32 +355,65 @@ fn hyperlink_top_navigation_checks_source_document_sandbox_across_realms() { } } -#[test] -fn form_navigation_events_use_resolved_top_window_across_realms() { +async fn load_form_navigation_frames( + vm: &mut crate::runtime::PageVmTaskExecutorTestHarness, + loader: &ResourceRequestClient, + depth: usize, +) { + vm.eval("globalThis.formNavigationSource = document; globalThis.formNavigationFrames = [];") + .unwrap(); + for _ in 0..depth { + vm.eval( + r#" + globalThis.formNavigationFrame = formNavigationSource.createElement('iframe'); + formNavigationFrame.srcdoc = ''; + formNavigationSource.body.appendChild(formNavigationFrame); + formNavigationFrames.push(formNavigationFrame); + "#, + ) + .unwrap(); + advance_page_task_executor_until_eval_equals( + vm, + loader, + "String(formNavigationFrame.contentDocument.URL === 'about:srcdoc' && \ + formNavigationFrame.contentDocument.readyState === 'complete')", + "true", + "form navigation source frame", + ) + .await; + vm.eval("formNavigationSource = formNavigationFrame.contentDocument;") + .unwrap(); + } + vm.drain_ready_page_task_executor_turns_for_setup(loader, 100) + .await + .unwrap(); +} + +#[tokio::test] +async fn form_navigation_events_use_resolved_top_window_across_realms() { for (depth, target) in [(1, "_top"), (1, "_PARENT"), (2, "_ToP")] { for method in ["get", "post"] { for api in ["submit", "requestSubmit"] { for use_child_realm in [true, false] { for cancel_top in [true, false] { - let mut vm = new_parsed_test_vm( + let loader = static_http_loader([]); + let mut vm = new_storage_page_task_executor_test_vm_with_loader( "https://form-navigation-target.test/source", - "", + &loader, ); let description = format!( "depth={depth} target={target} method={method} api={api} \ child_realm={use_child_realm} cancel_top={cancel_top}" ); + load_form_navigation_frames(&mut vm, &loader, depth).await; let result = vm .eval(&format!( r#" (() => {{ - const seen = []; - let source = document; + const seen = globalThis.__lmFormNavigationLog = []; + const source = formNavigationSource; for (let i = 0; i < {depth}; ++i) {{ - const frame = source.createElement('iframe'); - source.body.appendChild(frame); - frame.srcdoc = ''; - source = frame.contentDocument; + const frame = formNavigationFrames[i]; frame.contentWindow.navigation.onnavigate = event => {{ seen.push('child:' + i); event.preventDefault(); @@ -420,6 +453,21 @@ fn form_navigation_events_use_resolved_top_window_across_realms() { "#, )) .expect(&description); + assert_eq!(result, "[]", "{description}"); + assert!(vm.take_pending_location_navigation_with_seed().is_none()); + // Execute the queued submission body before inspecting + // its request, which task-end reconciliation consumes. + assert!( + vm.run_one_dom_manipulation_body_for_test( + crate::runtime::PageDomManipulationTestFamily::FormNavigation, + ) + .await + .expect(&description), + "{description}" + ); + let result = vm + .eval("JSON.stringify(__lmFormNavigationLog)") + .expect(&description); let expected_url = if method == "get" { "https://form-navigation-target.test/submitted?value=b" } else { @@ -431,7 +479,7 @@ fn form_navigation_events_use_resolved_top_window_across_realms() { true, true, true, - "replace", + "push", expected_url, true, true, @@ -473,38 +521,53 @@ fn form_navigation_events_use_resolved_top_window_across_realms() { } } -#[test] -fn post_form_navigation_uses_target_intrinsic_form_data_constructor() { +#[tokio::test] +async fn post_form_navigation_uses_target_intrinsic_form_data_constructor() { for (depth, target) in [(1, "_top"), (1, "_self"), (2, "_parent"), (1, "receiver")] { for api in ["submit", "requestSubmit"] { for use_child_realm in [true, false] { - let mut vm = new_parsed_test_vm( + let loader = static_http_loader([]); + let mut vm = new_storage_page_task_executor_test_vm_with_loader( "https://form-navigation-target.test/source", - "", + &loader, ); let description = format!( "depth={depth} target={target} api={api} child_realm={use_child_realm}" ); + load_form_navigation_frames(&mut vm, &loader, depth).await; + if target == "receiver" { + vm.eval( + r#" + globalThis.formNavigationReceiver = document.createElement('iframe'); + formNavigationReceiver.name = 'receiver'; + formNavigationReceiver.srcdoc = ''; + document.body.appendChild(formNavigationReceiver); + "#, + ) + .expect(&description); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(formNavigationReceiver.contentDocument.URL === 'about:srcdoc' && \ + formNavigationReceiver.contentDocument.readyState === 'complete')", + "true", + &description, + ) + .await; + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 100) + .await + .expect(&description); + } let result = vm .eval(&format!( r#" (() => {{ const seen = []; - let source = document; - for (let i = 0; i < {depth}; ++i) {{ - const frame = source.createElement('iframe'); - source.body.appendChild(frame); - frame.srcdoc = ''; - source = frame.contentDocument; - }} + const source = formNavigationSource; const child = source.defaultView; let targetWindow = {target:?} === '_self' ? child : child.parent; if ({target:?} === 'receiver') {{ - const receiver = document.createElement('iframe'); - receiver.name = 'receiver'; - document.body.appendChild(receiver); - receiver.srcdoc = ''; - targetWindow = receiver.contentWindow; + targetWindow = formNavigationReceiver.contentWindow; }} const TargetFormData = targetWindow.FormData; const ChildFormData = child.FormData; @@ -536,12 +599,27 @@ fn post_form_navigation_uses_target_intrinsic_form_data_constructor() { if (targetWindow !== window) event.preventDefault(); }}; const realm = {use_child_realm} ? child : window; + globalThis.__lmFormDataNavigationSnapshot = () => + JSON.stringify([seen, constructorReads]); realm.HTMLFormElement.prototype[{api:?}].call(form); return JSON.stringify([seen, constructorReads]); }})() "#, )) .expect(&description); + assert_eq!(result, "[[],0]", "{description}"); + assert!(vm.take_pending_location_navigation_with_seed().is_none()); + assert!( + vm.run_one_dom_manipulation_body_for_test( + crate::runtime::PageDomManipulationTestFamily::FormNavigation, + ) + .await + .expect(&description), + "{description}" + ); + let result = vm + .eval("__lmFormDataNavigationSnapshot()") + .expect(&description); assert_eq!( result, r#"[[[true,true,true,"b","from-event"]],0]"#, "{description}" diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index fb033514ac..655877bdc7 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2334,6 +2334,7 @@ mod storage_dense_name_arrays; mod streams; mod string_timers; mod svg_animated_number; +mod svg_computed_path_precision; mod svg_cors_attributes; mod svg_element_receivers; mod svg_filter_enumerations;