mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
fix(dom): update document.open URLs from the entry document
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
(async () => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.src = '/compat/child-dynamic-markup-document?markup=%3Cbody%3Esource#source-fragment';
|
||||
const loaded = new Promise(resolve => frame.onload = resolve);
|
||||
document.body.append(frame);
|
||||
await loaded;
|
||||
const child = frame.contentWindow;
|
||||
return new Promise(resolve => {
|
||||
child.finish = resolve;
|
||||
child.target = document;
|
||||
child.targetWindow = window;
|
||||
child.setTimeout(child.Function(`
|
||||
const done = finish, doc = target, win = targetWindow;
|
||||
const expected = document.URL.split('#')[0];
|
||||
const oldLength = win.history.length;
|
||||
const returned = doc.open();
|
||||
const observations = {url: doc.URL, uri: doc.documentURI, base: doc.baseURI,
|
||||
location: win.location.href, length: win.history.length, identity: returned === doc};
|
||||
const failures = [];
|
||||
for (const field of ['url', 'uri', 'base', 'location']) {
|
||||
if (observations[field] !== expected) failures.push({field, actual: observations[field], expected});
|
||||
}
|
||||
if (observations.length !== oldLength) failures.push({field: 'length', actual: observations.length, expected: oldLength});
|
||||
if (!observations.identity) failures.push({field: 'identity', actual: false, expected: true});
|
||||
doc.write('<!doctype html><body>replacement');
|
||||
doc.close();
|
||||
done({checks: 6, failures, observations});
|
||||
`), 0);
|
||||
});
|
||||
})()
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
(async () => {
|
||||
let checks = 0;
|
||||
const failures = [], observations = [];
|
||||
const check = (name, actual, expected) => {
|
||||
checks++;
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) failures.push({name, actual, expected});
|
||||
};
|
||||
const frame = async (src) => {
|
||||
const node = document.createElement('iframe');
|
||||
if (src) node.src = src;
|
||||
const loaded = new Promise(resolve => node.onload = resolve);
|
||||
document.body.append(node);
|
||||
await loaded;
|
||||
return node;
|
||||
};
|
||||
const snapshot = (doc, win) => ({
|
||||
url: doc.URL, uri: doc.documentURI, base: doc.baseURI,
|
||||
location: win.location.href, length: win.history.length, state: win.history.state,
|
||||
navigation: win.navigation?.currentEntry?.url ?? null,
|
||||
link: new win.URL('relative', doc.baseURI).href,
|
||||
});
|
||||
const parentURL = new URL(document.URL); parentURL.hash = '';
|
||||
for (const mode of ['blank-open', 'loaded-open', 'loaded-write', 'base-open', 'borrowed-open', 'self-open']) {
|
||||
const f = await frame(mode === 'blank-open' ? null : '/compat/child-dynamic-markup-document?markup=%3C!doctype%20html%3E%3Cbody%3Etarget#target');
|
||||
const d = f.contentDocument, w = f.contentWindow;
|
||||
if (mode !== 'blank-open') w.history.replaceState({kept: true, map: new w.Map([['key', 'value']])}, '');
|
||||
const before = snapshot(d,w);
|
||||
const base = document.createElement('base');
|
||||
if (mode === 'base-open') {base.href = '/different-base/'; document.head.append(base);}
|
||||
let returned;
|
||||
if (mode === 'loaded-write') {d.write('<p>replacement</p>');}
|
||||
else if (mode === 'borrowed-open') {returned = Document.prototype.open.call(d);}
|
||||
else if (mode === 'self-open') {
|
||||
await new Promise(resolve => {
|
||||
w.done = result => {returned = result; resolve();};
|
||||
w.setTimeout(w.Function('done(document.open())'), 0);
|
||||
});
|
||||
} else {returned = d.open();}
|
||||
const after = snapshot(d,w);
|
||||
const expectedURL = mode === 'self-open' ? before.url : parentURL.href;
|
||||
for (const field of ['url','uri','location']) check(mode + ':' + field, after[field], expectedURL);
|
||||
check(mode + ':base', after.base, expectedURL);
|
||||
check(mode + ':history-length', after.length, before.length);
|
||||
check(mode + ':history-state', after.state, before.state);
|
||||
if (mode !== 'loaded-write') check(mode + ':identity', returned === d, true);
|
||||
check(mode + ':state-identity', after.state === before.state, true);
|
||||
if (mode !== 'blank-open') check(mode + ':structured-state', after.state.map.get('key'), 'value');
|
||||
observations.push({mode,before,after});
|
||||
d.close();
|
||||
if (mode === 'loaded-open') {
|
||||
w.history.pushState(null, '', new URL('?after-open', expectedURL).href);
|
||||
await new Promise(resolve => {
|
||||
w.addEventListener('popstate', resolve, {once: true});
|
||||
w.history.back();
|
||||
});
|
||||
check(mode + ':back-url', d.URL, expectedURL);
|
||||
check(mode + ':back-location', w.location.href, expectedURL);
|
||||
check(mode + ':back-structured-state', w.history.state.map.get('key'), 'value');
|
||||
}
|
||||
f.remove(); base.remove();
|
||||
}
|
||||
for (const mode of ['nested-call', 'timer', 'microtask']) {
|
||||
const source = await frame('/compat/child-dynamic-markup-document?markup=%3Cbody%3Esource#source-fragment');
|
||||
const target = await frame('/compat/child-dynamic-markup-document?markup=%3Cbody%3Etarget#target-fragment');
|
||||
const w = source.contentWindow, d = target.contentDocument;
|
||||
w.target = d;
|
||||
const expected = mode === 'nested-call' ? parentURL.href : w.document.URL.split('#')[0];
|
||||
if (mode === 'nested-call') {
|
||||
w.Function('target.open()')();
|
||||
} else {
|
||||
await new Promise(resolve => {
|
||||
w.done = resolve;
|
||||
const action = w.Function('target.open(); done();');
|
||||
if (mode === 'timer') w.setTimeout(action, 0);
|
||||
else w.Promise.resolve().then(action);
|
||||
});
|
||||
}
|
||||
const observed = snapshot(d, target.contentWindow);
|
||||
for (const field of ['url', 'uri', 'base', 'location']) check(mode + ':' + field, observed[field], expected);
|
||||
d.close(); source.remove(); target.remove();
|
||||
}
|
||||
for (const mode of ['removed', 'windowless', 'old-document']) {
|
||||
const f = await frame();
|
||||
let d = f.contentDocument;
|
||||
if (mode === 'removed') f.remove();
|
||||
if (mode === 'windowless') d = d.implementation.createHTMLDocument('');
|
||||
if (mode === 'old-document') {
|
||||
const loaded = new Promise(resolve => f.onload = resolve);
|
||||
f.src = '/compat/child-dynamic-markup-document?markup=replacement'; await loaded;
|
||||
}
|
||||
const before = d.URL;
|
||||
check(mode + ':identity', d.open() === d, true);
|
||||
check(mode + ':url', d.URL, before);
|
||||
d.close(); f.remove();
|
||||
}
|
||||
return {checks, failures, observations};
|
||||
})()
|
||||
@@ -34,6 +34,9 @@ mod module_document_write;
|
||||
#[path = "scripts/document_close.rs"]
|
||||
mod document_close;
|
||||
|
||||
#[path = "scripts/document_open_url.rs"]
|
||||
mod document_open_url;
|
||||
|
||||
#[path = "scripts/document_write_insertion.rs"]
|
||||
mod document_write_insertion;
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use super::*;
|
||||
|
||||
async fn check_document_open_urls(script: &str, checks: u64) -> Result<()> {
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
let mut url = url::Url::parse(&server.url("/compat/child-dynamic-markup-document"))?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("markup", "<!doctype html><body>entry");
|
||||
url.set_fragment(Some("entry-fragment"));
|
||||
let mut page = browser.fetch(url.as_str()).await?;
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
page.evaluate_runtime_expression_with_await_async(
|
||||
&format!("({script}).then(JSON.stringify)"),
|
||||
true,
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
let observed: serde_json::Value = serde_json::from_str(
|
||||
result["value"]
|
||||
.as_str()
|
||||
.expect("document.open URL observations"),
|
||||
)?;
|
||||
assert_eq!(observed["checks"], checks);
|
||||
assert_eq!(observed["failures"], serde_json::json!([]));
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn document_open_updates_active_document_urls_from_entry_document() -> Result<()> {
|
||||
check_document_open_urls(include_str!("../fixtures/document-open-url.js"), 73).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn document_open_updates_root_url_from_child_entry_document() -> Result<()> {
|
||||
check_document_open_urls(include_str!("../fixtures/document-open-root-url.js"), 6).await
|
||||
}
|
||||
@@ -408,6 +408,72 @@ async fn set_document_content_preserves_history_length_and_state() {
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn document_open_keeps_reentrant_history_url_in_frame_tree() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
let app = axum::Router::new()
|
||||
.route(
|
||||
"/history",
|
||||
axum::routing::get(|| async {
|
||||
axum::response::Html(
|
||||
"<!doctype html><body><iframe src='/source#entry'></iframe>",
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/source",
|
||||
axum::routing::get(|| async { axum::response::Html("<!doctype html><body>entry") }),
|
||||
);
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
for child_entry in [false, true] {
|
||||
let mut ctx = TestContext::new();
|
||||
install_document_content_test_page(&mut ctx, &format!("http://{addr}/history")).await;
|
||||
ctx.process_async(json!({
|
||||
"id": 17,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"returnByValue": true,
|
||||
"awaitPromise": true,
|
||||
"expression": format!(r#"new Promise(resolve => {{
|
||||
navigation.addEventListener('currententrychange', () => {{
|
||||
history.replaceState({{after: true}}, '', '/after-open');
|
||||
}}, {{once: true}});
|
||||
const source = {child_entry} ? document.querySelector('iframe').contentWindow : window;
|
||||
source.finishOpen = resolve;
|
||||
source.targetDocument = document;
|
||||
source.setTimeout(source.Function(`
|
||||
const finish = finishOpen, target = targetDocument;
|
||||
target.open();
|
||||
target.write('<!doctype html><body>replacement');
|
||||
target.close();
|
||||
finish(target.URL);
|
||||
`), 0);
|
||||
}})"#),
|
||||
},
|
||||
}))
|
||||
.await;
|
||||
wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"document.open reentrant history response",
|
||||
|message| message["id"] == json!(17) && message["sessionId"] == json!("SID-1"),
|
||||
)
|
||||
.await;
|
||||
let response = take_response_by_id(&mut ctx, 17);
|
||||
let expected = json!(format!("http://{addr}/after-open"));
|
||||
assert_eq!(
|
||||
response["result"]["result"]["value"], expected,
|
||||
"{response:?}"
|
||||
);
|
||||
assert_eq!(frame_tree(&mut ctx, 18).await["frame"]["url"], expected);
|
||||
}
|
||||
server.abort();
|
||||
}
|
||||
|
||||
// Ported from WPT opening-the-input-stream/mutation-observer.window.js and
|
||||
// verified against Chromium's Page.setDocumentContent path. Unlike a bare
|
||||
// document.open(), SetContent also exposes the parser's subsequent additions.
|
||||
|
||||
@@ -136,6 +136,7 @@ pub(crate) use crypto::{
|
||||
};
|
||||
pub(crate) use css_fontface_runtime::{load_font_faces_for_family, rebuild_font_face_set_faces};
|
||||
pub(crate) use form_navigation::FormNavigationHistory;
|
||||
pub(crate) use history_mutation::update_history_for_document_open;
|
||||
pub(crate) use location_navigation::{
|
||||
LocationNavigationKind, dispatch_top_level_form_navigation_event,
|
||||
dispatch_top_level_navigation_event_with_source_element, meta_refresh_navigation_kind,
|
||||
|
||||
@@ -24,13 +24,68 @@ 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,
|
||||
child_browsing_context_handle_for_runtime_owner, runtime_window_is_global,
|
||||
window_history_for_holder, window_location_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
use moli_page_types::SameDocumentHistoryUpdate;
|
||||
|
||||
/// The URL/history update used by document.open() replaces the current entry
|
||||
/// without running history API argument conversion or firing a navigate event.
|
||||
/// A missing serialized state preserves both history.state and its snapshot.
|
||||
pub(crate) fn update_history_for_document_open<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
url: &url::Url,
|
||||
) {
|
||||
let Some(location) = window_location_for_holder(scope, window) else {
|
||||
return;
|
||||
};
|
||||
sync_location_object(scope, location, url.as_str());
|
||||
let Some(history) = window_history_for_holder(scope, window) else {
|
||||
return;
|
||||
};
|
||||
let Some(entries) = history_entries(scope, history) else {
|
||||
return;
|
||||
};
|
||||
let index = history_index(scope, history);
|
||||
let Some(previous) = entries
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
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);
|
||||
sync_navigation_current_entry_from_history_entry(scope, window, entry);
|
||||
sync_child_navigation_entry_seed_from_owner(scope, window);
|
||||
if let Some(navigation) = window_navigation_for_holder(scope, window) {
|
||||
dispatch_navigation_currententrychange(scope, navigation, Some(previous), Some("replace"));
|
||||
dispatch_navigation_entry_dispose(scope, previous);
|
||||
}
|
||||
}
|
||||
|
||||
struct ParsedHistoryMutationArgs<'s> {
|
||||
state: v8::Local<'s, v8::Value>,
|
||||
unused: String,
|
||||
|
||||
@@ -299,11 +299,13 @@ impl JsContextHost {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let entry_document = host.document_open_entry_document(scope);
|
||||
let Some(context) = host.begin_child_document_stream_replacement(
|
||||
scope,
|
||||
host_ptr,
|
||||
child_handle,
|
||||
document_handle,
|
||||
entry_document,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
@@ -422,6 +424,7 @@ impl JsContextHost {
|
||||
host_ptr: *mut JsContextHost,
|
||||
child_handle: DomHandle,
|
||||
document_handle: DomHandle,
|
||||
entry_document: Option<DomHandle>,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
debug_assert!(std::ptr::eq(host_ptr, self));
|
||||
if self.child_browsing_context_document_handle(child_handle) != Some(document_handle) {
|
||||
@@ -483,8 +486,24 @@ impl JsContextHost {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let document_url = self.document_url_for_handle(document_handle);
|
||||
let document_base_url = self.document_base_url_for_handle(document_handle);
|
||||
let previous_url = self.document_url_for_handle(document_handle);
|
||||
let replacement_url = entry_document
|
||||
.filter(|_| self.child_browsing_context_is_live(child_handle))
|
||||
.map(|entry| self.document_open_replacement_url(document_handle, entry));
|
||||
let document_url = replacement_url.as_ref().unwrap_or(&previous_url).clone();
|
||||
let document_base_url = if replacement_url.is_some() {
|
||||
if document_url == previous_url {
|
||||
self.dom_host()
|
||||
.node(document_handle)?
|
||||
.as_document()?
|
||||
.fallback_base_url()
|
||||
.clone()
|
||||
} else {
|
||||
document_url.clone()
|
||||
}
|
||||
} else {
|
||||
self.document_base_url_for_handle(document_handle)
|
||||
};
|
||||
let replacement_plan = self
|
||||
.frame_owner_store
|
||||
.plan_child_document_open_replacement(
|
||||
@@ -532,6 +551,9 @@ impl JsContextHost {
|
||||
let _ =
|
||||
remove_child_to_current_reaction_queue(scope, host_ptr, document_handle, child);
|
||||
}
|
||||
if replacement_url.is_some() {
|
||||
host.set_dom_document_url_for_handle(document_handle, document_url.clone());
|
||||
}
|
||||
|
||||
host.cancel_child_meta_refresh_navigation(child_handle);
|
||||
host.cancel_stylesheet_subresource_fetches_for_document_owner(retired_owner);
|
||||
@@ -559,7 +581,7 @@ impl JsContextHost {
|
||||
child_handle,
|
||||
current_owner.document_owner(),
|
||||
document_handle,
|
||||
document_url,
|
||||
document_url.clone(),
|
||||
);
|
||||
host.note_child_frame_load_started_for_parent(child_handle);
|
||||
host.queue_child_frame_document_opened_event(child_handle);
|
||||
@@ -572,6 +594,14 @@ impl JsContextHost {
|
||||
"opened child document stream through same-LocalWindow owner transaction"
|
||||
);
|
||||
});
|
||||
if replacement_url.is_some() {
|
||||
let window = script_context.global(scope);
|
||||
crate::context_bootstrap::update_history_for_document_open(
|
||||
scope,
|
||||
window,
|
||||
&document_url,
|
||||
);
|
||||
}
|
||||
Some(script_context)
|
||||
}
|
||||
|
||||
@@ -593,6 +623,7 @@ impl JsContextHost {
|
||||
host_ptr,
|
||||
child_handle,
|
||||
document_handle,
|
||||
None,
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -172,8 +172,9 @@ fn node_document_write_or_writeln_callback<'s>(
|
||||
return;
|
||||
}
|
||||
if implicit_replacement_session {
|
||||
let entry_document = runtime.document_open_entry_document(scope);
|
||||
clear_window_event_handlers(scope);
|
||||
runtime.prepare_root_document_replacement(scope, runtime_ptr, handle);
|
||||
runtime.prepare_root_document_replacement(scope, runtime_ptr, handle, entry_document);
|
||||
}
|
||||
let _ = runtime.write_html(scope, runtime_ptr, handle, &html);
|
||||
rv.set_undefined();
|
||||
@@ -274,11 +275,13 @@ pub(in crate::native_bridge) fn node_document_open_callback<'s>(
|
||||
if let Some(child_handle) =
|
||||
unsafe { &*runtime_ptr }.child_browsing_context_host_for_document_handle(handle)
|
||||
{
|
||||
let entry_document = unsafe { &*runtime_ptr }.document_open_entry_document(scope);
|
||||
let _ = unsafe { &mut *runtime_ptr }.begin_child_document_stream_replacement(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
child_handle,
|
||||
handle,
|
||||
entry_document,
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
return;
|
||||
@@ -294,8 +297,9 @@ pub(in crate::native_bridge) fn node_document_open_callback<'s>(
|
||||
}
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
if !runtime.has_active_parser_write_insertion_point() {
|
||||
let entry_document = runtime.document_open_entry_document(scope);
|
||||
clear_window_event_handlers(scope);
|
||||
runtime.prepare_root_document_replacement(scope, runtime_ptr, handle);
|
||||
runtime.prepare_root_document_replacement(scope, runtime_ptr, handle, entry_document);
|
||||
}
|
||||
}
|
||||
rv.set(args.this().into());
|
||||
@@ -310,6 +314,47 @@ fn clear_window_event_handlers(scope: &mut v8::PinScope<'_, '_>) {
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
pub(in crate::native_bridge) fn document_open_entry_document(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Option<DomHandle> {
|
||||
if let Some(popup_id) = crate::native_bridge::active_lightweight_popup_id(scope) {
|
||||
return self.lightweight_popup_document_handle(popup_id);
|
||||
}
|
||||
// Borrowed methods execute in their callee realm. HTML instead uses
|
||||
// the Window that entered this script or microtask.
|
||||
let context = scope.get_entered_or_microtask_context();
|
||||
let host_ptr = crate::util::context_host_ptr_from_context_slot(context)?;
|
||||
if !std::ptr::eq(host_ptr, self) {
|
||||
return None;
|
||||
}
|
||||
let window = context.global(scope);
|
||||
if let Some(document) = crate::util::get_private_value(
|
||||
scope,
|
||||
window,
|
||||
crate::context_bootstrap::WINDOW_DOCUMENT_SLOT,
|
||||
)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
{
|
||||
let (document_host, handle) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, document).ok()?;
|
||||
return std::ptr::eq(document_host, self).then_some(handle);
|
||||
}
|
||||
Some(self.document_handle())
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn document_open_replacement_url(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
entry_document: DomHandle,
|
||||
) -> url::Url {
|
||||
let mut url = self.document_url_for_handle(entry_document);
|
||||
if document != entry_document {
|
||||
url.set_fragment(None);
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
fn prepare_windowless_document_replacement(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -329,6 +374,7 @@ impl JsContextHost {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
document_handle: DomHandle,
|
||||
entry_document: Option<DomHandle>,
|
||||
) {
|
||||
self.dispatch_document_open_descendant_frame_unload_lifecycle(scope, document_handle);
|
||||
self.clear_event_callbacks_for_document_replacement(document_handle, true);
|
||||
@@ -339,7 +385,30 @@ impl JsContextHost {
|
||||
document_handle,
|
||||
);
|
||||
});
|
||||
let replacement_url =
|
||||
entry_document.map(|entry| self.document_open_replacement_url(document_handle, entry));
|
||||
let url_changed = replacement_url
|
||||
.as_ref()
|
||||
.is_some_and(|url| url != self.document_url());
|
||||
if let Some(url) = replacement_url.as_ref() {
|
||||
self.set_document_url(url.clone());
|
||||
}
|
||||
self.open_root_document(scope);
|
||||
if let Some(url) = replacement_url
|
||||
&& let Some(window) =
|
||||
super::document_associated_window_for_handle(scope, host_ptr, document_handle)
|
||||
{
|
||||
// Entry-change listeners can synchronously change the URL again.
|
||||
// Publish this change first so their later handoffs remain last.
|
||||
if url_changed {
|
||||
self.record_same_document_navigation(
|
||||
&url,
|
||||
"historyApi",
|
||||
moli_page_types::SameDocumentHistoryUpdate::Replace,
|
||||
);
|
||||
}
|
||||
crate::context_bootstrap::update_history_for_document_open(scope, window, &url);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the active root document through the native document stream.
|
||||
@@ -355,7 +424,7 @@ impl JsContextHost {
|
||||
) {
|
||||
let document_handle = self.document_handle();
|
||||
clear_window_event_handlers(scope);
|
||||
self.prepare_root_document_replacement(scope, host_ptr, document_handle);
|
||||
self.prepare_root_document_replacement(scope, host_ptr, document_handle, None);
|
||||
let _ = self.write_html(scope, host_ptr, document_handle, html);
|
||||
self.close_document(scope, host_ptr);
|
||||
}
|
||||
|
||||
@@ -9823,7 +9823,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() {
|
||||
</body>
|
||||
`);
|
||||
childDocument.close();
|
||||
childWindow.history.replaceState(null, '', 'about:blank#old');
|
||||
childWindow.history.replaceState(null, '', '#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, '', 'about:blank#new');
|
||||
childWindow.history.replaceState(null, '', '#new');
|
||||
const result = [
|
||||
globalThis.__childTargetOldStyle.color,
|
||||
globalThis.__childTargetNewStyle.color
|
||||
|
||||
Reference in New Issue
Block a user