From 6f3fdb80d4d337b03476b7a943c8e0d3ce53d689 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 19 Sep 2026 06:00:42 +0800 Subject: [PATCH] fix(dom): update document.open URLs from the entry document --- .../tests/fixtures/document-open-root-url.js | 30 ++++++ moli-core/tests/fixtures/document-open-url.js | 97 +++++++++++++++++++ moli-core/tests/scripts.rs | 3 + moli-core/tests/scripts/document_open_url.rs | 38 ++++++++ .../domains/page/tests/document_content.rs | 66 +++++++++++++ moli-renderer-v8/src/context_bootstrap.rs | 1 + .../src/context_bootstrap/history_mutation.rs | 59 ++++++++++- .../child_frame_runtime/document.rs | 37 ++++++- .../src/native_bridge/document/lifecycle.rs | 75 +++++++++++++- .../script_vm/tests/dom_xhr/computed_style.rs | 4 +- 10 files changed, 400 insertions(+), 10 deletions(-) create mode 100644 moli-core/tests/fixtures/document-open-root-url.js create mode 100644 moli-core/tests/fixtures/document-open-url.js create mode 100644 moli-core/tests/scripts/document_open_url.rs diff --git a/moli-core/tests/fixtures/document-open-root-url.js b/moli-core/tests/fixtures/document-open-root-url.js new file mode 100644 index 0000000000..936dd9a150 --- /dev/null +++ b/moli-core/tests/fixtures/document-open-root-url.js @@ -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('replacement'); + doc.close(); + done({checks: 6, failures, observations}); + `), 0); + }); +})() diff --git a/moli-core/tests/fixtures/document-open-url.js b/moli-core/tests/fixtures/document-open-url.js new file mode 100644 index 0000000000..4719082cee --- /dev/null +++ b/moli-core/tests/fixtures/document-open-url.js @@ -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('

replacement

');} + 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}; +})() diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs index 82c5b0a764..accb861991 100644 --- a/moli-core/tests/scripts.rs +++ b/moli-core/tests/scripts.rs @@ -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; diff --git a/moli-core/tests/scripts/document_open_url.rs b/moli-core/tests/scripts/document_open_url.rs new file mode 100644 index 0000000000..a257fe2bc3 --- /dev/null +++ b/moli-core/tests/scripts/document_open_url.rs @@ -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", "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 +} diff --git a/moli-protocol/src/domains/page/tests/document_content.rs b/moli-protocol/src/domains/page/tests/document_content.rs index 612165ce2c..9afe679dc6 100644 --- a/moli-protocol/src/domains/page/tests/document_content.rs +++ b/moli-protocol/src/domains/page/tests/document_content.rs @@ -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( + "", + ) + }), + ) + .route( + "/source", + axum::routing::get(|| async { axum::response::Html("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('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. diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index e8ff5115dc..d22f4fd435 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index 6697702f61..c9d0c8a4d1 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -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::::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, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs index 98d6da6cba..4bca3f8546 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs @@ -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, ) -> Option> { 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; }; diff --git a/moli-renderer-v8/src/native_bridge/document/lifecycle.rs b/moli-renderer-v8/src/native_bridge/document/lifecycle.rs index e9399066b0..31fb7eef1d 100644 --- a/moli-renderer-v8/src/native_bridge/document/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/document/lifecycle.rs @@ -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 { + 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::::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, ) { 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); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs index 40f96fca54..475580e97a 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs @@ -9823,7 +9823,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() { `); 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