fix(history): enforce document URL rewrite rules

This commit is contained in:
ldm0
2026-09-23 00:11:54 +08:00
parent 91b2139039
commit aadee3eb85
11 changed files with 386 additions and 80 deletions
@@ -24,6 +24,7 @@ use super::navigation_result::{
};
use super::navigation_serialize::sync_child_navigation_entry_seed_from_owner;
use super::navigation_window::{
child_browsing_context_handle_for_runtime_owner,
runtime_window_is_global, window_location_for_holder, window_navigation_for_holder,
};
use super::*;
@@ -107,6 +108,7 @@ fn mutate_history_object<'s>(
let Some(owner) = require_fully_active_history_owner(scope, history) else {
return;
};
let kind = effective_history_mutation_kind(scope, owner, kind);
let Some(state) = structured_clone_value_for_storage(scope, parsed.state) else {
return;
};
@@ -116,9 +118,9 @@ fn mutate_history_object<'s>(
};
let current_href =
location_href_slot(scope, location).unwrap_or_else(|| "about:blank".to_owned());
let current_url = match history_same_origin_reference_url(scope, owner, &current_href) {
Ok(url) => url,
Err(_) => {
let current_url = match history_document_url(scope, owner, &current_href) {
Some(url) => url,
None => {
throw_history_security_error(
scope,
"Failed to execute 'pushState' or 'replaceState' on 'History': The current URL is invalid.",
@@ -126,13 +128,9 @@ fn mutate_history_object<'s>(
return;
}
};
let resolve_base_href = if history_url_inherits_origin(scope, owner, &current_href) {
current_url.as_str()
} else {
&current_href
};
let api_base_url = history_api_base_url(scope, owner, &current_url);
let url = match parsed.url {
Some(target) if !target.is_empty() => resolve_history_state_url(resolve_base_href, &target),
Some(target) if !target.is_empty() => resolve_history_state_url(&api_base_url, &target),
_ => Some(current_url.clone()),
};
let Some(url) = url else {
@@ -142,7 +140,7 @@ fn mutate_history_object<'s>(
);
return;
};
if !moli_url::same_origin(&url, &current_url) {
if !document_can_have_url_rewritten(&current_url, &url) {
throw_history_security_error(
scope,
"Failed to execute 'pushState' or 'replaceState' on 'History': A history state object with URL of a different origin cannot be created in a document with origin.",
@@ -305,18 +303,16 @@ fn mutate_history_object<'s>(
} else if let Some(popup_id) =
crate::native_bridge::lightweight_popup_id_from_window(scope, owner)
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
&& let Some(document_handle) =
unsafe { &*host_ptr }.lightweight_popup_document_handle(popup_id)
{
let _ = unsafe { &mut *host_ptr }.set_dom_document_url_for_handle(document_handle, url);
let _ = unsafe { &mut *host_ptr }.set_lightweight_popup_same_document_url(popup_id, url);
}
}
fn resolve_history_state_url(base_href: &str, target: &str) -> Option<url::Url> {
fn resolve_history_state_url(base_url: &url::Url, target: &str) -> Option<url::Url> {
if let Ok(absolute) = url::Url::parse(target) {
return Some(absolute);
}
url::Url::parse(base_href).ok()?.join(target).ok()
base_url.join(target).ok()
}
fn parse_history_mutation_args<'s>(
@@ -344,28 +340,155 @@ fn parse_history_mutation_args<'s>(
}
}
fn history_same_origin_reference_url<'s>(
fn history_api_base_url<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
current_href: &str,
) -> Result<url::Url, url::ParseError> {
if history_url_inherits_origin(scope, owner, current_href)
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
return Ok(unsafe { &mut *host_ptr }.host_document().url().clone());
current_url: &url::Url,
) -> url::Url {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return current_url.clone();
};
let host = unsafe { &*host_ptr };
if runtime_window_is_global(scope, owner) {
return host
.dom_host()
.document_base_url()
.unwrap_or_else(|| current_url.clone());
}
url::Url::parse(current_href)
if let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) {
return host
.child_browsing_context_base_url(handle)
.unwrap_or_else(|| current_url.clone());
}
if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) {
return host
.lightweight_popup_request_base_url(scope, popup_id)
.unwrap_or_else(|| current_url.clone());
}
current_url.clone()
}
fn history_url_inherits_origin<'s>(
fn history_document_url<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
current_href: &str,
) -> bool {
!runtime_window_is_global(scope, owner)
&& matches!(current_href, "about:blank" | "about:srcdoc")
) -> Option<url::Url> {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return url::Url::parse(current_href).ok();
};
let host = unsafe { &*host_ptr };
if runtime_window_is_global(scope, owner) {
return Some(host.document_url().clone());
}
if let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) {
return host.child_browsing_context_current_url(handle);
}
if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) {
if host.lightweight_popup_current_document_is_initial_empty(popup_id) {
return url::Url::parse("about:blank").ok();
}
return host.lightweight_popup_document_url(popup_id);
}
url::Url::parse(current_href).ok()
}
fn effective_history_mutation_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
kind: HistoryMutationKind,
) -> HistoryMutationKind {
if !matches!(kind, HistoryMutationKind::Push) {
return kind;
}
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return kind;
};
let host = unsafe { &*host_ptr };
let is_initial_empty = child_browsing_context_handle_for_runtime_owner(scope, owner)
.is_some_and(|handle| host.child_browsing_context_is_on_initial_about_blank_entry(handle))
|| crate::native_bridge::lightweight_popup_id_from_window(scope, owner).is_some_and(
|popup_id| host.lightweight_popup_current_document_is_initial_empty(popup_id),
);
if is_initial_empty {
HistoryMutationKind::Replace
} else {
kind
}
}
fn document_can_have_url_rewritten(document_url: &url::Url, target_url: &url::Url) -> bool {
if document_url.scheme() != target_url.scheme()
|| document_url.username() != target_url.username()
|| document_url.password() != target_url.password()
|| document_url.host() != target_url.host()
|| document_url.port() != target_url.port()
{
return false;
}
if matches!(target_url.scheme(), "http" | "https") {
return true;
}
if target_url.scheme() == "file" {
return document_url.path() == target_url.path();
}
document_url.path() == target_url.path() && document_url.query() == target_url.query()
}
fn throw_history_security_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
crate::context_bootstrap::throw_dom_exception_value(scope, message, "SecurityError");
}
#[cfg(test)]
mod tests {
use super::document_can_have_url_rewritten;
use url::Url;
fn url(value: &str) -> Url {
Url::parse(value).expect("test URL should parse")
}
#[test]
fn history_url_rewrite_rules_match_url_component_boundaries() {
let cases = [
(
"https://example.test/home",
"https://example.test/shop",
true,
),
(
"https://example.test/home",
"https://user:pass@example.test/home",
false,
),
("file:///path/to/x", "file:///path/to/x?search", true),
("file:///path/to/x", "file:///path/to/y", false),
("about:blank", "about:blank#hash", true),
("about:blank", "about:blank?search", false),
("about:blank", "about:srcdoc", false),
("data:text/html,body", "data:text/html,body#hash", true),
("data:text/html,body", "data:text/html,body?search", false),
(
"blob:https://example.test/00000000-0000-0000-0000-000000000001",
"blob:https://example.test/00000000-0000-0000-0000-000000000001#hash",
true,
),
(
"blob:https://example.test/00000000-0000-0000-0000-000000000001",
"blob:https://example.test/00000000-0000-0000-0000-000000000001?search",
false,
),
(
"blob:https://example.test/00000000-0000-0000-0000-000000000001",
"blob:https://example.test/00000000-0000-0000-0000-000000000002",
false,
),
];
for (document, target, expected) in cases {
assert_eq!(
document_can_have_url_rewritten(&url(document), &url(target)),
expected,
"rewrite result for {document} -> {target}"
);
}
}
}
@@ -217,7 +217,8 @@ fn entry_origin_url<'s>(
}
fn child_navigation_entry_url_inherits_origin(url: &url::Url) -> bool {
url.scheme() == "about" && matches!(url.as_str(), "about:blank" | "about:srcdoc")
url.scheme() == "about"
&& (url.path().eq_ignore_ascii_case("blank") || url.path().eq_ignore_ascii_case("srcdoc"))
}
pub(super) fn set_history_length_from_visible_entries<'s>(
@@ -278,3 +279,26 @@ fn history_length_floor_from_visible_entries<'s>(
.max(0.0);
visible_length.max(top_length)
}
#[cfg(test)]
mod tests {
use super::child_navigation_entry_url_inherits_origin;
use url::Url;
#[test]
fn about_document_history_urls_inherit_origin_with_fragments() {
for raw_url in [
"about:blank",
"about:blank#history",
"about:srcdoc",
"about:srcdoc#history",
] {
assert!(child_navigation_entry_url_inherits_origin(
&Url::parse(raw_url).expect("about URL should parse")
));
}
assert!(!child_navigation_entry_url_inherits_origin(
&Url::parse("about:other#history").expect("about URL should parse")
));
}
}
@@ -728,8 +728,9 @@ impl ChildBrowsingContextEntry {
self.navigation_entry_seed = entry_seed.clone();
if same_document_update {
self.committed_navigation_entry_seed = entry_seed;
self.pending_attribute_bootstrap_commit = false;
self.pending_live_navigation_reflects_window_state = false;
if !self.pending_attribute_bootstrap_commit {
self.pending_live_navigation_reflects_window_state = false;
}
}
same_document_update
}
@@ -386,6 +386,7 @@ pub(super) struct LightweightPopupDocumentState {
struct LightweightPopupDocumentRecord {
owner: LightweightPopupDocumentOwner,
local_window_id: LightweightPopupLocalWindowId,
is_initial_empty_document: bool,
url: Url,
access_origin: super::window_security_tokens::WindowAccessOrigin,
state: LightweightPopupDocumentState,
@@ -685,15 +686,25 @@ impl JsContextHost {
true
}
fn set_lightweight_popup_same_document_url(&mut self, popup_id: u64, url: Url) -> bool {
let Some(record) = self.lightweight_popup_record_mut(popup_id) else {
return false;
pub(crate) fn set_lightweight_popup_same_document_url(
&mut self,
popup_id: u64,
url: Url,
) -> bool {
let document_handle = {
let Some(record) = self.lightweight_popup_record_mut(popup_id) else {
return false;
};
let LightweightPopupLifecycle::Open(open) = &mut record.lifecycle else {
return false;
};
record.location_url = url.clone();
open.document.url = url.clone();
open.document.handle
};
let LightweightPopupLifecycle::Open(open) = &mut record.lifecycle else {
return false;
};
record.location_url = url.clone();
open.document.url = url;
if let Some(document_handle) = document_handle {
let _ = self.set_dom_document_url_for_handle(document_handle, url);
}
true
}
@@ -933,6 +944,7 @@ impl JsContextHost {
document: LightweightPopupDocumentRecord {
owner: initial_document_owner,
local_window_id: initial_local_window_id,
is_initial_empty_document: true,
url: initial_url.clone(),
access_origin: initial_origin,
state: initial_document_state.clone(),
@@ -1081,7 +1093,6 @@ impl JsContextHost {
window,
&target_url,
crate::context_bootstrap::LocationNavigationKind::Assign,
Some(&previous_url),
);
let queue_synthetic_load = if moli_url::is_about_blank(&target_url) {
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
@@ -1228,6 +1239,14 @@ impl JsContextHost {
.map(|document| document.url.clone())
}
pub(crate) fn lightweight_popup_current_document_is_initial_empty(
&self,
popup_id: u64,
) -> bool {
self.lightweight_popup_document_record(popup_id)
.is_some_and(|document| document.is_initial_empty_document)
}
pub(crate) fn lightweight_popup_session_storage_store(
&self,
popup_id: u64,
@@ -1495,9 +1514,7 @@ impl JsContextHost {
window: v8::Local<'s, v8::Object>,
target_url: &Url,
kind: crate::context_bootstrap::LocationNavigationKind,
current_url: Option<&Url>,
) {
let effective_kind = lightweight_popup_effective_navigation_kind(current_url, kind);
let base_url = self
.lightweight_popup_base_url(scope, popup_id)
.unwrap_or_else(|| target_url.clone());
@@ -1512,7 +1529,7 @@ impl JsContextHost {
&base_url,
&document_referrer,
);
apply_local_window_location_navigation(scope, window, target_url, effective_kind);
apply_local_window_location_navigation(scope, window, target_url, kind);
sync_window_location_history_navigation_runtime_surface(scope, window);
}
@@ -1566,6 +1583,10 @@ impl JsContextHost {
let Some(window) = self.lightweight_popup_window(scope, popup_id) else {
return false;
};
let kind = lightweight_popup_effective_navigation_kind(
self.lightweight_popup_current_document_is_initial_empty(popup_id),
kind,
);
let current_url = lightweight_popup_location_href(scope, window)
.or_else(|| self.lightweight_popup_location_url(popup_id));
if !matches!(
@@ -1696,7 +1717,6 @@ impl JsContextHost {
window,
&target_url,
kind,
current_url.as_ref(),
);
self.install_lightweight_popup_empty_document(scope, popup_id, window, target_url);
self.queue_lightweight_popup_load_event(navigation_task);
@@ -1709,7 +1729,6 @@ impl JsContextHost {
window,
&target_url,
kind,
current_url.as_ref(),
);
if self
.start_lightweight_popup_document_load(
@@ -1951,6 +1970,7 @@ impl JsContextHost {
LightweightPopupDocumentRecord {
owner: commit.owner,
local_window_id: current_local_window_id,
is_initial_empty_document: false,
url: commit.location_url.clone(),
access_origin,
state: commit.state,
@@ -4926,13 +4946,13 @@ fn set_lightweight_popup_persisted_script_global<'s>(
}
fn lightweight_popup_effective_navigation_kind(
current_url: Option<&Url>,
current_document_is_initial_empty: bool,
kind: crate::context_bootstrap::LocationNavigationKind,
) -> crate::context_bootstrap::LocationNavigationKind {
if matches!(
kind,
crate::context_bootstrap::LocationNavigationKind::Assign
) && current_url.is_some_and(moli_url::is_about_blank)
) && current_document_is_initial_empty
{
return crate::context_bootstrap::LocationNavigationKind::Replace;
}
@@ -398,10 +398,17 @@ async fn hashchange_discards_a_retired_child_local_window() {
r#"
const frame = document.createElement("iframe");
frame.id = "stale-hashchange-recipient";
frame.srcdoc = "<!doctype html><p>child</p>";
document.body.appendChild(frame);
"created"
"#,
)?;
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
ChildFrameSemanticTurnKind::NavigationCommit,
"stale hashchange child srcdoc commit",
)
.await;
materialize_child_realm_through_page_turn_for_test(
&mut page_vm,
"stale-hashchange-recipient",
@@ -411,7 +418,7 @@ document.body.appendChild(frame);
globalThis.__staleHashChanges = 0;
const staleFrame = document.getElementById("stale-hashchange-recipient");
staleFrame.contentWindow.addEventListener("hashchange", () => parent.__staleHashChanges++);
staleFrame.contentWindow.history.replaceState(null, "", "/child-hashchange");
staleFrame.contentWindow.history.replaceState(null, "", "about:srcdoc#child-hashchange");
staleFrame.contentWindow.location.hash = "#queued";
staleFrame.remove();
"retired"
@@ -447,7 +454,7 @@ async fn hashchange_discards_a_retired_lightweight_popup_local_window() {
r##"
globalThis.__popupHashChanges = [];
globalThis.__hashPopup = open("about:blank", "hashchange-owner-popup");
__hashPopup.history.replaceState(null, "", "/popup-hashchange");
__hashPopup.history.replaceState(null, "", "about:blank#popup-hashchange");
__hashPopup.addEventListener("hashchange", () => __popupHashChanges.push("retired"));
__hashPopup.location.hash = "#queued-before-replacement";
open("about:blank", "hashchange-owner-popup");
@@ -479,7 +486,7 @@ open("about:blank", "hashchange-owner-popup");
page_vm.vm_mut().eval(
r##"
__hashPopup.history.replaceState(null, "", "/replacement-popup-hashchange");
__hashPopup.history.replaceState(null, "", "about:blank#replacement-popup-hashchange");
__hashPopup.addEventListener("hashchange", () => __popupHashChanges.push("current"));
__hashPopup.location.hash = "#queued-after-replacement";
"queued-current"
@@ -313,10 +313,17 @@ async fn history_traversal_discards_a_retired_child_local_window() {
r##"
const frame = document.createElement("iframe");
frame.id = "history-stale-child";
frame.srcdoc = "<!doctype html><p>child</p>";
document.body.appendChild(frame);
"created"
"##,
)?;
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
ChildFrameSemanticTurnKind::NavigationCommit,
"history stale-child srcdoc commit",
)
.await;
materialize_child_realm_through_page_turn_for_test(&mut page_vm, "history-stale-child")?;
page_vm.vm_mut().eval(
r##"
@@ -325,7 +332,7 @@ const child = document.getElementById("history-stale-child").contentWindow;
child.addEventListener("popstate", () => {
parent.__retiredChildHistoryEvents += 1;
});
child.history.pushState(null, "", "#queued");
child.history.pushState(null, "", "about:srcdoc#queued");
child.history.back();
document.getElementById("history-stale-child").remove();
"retired"
@@ -26279,6 +26279,39 @@ JSON.stringify({
);
}
#[test]
fn window_open_named_reuse_before_first_commit_pushes_history() {
let mut vm = new_storage_test_vm("https://example.com/base/page.html");
assert_eq!(
vm.eval(
r#"
(() => {
const firstUrl = URL.createObjectURL(
new Blob(['<!doctype html><p>first</p>'], { type: 'text/html' })
);
const secondUrl = URL.createObjectURL(
new Blob(['<!doctype html><p>second</p>'], { type: 'text/html' })
);
const popup = open(firstUrl, 'pendingNamedPopup');
const reopened = open(secondUrl, 'pendingNamedPopup');
const result = [
popup === reopened,
reopened.location.href === secondUrl,
reopened.history.length,
].join('|');
popup.close();
URL.revokeObjectURL(firstUrl);
URL.revokeObjectURL(secondUrl);
return result;
})()
"#,
)
.expect("pending named popup reuse should evaluate"),
"true|true|2"
);
}
#[tokio::test]
async fn lightweight_popup_cross_document_navigation_clears_old_onload_handler() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
@@ -27580,6 +27613,32 @@ async fn window_open_204_popup_ignores_navigation_and_preserves_initial_empty_hi
);
}
#[test]
fn window_open_initial_empty_history_push_is_a_replacement() {
let mut vm = new_storage_test_vm("https://initial-popup-history.test/page.html");
assert_eq!(
vm.eval(
r#"
(() => {
const popup = open();
popup.history.pushState({ step: 1 }, '', 'about:blank#pushed');
popup.history.replaceState({ step: 2 }, '', 'about:blank#replaced');
const result = [
popup.location.href,
popup.history.state.step,
popup.history.length,
].join('|');
popup.close();
return result;
})()
"#,
)
.expect("initial empty popup history mutation should evaluate"),
"about:blank#replaced|2|1"
);
}
#[tokio::test]
async fn window_open_without_url_replaces_initial_empty_history_on_first_navigation() {
let (origin, server) = spawn_lightweight_popup_relative_navigation_server().await;
@@ -4192,7 +4192,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() {
.clone();
vm.eval_in_child_default_context(
child_context_id,
r##"history.pushState({ realm: "child-default" }, "", "#child-default")"##,
r##"history.pushState({ realm: "child-default" }, "", "about:srcdoc#child-default")"##,
)
.expect("child default history setup should evaluate");
@@ -4209,7 +4209,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() {
.expect("top isolated history setup should evaluate");
vm.eval_in_isolated_context(
child_isolated_context_id,
r##"history.pushState({ realm: "child-isolated" }, "", "#child-isolated")"##,
r##"history.pushState({ realm: "child-isolated" }, "", "about:srcdoc#child-isolated")"##,
)
.expect("child isolated history setup should evaluate");
@@ -4347,7 +4347,11 @@ async fn reset_navigation_history_preserves_child_entry_created_by_top_dispose_l
child_context_id,
r##"
(() => {
history.pushState({ realm: "child-before-reset" }, "", "#child-before-reset");
history.pushState(
{ realm: "child-before-reset" },
"",
"about:srcdoc#child-before-reset"
);
globalThis.__lmChildCurrentBeforeReset = navigation.currentEntry;
globalThis.__lmChildEntriesBeforeReset = navigation.entries();
globalThis.__lmChildDisposed = [];
@@ -4364,7 +4368,7 @@ navigation.entries()[0].addEventListener("dispose", () => {
document.querySelector("iframe").contentWindow.history.pushState(
{ realm: "child-during-top-dispose" },
"",
"#child-during-top-dispose"
"about:srcdoc#child-during-top-dispose"
);
});
"##,
@@ -6709,7 +6709,7 @@ async fn element_matches_delegates_loaded_child_document_elements() {
r##"
(() => {
const doc = document.querySelector("iframe").contentDocument;
doc.defaultView.history.replaceState(null, "", "#target");
doc.defaultView.history.replaceState(null, "", "about:srcdoc#target");
const code = doc.getElementById("code");
return [
code.ownerDocument === doc,
@@ -11000,9 +11000,9 @@ const frame = document.createElement('iframe');
frame.name = 'target';
const root = document.body || document.documentElement || document;
root.appendChild(frame);
frame.contentWindow.history.pushState(null, '', '/child.html');
frame.contentWindow.history.replaceState(null, '', 'about:blank#child');
const link = document.createElement('a');
link.href = '/child.html#next';
link.href = 'about:blank#next';
link.target = 'target';
root.appendChild(link);
let seen = [];
@@ -11020,10 +11020,7 @@ seen.join('|')
)
.expect("targeted same-document anchor click should dispatch child navigate");
assert_eq!(
result,
"true,true,https://targeted-child-hash.test/child.html#next,-1"
);
assert_eq!(result, "true,true,about:blank#next,-1");
}
#[test]
fn anchor_click_to_identical_url_dispatches_replace_navigate_event() {
@@ -11933,7 +11930,7 @@ globalThis.__firstJointLengthFrame = first;
(() => {
const child = __firstJointLengthFrame.contentWindow;
const before = history.length;
child.history.pushState(null, '', '#first');
child.history.pushState(null, '', 'about:srcdoc#first');
return [before, history.length, child.history.length].join('|');
})()
"#,
@@ -11960,7 +11957,7 @@ globalThis.__secondJointLengthFrame = second;
(() => {
const child = __secondJointLengthFrame.contentWindow;
const before = history.length;
child.history.pushState(null, '', '#second');
child.history.pushState(null, '', 'about:srcdoc#second');
return [before, history.length, child.history.length].join('|');
})()
"#,
@@ -11970,6 +11967,65 @@ globalThis.__secondJointLengthFrame = second;
);
}
#[test]
fn initial_empty_child_history_can_rewrite_about_blank_fragment() {
let mut vm = new_storage_test_vm("https://initial-empty-history.test/page.html");
assert_eq!(
vm.eval(
r#"
(() => {
const frame = document.createElement('iframe');
(document.body || document.documentElement || document).appendChild(frame);
const child = frame.contentWindow;
const before = history.length;
child.history.pushState({ step: 1 }, '', 'about:blank#pushed');
child.history.replaceState({ step: 2 }, '', 'about:blank#replaced');
return [
child.location.href,
child.history.state.step,
history.length,
before,
].join('|');
})()
"#,
)
.expect("initial empty child history mutation should evaluate"),
"about:blank#replaced|2|1|1"
);
}
#[test]
fn initial_empty_child_history_mutation_preserves_pending_srcdoc_navigation() {
let mut vm = new_storage_test_vm("https://pending-child-history.test/page.html");
assert_eq!(
vm.eval(
r#"
(() => {
const frame = document.createElement('iframe');
frame.srcdoc = '<p id="committed">child</p>';
(document.body || document.documentElement || document).appendChild(frame);
frame.contentWindow.history.pushState(null, '', 'about:blank#before-commit');
globalThis.__pendingHistoryFrame = frame;
return frame.contentWindow.location.href;
})()
"#,
)
.expect("pending child history mutation should evaluate"),
"about:blank#before-commit"
);
vm.drain_pending_child_frame_work_for_test();
assert_eq!(
vm.eval(
"[__pendingHistoryFrame.contentWindow.location.href, Boolean(__pendingHistoryFrame.contentDocument.getElementById('committed'))].join('|')"
)
.expect("pending srcdoc navigation should still commit"),
"about:srcdoc|true"
);
}
#[tokio::test]
async fn top_history_back_routes_to_child_joint_history_entry() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
@@ -12004,7 +12060,11 @@ async fn top_history_back_routes_to_child_joint_history_entry() {
r#"
(() => {
const frame = document.querySelector('iframe');
frame.contentWindow.history.pushState({ child: true }, '', '#child');
frame.contentWindow.history.pushState(
{ child: true },
'',
'about:srcdoc#child'
);
return [
location.href,
history.length,
@@ -12018,7 +12078,7 @@ async fn top_history_back_routes_to_child_joint_history_entry() {
assert_eq!(
setup,
"https://joint-child-back.test/page.html|2|2|https://joint-child-back.test/page.html#child"
"https://joint-child-back.test/page.html|2|2|about:srcdoc#child"
);
let _ = vm
.run_one_oldest_ready_page_task_executor_turn(&loader)
@@ -12084,7 +12144,11 @@ async fn top_history_back_ignores_removed_child_joint_history_entry() {
r#"
(() => {
const frame = document.querySelector('iframe');
frame.contentWindow.history.pushState({ child: true }, '', '#child');
frame.contentWindow.history.pushState(
{ child: true },
'',
'about:srcdoc#child'
);
return [
location.href,
navigation.entries().length,
@@ -12100,7 +12164,7 @@ async fn top_history_back_ignores_removed_child_joint_history_entry() {
assert_eq!(
setup,
"https://removed-child-joint-back.test/page.html|1|0|2|2|https://removed-child-joint-back.test/page.html#child"
"https://removed-child-joint-back.test/page.html|1|0|2|2|about:srcdoc#child"
);
vm.eval("document.querySelector('iframe').remove(); history.back(); 'queued'")
@@ -12208,7 +12272,7 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch
(() => {
globalThis.__lmDetachedChildNavigateErrorLog = [];
const child = document.querySelector('iframe').contentWindow;
child.history.pushState({ child: true }, "", "#one");
child.history.pushState({ child: true }, "", "about:srcdoc#one");
return [
child.navigation.entries().length,
child.navigation.currentEntry.index,
@@ -12218,10 +12282,7 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch
"##,
)
.expect("child initial same-document navigation should evaluate");
assert_eq!(
initial,
"2|1|https://child-detach-navigation-error.test/page.html#one"
);
assert_eq!(initial, "2|1|about:srcdoc#one");
let setup = vm
.eval(
@@ -9823,7 +9823,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() {
</body>
`);
childDocument.close();
childWindow.history.replaceState(null, '', '#old');
childWindow.history.replaceState(null, '', 'about:blank#old');
globalThis.__childTargetFrame = frame;
globalThis.__childTargetOldStyle =
childWindow.getComputedStyle(childDocument.getElementById('old'));
@@ -9855,7 +9855,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() {
r#"
(() => {
const childWindow = globalThis.__childTargetFrame.contentWindow;
childWindow.history.replaceState(null, '', '#new');
childWindow.history.replaceState(null, '', 'about:blank#new');
const result = [
globalThis.__childTargetOldStyle.color,
globalThis.__childTargetNewStyle.color
@@ -9923,7 +9923,7 @@ fn popup_target_selector_invalidation_uses_popup_document_world() {
newTarget.id = 'popup-new-target';
newTarget.className = 'probe';
popupBody.append(oldTarget, newTarget);
popup.history.replaceState(null, '', '#popup-old-target');
popup.history.replaceState(null, '', 'about:blank#popup-old-target');
globalThis.__popupTargetOldStyle = popup.getComputedStyle(oldTarget);
globalThis.__popupTargetNewStyle = popup.getComputedStyle(newTarget);
@@ -9952,7 +9952,7 @@ fn popup_target_selector_invalidation_uses_popup_document_world() {
.eval(
r#"
(() => {
__popupTargetWindow.history.replaceState(null, '', '#popup-new-target');
__popupTargetWindow.history.replaceState(null, '', 'about:blank#popup-new-target');
const result = [
globalThis.__popupTargetOldStyle.color,
globalThis.__popupTargetNewStyle.color
@@ -9614,7 +9614,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() {
const frame = document.createElement('iframe');
(document.body || document.documentElement || document).appendChild(frame);
const child = frame.contentWindow;
child.history.replaceState(null, '', '/child-v1.html');
child.history.replaceState(null, '', 'about:blank#child-v1');
const topParser = new DOMParser();
const childParser = new child.DOMParser();
@@ -9635,7 +9635,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() {
'text/html'
).URL;
const beforeSameDocumentUpdate = parseUrl(childParser);
child.history.replaceState(null, '', '/child-v2.html');
child.history.replaceState(null, '', 'about:blank#child-v2');
let invalidReceiver;
try {
@@ -9664,7 +9664,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() {
assert_eq!(
result,
r#"{"top":"https://dom-parser-realm.test/top.html","child":"https://dom-parser-realm.test/child-v2.html","childWithTopNewTarget":"https://dom-parser-realm.test/child-v2.html","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"https://dom-parser-realm.test/child-v1.html","invalidReceiver":"TypeError"}"#
r#"{"top":"https://dom-parser-realm.test/top.html","child":"about:blank#child-v2","childWithTopNewTarget":"about:blank#child-v2","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"about:blank#child-v1","invalidReceiver":"TypeError"}"#
);
}
@@ -9679,7 +9679,7 @@ fn dom_parser_non_object_new_target_prototype_uses_new_target_realm_default() {
const frame = document.createElement('iframe');
(document.body || document.documentElement || document).appendChild(frame);
const child = frame.contentWindow;
child.history.replaceState(null, '', '/child.html');
child.history.replaceState(null, '', 'about:blank#child');
const TopBad = new Function();
TopBad.prototype = 7;