fix(runtime): integrate WPT fixes with current main

Route existing Window.open targets through native navigation without the
Location-specific before-load replacement rule. Run form event assertions
through queued submission tasks using fully loaded child Documents, and
retain target-realm FormData and request payload checks.

Reconcile the expanded FontFace and IndexedDB fixtures, restore the SVG
precision test registration, and adapt ignored-response assertions to the
NoDocument outcome while retaining response metadata checks.
This commit is contained in:
ldm0
2026-10-11 18:06:49 +08:00
parent 4612525d2f
commit ffa601fbac
10 changed files with 132 additions and 97 deletions
@@ -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(())
}
@@ -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}"
);
}
@@ -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()
+1 -1
View File
@@ -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;
@@ -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,
};
@@ -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,
};
@@ -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 {
@@ -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)]
@@ -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 = '<body></body>';
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",
"<!doctype html><body></body>",
&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 = '<body></body>';
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",
"<!doctype html><body></body>",
&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 = '<body></body>';
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 = '<body></body>';
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 = '<body></body>';
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}"
@@ -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;