mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 08:01:01 +00:00
fix(history): restore nested histories across document replacement
Keep child entries and srcdoc resources with their parent Document identity when top-level and popup Documents are replaced. Restore their original joint history steps, avoid traversing descendants of a retiring parent, and prune forward entries when navigation branches from an earlier step. Preserve parser-created frame identities in navigation projections. Cover named and unnamed frames, script-created parents, nested Documents, entry state and identity, and interleaved multi-step traversals through CDP.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
mod browser_target;
|
||||
mod emulation_input_storage;
|
||||
mod nested_history;
|
||||
mod network_fetch;
|
||||
mod p0_input;
|
||||
mod p0_network;
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
use super::super::tests_cdp_smoke_fixture::SmokeFixtureServer;
|
||||
use super::super::*;
|
||||
use super::support::CdpPageHarness;
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
struct HistoryPage {
|
||||
ctx: TestContext,
|
||||
cdp: CdpPageHarness,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl HistoryPage {
|
||||
async fn evaluate(&mut self, expression: &str) -> Result<Value> {
|
||||
self.evaluate_with_await(expression, false).await
|
||||
}
|
||||
|
||||
async fn evaluate_await(&mut self, expression: &str) -> Result<Value> {
|
||||
self.evaluate_with_await(expression, true).await
|
||||
}
|
||||
|
||||
async fn evaluate_with_await(
|
||||
&mut self,
|
||||
expression: &str,
|
||||
await_promise: bool,
|
||||
) -> Result<Value> {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
let response = if await_promise {
|
||||
self.cdp
|
||||
.evaluate_await_value(&mut self.ctx, id, expression)
|
||||
.await
|
||||
} else {
|
||||
self.cdp.evaluate_value(&mut self.ctx, id, expression).await
|
||||
};
|
||||
anyhow::ensure!(
|
||||
response.get("error").is_none() && response["result"].get("exceptionDetails").is_none(),
|
||||
"evaluating {expression}: {response}"
|
||||
);
|
||||
Ok(response["result"]["result"].clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn markup_url(server: &SmokeFixtureServer, markup: &str) -> String {
|
||||
let mut url = Url::parse(&server.url("/history-markup")).unwrap();
|
||||
url.query_pairs_mut().append_pair(
|
||||
"markup",
|
||||
&markup.replace(
|
||||
"<body>",
|
||||
"<head><script>onload=()=>document.body.dataset.loaded='yes';</script></head><body>",
|
||||
),
|
||||
);
|
||||
url.into()
|
||||
}
|
||||
|
||||
fn frame(url: &str, name: &str) -> String {
|
||||
format!(
|
||||
"<iframe data-static name=\"{name}\" src=\"{}\"></iframe>",
|
||||
url.replace('&', "&").replace('"', """)
|
||||
)
|
||||
}
|
||||
|
||||
async fn value(page: &mut HistoryPage, expression: &str) -> Result<Value> {
|
||||
let result = page
|
||||
.evaluate(&format!("JSON.stringify({expression})"))
|
||||
.await?;
|
||||
Ok(serde_json::from_str(
|
||||
result["value"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("{expression}: {result}")),
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn wait(page: &mut HistoryPage, expression: &str) -> Result<()> {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let result = page.evaluate(expression).await;
|
||||
if result.as_ref().is_ok_and(|value| value["value"] == true) {
|
||||
return Ok(());
|
||||
}
|
||||
anyhow::ensure!(
|
||||
tokio::time::Instant::now() < deadline,
|
||||
"timed out waiting for {expression}: {result:?}"
|
||||
);
|
||||
page.ctx.complete_one_ready_scheduler_input_for_test().await;
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn navigate(page: &mut HistoryPage, root: &str, action: &str, url: &str) -> Result<()> {
|
||||
page.evaluate(&format!("({root}).{action}; true")).await?;
|
||||
wait(
|
||||
page,
|
||||
&format!(
|
||||
"({root}).location.href === {} && ({root}).document.body?.dataset.loaded === 'yes'",
|
||||
serde_json::to_string(url)?
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("after {root}.{action}"))
|
||||
}
|
||||
|
||||
async fn open_root(
|
||||
url: &str,
|
||||
popup: bool,
|
||||
opener_url: &str,
|
||||
) -> Result<(HistoryPage, &'static str)> {
|
||||
let mut ctx = TestContext::new_with_target_discovery(false);
|
||||
let cdp = CdpPageHarness::attach(&mut ctx, 220_000).await;
|
||||
let response = cdp
|
||||
.navigate(&mut ctx, 220_005, if popup { opener_url } else { url })
|
||||
.await;
|
||||
anyhow::ensure!(response.get("error").is_none(), "{response}");
|
||||
let mut page = HistoryPage {
|
||||
ctx,
|
||||
cdp,
|
||||
next_id: 220_010,
|
||||
};
|
||||
let root = if popup { "testPopup" } else { "window" };
|
||||
if popup {
|
||||
page.evaluate(&format!(
|
||||
"globalThis.testPopup = open({}); true",
|
||||
serde_json::to_string(url)?
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
wait(
|
||||
&mut page,
|
||||
&format!(
|
||||
"({root}).location.href === {} && ({root}).document.body?.dataset.loaded === 'yes'",
|
||||
serde_json::to_string(url)?
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("opening {root}"))?;
|
||||
Ok((page, root))
|
||||
}
|
||||
|
||||
fn frames(root: &str) -> String {
|
||||
format!("Array.from(({root}).document.querySelectorAll('iframe[data-static]'))")
|
||||
}
|
||||
|
||||
async fn snapshot(page: &mut HistoryPage, root: &str) -> Result<Value> {
|
||||
value(page, &format!("({{length:({root}).history.length, frames:{}.map(f=>({{url:f.contentWindow.location.href, text:f.contentDocument.body.textContent, state:f.contentWindow.history.state, navigationState:f.contentWindow.navigation.currentEntry.getState() ?? null, id:f.contentWindow.navigation.currentEntry.id, keys:f.contentWindow.navigation.entries().map(e=>e.key)}}))}})", frames(root))).await
|
||||
}
|
||||
|
||||
async fn srcdoc(page: &mut HistoryPage, frame: &str, markup: &str) -> Result<()> {
|
||||
page.evaluate_await(&format!(
|
||||
"(async()=>{{const f=({frame}); const loaded=new Promise(resolve=>f.addEventListener('load',()=>setTimeout(resolve,0),{{once:true}})); f.srcdoc={}; await loaded; return true;}})()",
|
||||
serde_json::to_string(markup)?)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn nested_history_restores_interleaved_frames_after_parent_document_replacement() -> Result<()>
|
||||
{
|
||||
let server = SmokeFixtureServer::start().await;
|
||||
let initial_url = markup_url(&server, "<!doctype html><body><p>initial</p>");
|
||||
for popup in [false, true] {
|
||||
for layout in ["named", "unnamed", "dynamic"] {
|
||||
let dynamic = if layout == "dynamic" {
|
||||
format!(
|
||||
"<script>const f=document.createElement('iframe');f.name='a';f.src={};document.body.prepend(f);</script>",
|
||||
serde_json::to_string(&initial_url)?
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let names = if layout == "unnamed" {
|
||||
["", ""]
|
||||
} else {
|
||||
["a", "b"]
|
||||
};
|
||||
let source = markup_url(
|
||||
&server,
|
||||
&format!(
|
||||
"<!doctype html><body>{dynamic}{}{}",
|
||||
frame(&initial_url, names[0]),
|
||||
frame(&initial_url, names[1])
|
||||
),
|
||||
);
|
||||
let away = markup_url(&server, "<!doctype html><body><p>away from parent</p>");
|
||||
let (mut page, root) = open_root(&source, popup, &server.url("/plain?opener")).await?;
|
||||
let list = frames(root);
|
||||
let initial = snapshot(&mut page, root).await?;
|
||||
let mut stages = vec![initial.clone()];
|
||||
for (index, text) in [
|
||||
(0, "first historical source"),
|
||||
(1, "second historical source"),
|
||||
] {
|
||||
srcdoc(
|
||||
&mut page,
|
||||
&format!("({list})[{index}]"),
|
||||
&format!("<p>{text}</p>"),
|
||||
)
|
||||
.await?;
|
||||
wait(
|
||||
&mut page,
|
||||
&format!("({list})[{index}].contentDocument.body.textContent === '{text}'"),
|
||||
)
|
||||
.await?;
|
||||
stages.push(snapshot(&mut page, root).await?);
|
||||
}
|
||||
page.evaluate(&format!(
|
||||
"({list})[0].contentWindow.location.hash = 'fragment'; true"
|
||||
))
|
||||
.await?;
|
||||
wait(
|
||||
&mut page,
|
||||
&format!("({list})[0].contentWindow.location.hash === '#fragment'"),
|
||||
)
|
||||
.await?;
|
||||
page.evaluate(&format!("({list})[0].contentWindow.history.replaceState({{classic:3}}, ''); ({list})[0].contentWindow.navigation.updateCurrentEntry({{state:{{navigation:3}}}}); true")).await?;
|
||||
let expected = snapshot(&mut page, root).await?;
|
||||
assert_eq!(
|
||||
expected["length"].as_u64(),
|
||||
initial["length"].as_u64().map(|length| length + 3)
|
||||
);
|
||||
|
||||
navigate(
|
||||
&mut page,
|
||||
root,
|
||||
&format!("location.href = {}", serde_json::to_string(&away)?),
|
||||
&away,
|
||||
)
|
||||
.await?;
|
||||
navigate(&mut page, root, "history.back()", &source).await?;
|
||||
let restored = snapshot(&mut page, root).await?;
|
||||
assert_eq!(
|
||||
restored["frames"], expected["frames"],
|
||||
"popup={popup}, {layout}: {restored}"
|
||||
);
|
||||
assert_eq!(
|
||||
restored["length"].as_u64(),
|
||||
initial["length"].as_u64().map(|length| length + 4)
|
||||
);
|
||||
for reference in stages.iter().rev() {
|
||||
page.evaluate(&format!("({root}).history.back(); true"))
|
||||
.await?;
|
||||
let urls = reference["frames"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|frame| frame["url"].clone())
|
||||
.collect::<Vec<_>>();
|
||||
wait(
|
||||
&mut page,
|
||||
&format!(
|
||||
"JSON.stringify(({list}).map(f=>f.contentWindow.location.href)) === {}",
|
||||
serde_json::to_string(&serde_json::to_string(&urls)?)?
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let current = snapshot(&mut page, root).await?;
|
||||
for index in 0..2 {
|
||||
assert_eq!(
|
||||
current["frames"][index]["id"], reference["frames"][index]["id"],
|
||||
"popup={popup}, {layout}: {current}"
|
||||
);
|
||||
}
|
||||
}
|
||||
navigate(&mut page, root, "history.go(4)", &away).await?;
|
||||
navigate(&mut page, root, "history.go(-3)", &source).await?;
|
||||
let restored = snapshot(&mut page, root).await?;
|
||||
for index in 0..2 {
|
||||
assert_eq!(
|
||||
restored["frames"][index]["id"], stages[1]["frames"][index]["id"],
|
||||
"popup={popup}, {layout}: {restored}"
|
||||
);
|
||||
}
|
||||
page.evaluate(&format!(
|
||||
"({list})[1].contentWindow.history.pushState({{fork:true}}, '', '#fork'); true"
|
||||
))
|
||||
.await?;
|
||||
assert_eq!(
|
||||
value(&mut page, &format!("({root}).history.length")).await?,
|
||||
json!(initial["length"].as_u64().unwrap() + 2)
|
||||
);
|
||||
if layout == "dynamic" {
|
||||
assert_eq!(value(&mut page, &format!("({root}).document.querySelector('iframe').contentDocument.body.textContent")).await?, json!("initial"));
|
||||
}
|
||||
if popup {
|
||||
page.evaluate("testPopup.close(); true").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn nested_history_retains_children_of_a_script_created_parent_navigable() -> Result<()> {
|
||||
let server = SmokeFixtureServer::start().await;
|
||||
let leaf = markup_url(&server, "<!doctype html><body><p>initial</p>");
|
||||
let parent = markup_url(
|
||||
&server,
|
||||
&format!("<!doctype html><body>{}", frame(&leaf, "grandchild")),
|
||||
);
|
||||
let source = markup_url(
|
||||
&server,
|
||||
&format!(
|
||||
"<!doctype html><body><script>const f=document.createElement('iframe');f.src={};document.body.append(f);</script>",
|
||||
serde_json::to_string(&parent)?
|
||||
),
|
||||
);
|
||||
let away = markup_url(&server, "<!doctype html><body><p>away from child</p>");
|
||||
for popup in [false, true] {
|
||||
let (mut page, root) = open_root(&source, popup, &server.url("/plain?opener")).await?;
|
||||
let container = format!("({root}).document.querySelector('iframe').contentWindow");
|
||||
let child = format!("({container}).document.querySelector('iframe')");
|
||||
srcdoc(&mut page, &child, "<p>script parent historical child</p>").await?;
|
||||
let id = value(
|
||||
&mut page,
|
||||
&format!("({child}).contentWindow.navigation.currentEntry.id"),
|
||||
)
|
||||
.await?;
|
||||
navigate(
|
||||
&mut page,
|
||||
&container,
|
||||
&format!("location.href = {}", serde_json::to_string(&away)?),
|
||||
&away,
|
||||
)
|
||||
.await?;
|
||||
navigate(&mut page, &container, "history.back()", &parent).await?;
|
||||
assert_eq!(
|
||||
value(
|
||||
&mut page,
|
||||
&format!("({child}).contentWindow.navigation.currentEntry.id")
|
||||
)
|
||||
.await?,
|
||||
id
|
||||
);
|
||||
assert_eq!(
|
||||
value(
|
||||
&mut page,
|
||||
&format!("({child}).contentDocument.body.textContent")
|
||||
)
|
||||
.await?,
|
||||
json!("script parent historical child")
|
||||
);
|
||||
if popup {
|
||||
page.evaluate("testPopup.close(); true").await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn nested_history_restores_grandchildren_with_their_parent_document_identity() -> Result<()> {
|
||||
let server = SmokeFixtureServer::start().await;
|
||||
let leaf = markup_url(&server, "<!doctype html><body><p>initial</p>");
|
||||
let parent = markup_url(
|
||||
&server,
|
||||
&format!("<!doctype html><body>{}", frame(&leaf, "grandchild")),
|
||||
);
|
||||
let source = markup_url(
|
||||
&server,
|
||||
&format!("<!doctype html><body>{}", frame(&parent, "child")),
|
||||
);
|
||||
let away = markup_url(&server, "<!doctype html><body><p>away from grandchild</p>");
|
||||
for popup in [false, true] {
|
||||
let (mut page, root) = open_root(&source, popup, &server.url("/plain?opener")).await?;
|
||||
let child = format!(
|
||||
"({root}).document.querySelector('iframe').contentDocument.querySelector('iframe')"
|
||||
);
|
||||
srcdoc(&mut page, &child, "<p>historical grandchild</p>").await?;
|
||||
wait(
|
||||
&mut page,
|
||||
&format!("({child}).contentDocument.body.textContent === 'historical grandchild'"),
|
||||
)
|
||||
.await?;
|
||||
let id = value(
|
||||
&mut page,
|
||||
&format!("({child}).contentWindow.navigation.currentEntry.id"),
|
||||
)
|
||||
.await?;
|
||||
navigate(
|
||||
&mut page,
|
||||
root,
|
||||
&format!("location.href = {}", serde_json::to_string(&away)?),
|
||||
&away,
|
||||
)
|
||||
.await?;
|
||||
navigate(&mut page, root, "history.back()", &source).await?;
|
||||
assert_eq!(
|
||||
value(
|
||||
&mut page,
|
||||
&format!("({child}).contentWindow.navigation.currentEntry.id")
|
||||
)
|
||||
.await?,
|
||||
id
|
||||
);
|
||||
assert_eq!(
|
||||
value(
|
||||
&mut page,
|
||||
&format!("({child}).contentDocument.body.textContent")
|
||||
)
|
||||
.await?,
|
||||
json!("historical grandchild")
|
||||
);
|
||||
if popup {
|
||||
page.evaluate("testPopup.close(); true").await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -290,6 +290,7 @@ async fn fixture_handler(
|
||||
&[],
|
||||
),
|
||||
"/plain" => html("<!doctype html><main>plain ok</main>"),
|
||||
"/history-markup" => html(query.get("markup").map(String::as_str).unwrap_or_default()),
|
||||
"/iframe" => html(r#"<!doctype html><main>parent</main><iframe src="/child"></iframe>"#),
|
||||
"/child" => html("<!doctype html><body>child body text</body>"),
|
||||
"/wait-for-function" => html(
|
||||
|
||||
@@ -73,6 +73,8 @@ mod navigation_restore;
|
||||
mod navigation_result;
|
||||
mod navigation_seed;
|
||||
mod navigation_serialize;
|
||||
pub(crate) use navigation_entry::navigation_entry_public_token;
|
||||
pub(crate) use navigation_serialize::history_document_id_for_holder;
|
||||
mod navigation_surface;
|
||||
mod navigation_traversal;
|
||||
mod navigation_traversal_execution;
|
||||
|
||||
@@ -113,6 +113,28 @@ pub(super) fn refresh<'s>(
|
||||
if pushing == Some(child) {
|
||||
child_snapshot = before_push(child_snapshot);
|
||||
}
|
||||
if let Some(current) = child_snapshot
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.index == child_snapshot.current_index)
|
||||
&& let Some(positions) = host
|
||||
.take_restored_child_history_positions(handle, ¤t.key)
|
||||
.or_else(|| {
|
||||
if host
|
||||
.joint_histories
|
||||
.get(root)?
|
||||
.navigable_history(child)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
host.saved_child_history_positions(handle, ¤t.key)
|
||||
})
|
||||
{
|
||||
host.joint_histories
|
||||
.get_mut(root)?
|
||||
.restore_child(child, positions, ¤t.key);
|
||||
}
|
||||
host.joint_histories
|
||||
.get_mut(root)?
|
||||
.ensure_child(child, child_snapshot);
|
||||
@@ -120,6 +142,7 @@ pub(super) fn refresh<'s>(
|
||||
host.joint_histories
|
||||
.get_mut(root)?
|
||||
.retain_navigables(|owner| live.contains(&owner));
|
||||
host.remember_nested_histories(root);
|
||||
Some(root)
|
||||
}
|
||||
|
||||
@@ -261,7 +284,7 @@ fn apply_plan(
|
||||
host: &mut JsContextHost,
|
||||
source_target: WindowTaskTarget,
|
||||
root: OwnerDispatchScope,
|
||||
plan: JointHistoryTraversal,
|
||||
mut plan: JointHistoryTraversal,
|
||||
mut method: Option<PendingHistoryTraversal>,
|
||||
) {
|
||||
if plan.targets.iter().any(|target| {
|
||||
@@ -305,9 +328,47 @@ fn apply_plan(
|
||||
host.record_pending_top_level_history_traversal(
|
||||
i64::from(target.index) - i64::from(current),
|
||||
);
|
||||
host.top_level_navigation_history()
|
||||
.select_joint_traversal(target.key.clone(), plan.step);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A replaced parent reconstructs its descendants from the target
|
||||
// Document's nested histories. Traversing the old descendants as well
|
||||
// can commit into a subtree whose parent is already being retired.
|
||||
let mut replacing_documents = Vec::new();
|
||||
for target in &plan.targets {
|
||||
let Some(window) = window_for_owner(scope, host, target.owner) else {
|
||||
continue;
|
||||
};
|
||||
let Some(history) = window_history_for_holder(scope, window) else {
|
||||
continue;
|
||||
};
|
||||
let current = history_index(scope, history);
|
||||
if super::navigation_seed::history_entry_seed_for_traversal(
|
||||
scope,
|
||||
window,
|
||||
current,
|
||||
target.index,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
replacing_documents.push(target.owner);
|
||||
}
|
||||
}
|
||||
plan.targets.retain(|target| {
|
||||
let mut owner = target.owner;
|
||||
while let OwnerDispatchScope::Child(handle) = owner {
|
||||
let Some(parent) = host.owner_dispatch_scope_for_node(handle) else {
|
||||
break;
|
||||
};
|
||||
if replacing_documents.contains(&parent) {
|
||||
return false;
|
||||
}
|
||||
owner = parent;
|
||||
}
|
||||
true
|
||||
});
|
||||
let targets = plan
|
||||
.targets
|
||||
.iter()
|
||||
@@ -437,6 +498,22 @@ pub(crate) fn commit<'s>(
|
||||
let Some(snapshot) = snapshot(scope, history) else {
|
||||
return;
|
||||
};
|
||||
if pushing.is_some() {
|
||||
let root_window = runtime_top_window_owner(scope, owner);
|
||||
if let Some(seed) = super::navigation_serialize::capture_navigation_entry_seed_for_holder(
|
||||
scope,
|
||||
root_window,
|
||||
) && let Some(joint) = host.joint_histories.get(root)
|
||||
{
|
||||
let step = joint.source_step();
|
||||
let roots = seed
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| entry.document_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
host.nested_history_store(root).prune(step, &roots);
|
||||
}
|
||||
}
|
||||
let Some(joint) = host.joint_histories.get_mut(root) else {
|
||||
return;
|
||||
};
|
||||
@@ -466,6 +543,7 @@ pub(crate) fn commit<'s>(
|
||||
set_history_length(scope, history, length);
|
||||
prune_runtime_entries(scope, host, removed);
|
||||
finish_traversal(scope, host, root);
|
||||
host.remember_nested_histories(root);
|
||||
}
|
||||
|
||||
fn finish_traversal(
|
||||
|
||||
@@ -274,7 +274,7 @@ pub(super) fn navigation_entry_referrer_policy_value<'s>(
|
||||
navigation_entry_stored_string(scope, entry, NAVIGATION_ENTRY_REFERRER_POLICY_SLOT)
|
||||
}
|
||||
|
||||
pub(super) fn navigation_entry_public_token(token: &str) -> String {
|
||||
pub(crate) fn navigation_entry_public_token(token: &str) -> String {
|
||||
if token.is_empty() || is_uuid_v4_like(token) {
|
||||
return token.to_owned();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
referrer_policy::normalize_referrer_policy, util::context_host_ptr_from_window_object,
|
||||
};
|
||||
|
||||
fn capture_navigation_entry_seed_for_holder<'s>(
|
||||
pub(crate) fn capture_navigation_entry_seed_for_holder<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) -> Option<NavigationHistoryEntrySeed> {
|
||||
@@ -34,6 +34,14 @@ fn capture_navigation_entry_seed_for_holder<'s>(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn history_document_id_for_holder<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) -> Option<NavigationHistoryDocumentId> {
|
||||
let entry = navigation_current_entry(scope, owner)?;
|
||||
navigation_entry_document_id(scope, entry).map(NavigationHistoryDocumentId::from_serialized)
|
||||
}
|
||||
|
||||
pub(super) fn sync_navigation_entry_seed_from_owner<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -388,13 +388,13 @@ pub(crate) fn parse_browsing_context_document_projection_from_source<'s>(
|
||||
let source = preserve_decoded_bom_only_browsing_context_body(source, content_type);
|
||||
let (parsed, kind) =
|
||||
parse_browsing_context_document_snapshot(document_url, &source, content_type, html_parser);
|
||||
build_detached_document_from_dom_host_with_content_type(
|
||||
build_detached_document_object_from_dom_host_with_content_type(
|
||||
scope,
|
||||
kind.bridge_kind(),
|
||||
parsed,
|
||||
kind,
|
||||
true,
|
||||
content_type,
|
||||
character_set,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -473,6 +473,7 @@ fn build_detached_document_with_content_type<'s>(
|
||||
DomHost::from_dom(parsed),
|
||||
content_type,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -501,6 +502,7 @@ fn build_detached_document_from_dom_host_with_content_type<'s>(
|
||||
parsed,
|
||||
content_type,
|
||||
character_set,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ impl JsContextHost {
|
||||
{
|
||||
entry.remember_srcdoc_history_resource(snapshot, origin);
|
||||
}
|
||||
self.remember_child_history(handle);
|
||||
let owner_local_window_id = current_owner.local_window_id;
|
||||
let owner_document_id = current_owner.document_id;
|
||||
self.dom_host_mut()
|
||||
|
||||
@@ -476,6 +476,7 @@ impl JsContextHost {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
handle: DomHandle,
|
||||
) {
|
||||
let restoring = self.take_child_history_restoration(handle);
|
||||
let increments_joint_history = self
|
||||
.child_browsing_contexts
|
||||
.get_mut(&handle)
|
||||
@@ -493,7 +494,7 @@ impl JsContextHost {
|
||||
});
|
||||
let update = if increments_joint_history {
|
||||
moli_page_types::SameDocumentHistoryUpdate::Push
|
||||
} else if traversing {
|
||||
} else if traversing && !restoring {
|
||||
moli_page_types::SameDocumentHistoryUpdate::Traverse { delta: 0 }
|
||||
} else {
|
||||
moli_page_types::SameDocumentHistoryUpdate::Replace
|
||||
|
||||
@@ -75,7 +75,11 @@ impl JsContextHost {
|
||||
let Some(entry) = self.child_browsing_contexts.get_mut(&handle) else {
|
||||
return false;
|
||||
};
|
||||
entry.set_navigation_entry_seed(entry_seed)
|
||||
let committed = entry.set_navigation_entry_seed(entry_seed);
|
||||
if committed {
|
||||
self.remember_child_history(handle);
|
||||
}
|
||||
committed
|
||||
}
|
||||
|
||||
pub(crate) fn pending_child_browsing_context_navigation_position(
|
||||
|
||||
@@ -27,6 +27,7 @@ mod classic_scripts;
|
||||
mod discovery;
|
||||
mod lookup;
|
||||
mod module_scripts;
|
||||
mod nested_history;
|
||||
mod registry;
|
||||
mod request_scope;
|
||||
mod srcdoc_history;
|
||||
@@ -34,6 +35,8 @@ mod stylesheets;
|
||||
|
||||
pub(in crate::native_bridge::context_host) use classic_scripts::ChildParserClassicScriptCandidate;
|
||||
pub(in crate::native_bridge::context_host) use classic_scripts::PendingChildExternalClassicDocumentScriptLoad;
|
||||
pub(super) use nested_history::ChildHistoryIdentity;
|
||||
pub(crate) use nested_history::{NestedHistoryStore, joint_snapshot};
|
||||
pub(crate) use request_scope::WebStorageScope;
|
||||
pub(in crate::native_bridge::context_host) use request_scope::{
|
||||
document_sandbox_policy_from_attribute, sandbox_attribute_forces_opaque_origin,
|
||||
@@ -42,6 +45,9 @@ pub(in crate::native_bridge::context_host) use request_scope::{
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct ChildBrowsingContextEntry {
|
||||
frame_id: String,
|
||||
history_identity: Option<ChildHistoryIdentity>,
|
||||
restored_history_positions: Option<crate::native_bridge::joint_history::NavigableHistory>,
|
||||
restoring_history: bool,
|
||||
current_document_loader_id: Option<String>,
|
||||
name: Option<String>,
|
||||
id: Option<String>,
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
use super::*;
|
||||
use crate::context_bootstrap::navigation_entry_public_token;
|
||||
use crate::native_bridge::{
|
||||
OwnerDispatchScope,
|
||||
joint_history::{
|
||||
JointHistoryEntry, JointHistorySnapshot, NavigableHistory, SessionHistoryStep,
|
||||
},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
/// A parser-created frame can be recreated when its parent Document is
|
||||
/// repopulated. Script-created frames retain their own nested histories, but
|
||||
/// a new script-created navigable must not claim an old one's history.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub(in crate::native_bridge::context_host) struct ChildHistoryIdentity {
|
||||
parent: NavigationHistoryDocumentId,
|
||||
key: ChildHistoryKey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
enum ChildHistoryKey {
|
||||
Parser { name: String, ordinal: usize },
|
||||
Script(NavigationHistoryDocumentId),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ChildHistoryRecord {
|
||||
seed: NavigationHistoryEntrySeed,
|
||||
resources: HashMap<NavigationHistoryDocumentId, Arc<srcdoc_history::SrcdocHistoryResource>>,
|
||||
positions: NavigableHistory,
|
||||
}
|
||||
|
||||
/// This store contains no live DOM handles or V8 objects. Retiring a Document
|
||||
/// can release its entire subtree without discarding its nested histories.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct NestedHistoryStore {
|
||||
records: Arc<Mutex<HashMap<ChildHistoryIdentity, ChildHistoryRecord>>>,
|
||||
}
|
||||
|
||||
impl NestedHistoryStore {
|
||||
pub(crate) fn prune(&self, step: SessionHistoryStep, roots: &[NavigationHistoryDocumentId]) {
|
||||
let mut records = self.records.lock();
|
||||
for record in records.values_mut() {
|
||||
record.positions.retain_through(step);
|
||||
record.seed.entries.retain(|entry| {
|
||||
record
|
||||
.positions
|
||||
.contains(&navigation_entry_public_token(entry.key.as_str()))
|
||||
});
|
||||
record.resources.retain(|id, _| {
|
||||
record
|
||||
.seed
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| &entry.document_id == id)
|
||||
});
|
||||
}
|
||||
let mut reachable: HashSet<_> = roots.iter().cloned().collect();
|
||||
loop {
|
||||
let count = reachable.len();
|
||||
for (identity, record) in records.iter() {
|
||||
if reachable.contains(&identity.parent) {
|
||||
reachable.extend(
|
||||
record
|
||||
.seed
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| entry.document_id.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
if reachable.len() == count {
|
||||
break;
|
||||
}
|
||||
}
|
||||
records.retain(|identity, record| {
|
||||
reachable.contains(&identity.parent) && !record.seed.entries.is_empty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn joint_snapshot(seed: &NavigationHistoryEntrySeed) -> JointHistorySnapshot {
|
||||
JointHistorySnapshot {
|
||||
entries: seed
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| JointHistoryEntry {
|
||||
index: entry.history_index,
|
||||
navigation_index: entry.index,
|
||||
key: navigation_entry_public_token(entry.key.as_str()),
|
||||
})
|
||||
.collect(),
|
||||
current_index: seed.current_index,
|
||||
}
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
fn nested_history_root(&self, handle: DomHandle) -> Option<OwnerDispatchScope> {
|
||||
let mut owner = self.owner_dispatch_scope_for_node(handle)?;
|
||||
while let OwnerDispatchScope::Child(parent) = owner {
|
||||
owner = self.owner_dispatch_scope_for_node(parent)?;
|
||||
}
|
||||
Some(owner)
|
||||
}
|
||||
|
||||
pub(crate) fn nested_history_store(&mut self, root: OwnerDispatchScope) -> NestedHistoryStore {
|
||||
if root == OwnerDispatchScope::Top {
|
||||
return self.top_level_navigation_history.nested_history();
|
||||
}
|
||||
self.popup_nested_histories.entry(root).or_default().clone()
|
||||
}
|
||||
|
||||
pub(super) fn child_history_identity(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
handle: DomHandle,
|
||||
) -> Option<ChildHistoryIdentity> {
|
||||
let node = self.dom_host().node(handle)?;
|
||||
let parser_created = node.flags().parser_created();
|
||||
let document = node.owner_document()?;
|
||||
let owner = self.owner_dispatch_scope_for_node(handle)?;
|
||||
let parent = match owner {
|
||||
OwnerDispatchScope::Top => self.top_level_navigation_history.current_document_id()?,
|
||||
OwnerDispatchScope::Child(parent) => {
|
||||
let seed = &self
|
||||
.child_browsing_contexts
|
||||
.get(&parent)?
|
||||
.committed_navigation_entry_seed;
|
||||
seed.entries
|
||||
.iter()
|
||||
.find(|entry| entry.history_index == seed.current_index)?
|
||||
.document_id
|
||||
.clone()
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(id) => {
|
||||
let window = self.lightweight_popup_window(scope, id)?;
|
||||
crate::context_bootstrap::history_document_id_for_holder(scope, window)?
|
||||
}
|
||||
};
|
||||
if !parser_created {
|
||||
return Some(ChildHistoryIdentity {
|
||||
parent,
|
||||
key: ChildHistoryKey::Script(NavigationHistoryDocumentId::allocate()),
|
||||
});
|
||||
}
|
||||
let name = self
|
||||
.dom_host()
|
||||
.get_attribute(handle, "name")
|
||||
.unwrap_or_default();
|
||||
let mut handles = Vec::new();
|
||||
self.collect_child_browsing_context_host_handles(document, &mut handles);
|
||||
let ordinal = handles
|
||||
.into_iter()
|
||||
.filter(|candidate| {
|
||||
self.dom_host()
|
||||
.node(*candidate)
|
||||
.is_some_and(|node| node.flags().parser_created())
|
||||
&& self
|
||||
.dom_host()
|
||||
.get_attribute(*candidate, "name")
|
||||
.unwrap_or_default()
|
||||
== name
|
||||
})
|
||||
.position(|candidate| candidate == handle)?;
|
||||
Some(ChildHistoryIdentity {
|
||||
parent,
|
||||
key: ChildHistoryKey::Parser { name, ordinal },
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn child_history_for_restoration(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
identity: &ChildHistoryIdentity,
|
||||
) -> Option<ChildHistoryRecord> {
|
||||
// Consume each parser identity once in this native parent Document.
|
||||
// Removing/reinserting a frame in a still-live Document is not history traversal.
|
||||
let document = self.dom_host().node(handle)?.owner_document()?;
|
||||
if !self
|
||||
.claimed_child_histories
|
||||
.entry(document)
|
||||
.or_default()
|
||||
.insert(identity.clone())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let root = self.nested_history_root(handle)?;
|
||||
let step = self.joint_histories.get(root)?.destination_step();
|
||||
let store = self.nested_history_store(root);
|
||||
let mut record = store.records.lock().get(identity)?.clone();
|
||||
let key = &record.positions.entry_at(step)?.key;
|
||||
let target = record
|
||||
.seed
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| navigation_entry_public_token(entry.key.as_str()) == *key)?
|
||||
.clone();
|
||||
record.seed.current_index = target.history_index;
|
||||
record.seed.activation = Some(NavigationActivationSeed {
|
||||
entry: target,
|
||||
from: None,
|
||||
navigation_type: Some("traverse".to_owned()),
|
||||
});
|
||||
Some(record)
|
||||
}
|
||||
|
||||
pub(super) fn restore_child_history_after_initial_empty(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
record: ChildHistoryRecord,
|
||||
) {
|
||||
let Some(target) = record
|
||||
.seed
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.history_index == record.seed.current_index)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let url = target.url.clone();
|
||||
if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) {
|
||||
entry.srcdoc_history = record.resources;
|
||||
entry.restored_history_positions = Some(record.positions);
|
||||
entry.restoring_history = true;
|
||||
}
|
||||
self.queue_deferred_child_browsing_context_navigation_from_entry_seed(
|
||||
handle,
|
||||
&url,
|
||||
record.seed,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn take_restored_child_history_positions(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
key: &str,
|
||||
) -> Option<NavigableHistory> {
|
||||
let entry = self.child_browsing_contexts.get_mut(&handle)?;
|
||||
if !entry.restored_history_positions.as_ref()?.contains(key) {
|
||||
return None;
|
||||
}
|
||||
entry.restored_history_positions.take()
|
||||
}
|
||||
|
||||
pub(crate) fn saved_child_history_positions(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
key: &str,
|
||||
) -> Option<NavigableHistory> {
|
||||
let identity = self
|
||||
.child_browsing_contexts
|
||||
.get(&handle)?
|
||||
.history_identity
|
||||
.clone()?;
|
||||
let root = self.nested_history_root(handle)?;
|
||||
let store = self.nested_history_store(root);
|
||||
let records = store.records.lock();
|
||||
let positions = &records.get(&identity)?.positions;
|
||||
positions.contains(key).then(|| positions.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn remember_child_history(&mut self, handle: DomHandle) {
|
||||
let Some(root) = self.nested_history_root(handle) else {
|
||||
return;
|
||||
};
|
||||
let store = self.nested_history_store(root);
|
||||
let Some(entry) = self.child_browsing_contexts.get(&handle) else {
|
||||
return;
|
||||
};
|
||||
let Some(identity) = entry.history_identity.clone() else {
|
||||
return;
|
||||
};
|
||||
let seed = entry.committed_navigation_entry_seed();
|
||||
let joint = self.joint_histories.get(root);
|
||||
let positions = entry
|
||||
.restored_history_positions
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
joint.and_then(|joint| joint.navigable_history(OwnerDispatchScope::Child(handle)))
|
||||
})
|
||||
// Lazy or retiring realms may have no live joint-ledger entry.
|
||||
// Their committed positions still belong to the navigable and
|
||||
// must not be rebased to the traversable's current step.
|
||||
.or_else(|| {
|
||||
store
|
||||
.records
|
||||
.lock()
|
||||
.get(&identity)
|
||||
.map(|record| record.positions.clone())
|
||||
})
|
||||
.or_else(|| {
|
||||
NavigableHistory::new(
|
||||
joint_snapshot(&seed),
|
||||
joint.map_or(SessionHistoryStep::INITIAL, |joint| {
|
||||
joint.destination_step()
|
||||
}),
|
||||
)
|
||||
});
|
||||
let Some(positions) = positions else {
|
||||
return;
|
||||
};
|
||||
// A new runtime can briefly expose its initial empty Document while
|
||||
// the historical target is pending. It must not overwrite that target.
|
||||
if !seed
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.history_index == seed.current_index)
|
||||
.is_some_and(|entry| {
|
||||
positions.contains(&navigation_entry_public_token(entry.key.as_str()))
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
let record = ChildHistoryRecord {
|
||||
seed,
|
||||
resources: entry.srcdoc_history.clone(),
|
||||
positions,
|
||||
};
|
||||
store.records.lock().insert(identity, record);
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn take_child_history_restoration(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
) -> bool {
|
||||
self.child_browsing_contexts
|
||||
.get_mut(&handle)
|
||||
.is_some_and(|entry| std::mem::take(&mut entry.restoring_history))
|
||||
}
|
||||
|
||||
pub(crate) fn remember_nested_histories(&mut self, root: OwnerDispatchScope) {
|
||||
let handles: Vec<_> = self
|
||||
.child_browsing_contexts
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|handle| self.nested_history_root(*handle) == Some(root))
|
||||
.collect();
|
||||
for handle in handles {
|
||||
self.remember_child_history(handle);
|
||||
}
|
||||
if root == OwnerDispatchScope::Top {
|
||||
self.top_level_navigation_history.publish_joint_history(
|
||||
self.joint_histories
|
||||
.get(root)
|
||||
.and_then(|joint| joint.restoration(root)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ impl JsContextHost {
|
||||
retired_owner: crate::frame_owner_model::FrameDocumentTaskOwner,
|
||||
document_handle: DomHandle,
|
||||
) {
|
||||
self.claimed_child_histories.remove(&document_handle);
|
||||
let _ = self.retire_document_resource_loader(
|
||||
crate::native_bridge::WindowDocumentOwner::Frame(retired_owner),
|
||||
);
|
||||
@@ -96,6 +97,17 @@ impl JsContextHost {
|
||||
self.object_fallback_bootstraps.remove(&handle);
|
||||
let existing = self.child_browsing_contexts.get(&handle).cloned();
|
||||
let is_new = existing.is_none();
|
||||
let history_identity = existing
|
||||
.as_ref()
|
||||
.and_then(|entry| entry.history_identity.clone())
|
||||
.or_else(|| self.child_history_identity(scope, handle));
|
||||
let restored_history = if is_new {
|
||||
history_identity
|
||||
.as_ref()
|
||||
.and_then(|identity| self.child_history_for_restoration(handle, identity))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let attribute_bootstrap_changed = existing
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.attribute_bootstrap_changed(&attribute_bootstrap));
|
||||
@@ -301,10 +313,12 @@ impl JsContextHost {
|
||||
});
|
||||
}
|
||||
let initial_about_blank_document_is_complete = is_new
|
||||
&& restored_history.is_none()
|
||||
&& child_browsing_context_bootstrap_is_initial_about_blank(
|
||||
&attribute_bootstrap,
|
||||
);
|
||||
let initial_navigation_uses_initial_empty_load = is_new
|
||||
&& restored_history.is_none()
|
||||
&& child_browsing_context_bootstrap_uses_initial_empty_load(
|
||||
&attribute_bootstrap,
|
||||
);
|
||||
@@ -438,6 +452,13 @@ impl JsContextHost {
|
||||
handle,
|
||||
ChildBrowsingContextEntry {
|
||||
frame_id,
|
||||
history_identity,
|
||||
restoring_history: existing
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.restoring_history),
|
||||
restored_history_positions: existing
|
||||
.as_ref()
|
||||
.and_then(|entry| entry.restored_history_positions.clone()),
|
||||
current_document_loader_id: existing.as_ref().and_then(|entry| {
|
||||
entry.current_document_loader_id().map(ToOwned::to_owned)
|
||||
}),
|
||||
@@ -587,6 +608,9 @@ impl JsContextHost {
|
||||
{
|
||||
entry.set_document_permissions_policy(policy);
|
||||
}
|
||||
if let Some(restored) = restored_history {
|
||||
self.restore_child_history_after_initial_empty(handle, restored);
|
||||
}
|
||||
if attribute_bootstrap_changed {
|
||||
self.cancel_child_meta_refresh_navigation(handle);
|
||||
}
|
||||
|
||||
@@ -339,6 +339,8 @@ impl JsContextHost {
|
||||
opfs_owner_state: None,
|
||||
history_queue: HistoryQueueState::default(),
|
||||
joint_histories: super::super::joint_history::JointSessionHistories::default(),
|
||||
popup_nested_histories: HashMap::new(),
|
||||
claimed_child_histories: HashMap::new(),
|
||||
rendering_updates: super::rendering_updates::RenderingUpdateState::default(),
|
||||
scroll_observable_effect_batch:
|
||||
super::interaction_batch::ScrollObservableEffectBatchState::default(),
|
||||
|
||||
@@ -195,6 +195,7 @@ pub(crate) use child_frame_snapshots::{
|
||||
};
|
||||
use child_frames::ChildBrowsingContextEntry;
|
||||
pub(in crate::native_bridge::context_host) use child_frames::ChildParserClassicScriptCandidate;
|
||||
pub(crate) use child_frames::{NestedHistoryStore, joint_snapshot};
|
||||
pub(crate) use event_callbacks::{EventCallbackId, PreparedEventCallback};
|
||||
pub(crate) use host_loads::ChildFrameAttachmentSnapshot;
|
||||
#[cfg(test)]
|
||||
@@ -970,6 +971,8 @@ pub(crate) struct JsContextHost {
|
||||
opfs_owner_state: Option<opfs_tasks::WindowOpfsOwnerState>,
|
||||
pub(super) history_queue: HistoryQueueState,
|
||||
pub(crate) joint_histories: super::joint_history::JointSessionHistories,
|
||||
popup_nested_histories: HashMap<OwnerDispatchScope, NestedHistoryStore>,
|
||||
claimed_child_histories: HashMap<DomHandle, HashSet<child_frames::ChildHistoryIdentity>>,
|
||||
rendering_updates: rendering_updates::RenderingUpdateState,
|
||||
scroll_observable_effect_batch: interaction_batch::ScrollObservableEffectBatchState,
|
||||
view_transition_updates: view_transition_updates::ViewTransitionUpdateState,
|
||||
|
||||
@@ -98,6 +98,16 @@ impl JsContextHost {
|
||||
self.top_level_navigation_history.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn restore_top_level_navigation_history(
|
||||
&mut self,
|
||||
source: &crate::runtime::RendererNavigationHistory,
|
||||
seed: &NavigationHistoryEntrySeed,
|
||||
) {
|
||||
self.top_level_navigation_history = source.restore_for(seed);
|
||||
self.top_level_navigation_history
|
||||
.install_joint_history(&mut self.joint_histories);
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn child_frame_attribute_initiator_has_same_origin(
|
||||
&self,
|
||||
handle: DomHandle,
|
||||
|
||||
@@ -746,6 +746,8 @@ impl JsContextHost {
|
||||
let Some(transition) = self.take_lightweight_popup_close_transition(popup_id) else {
|
||||
return false;
|
||||
};
|
||||
self.popup_nested_histories
|
||||
.remove(&OwnerDispatchScope::LightweightPopup(popup_id));
|
||||
let window = self.lightweight_popup_window(scope, popup_id);
|
||||
self.unregister_service_worker_popup_client(popup_id);
|
||||
self.cancel_lightweight_popup_document_loads(popup_id);
|
||||
@@ -3006,6 +3008,7 @@ impl JsContextHost {
|
||||
popup_id: u64,
|
||||
document_handle: DomHandle,
|
||||
) {
|
||||
self.claimed_child_histories.remove(&document_handle);
|
||||
if self
|
||||
.lightweight_popup_document_handles
|
||||
.get(&document_handle)
|
||||
|
||||
@@ -208,7 +208,9 @@ fn import_detached_document_children_from_host<'s>(
|
||||
document: v8::Local<'s, v8::Object>,
|
||||
parsed: &DomHost,
|
||||
) -> Option<()> {
|
||||
import_detached_document_children_from_host_with_reaction_policy(scope, document, parsed, false)
|
||||
import_detached_document_children_from_host_with_reaction_policy(
|
||||
scope, document, parsed, false, false,
|
||||
)
|
||||
}
|
||||
|
||||
fn import_detached_document_children_from_host_with_reaction_policy<'s>(
|
||||
@@ -216,6 +218,7 @@ fn import_detached_document_children_from_host_with_reaction_policy<'s>(
|
||||
document: v8::Local<'s, v8::Object>,
|
||||
parsed: &DomHost,
|
||||
append_to_current_reaction_queue: bool,
|
||||
preserve_parser_frames: bool,
|
||||
) -> Option<()> {
|
||||
let runtime_ptr = context_host_ptr_from_global_bridge(scope)?;
|
||||
let document_handle = detached_native_handle(scope, document)?;
|
||||
@@ -224,9 +227,34 @@ fn import_detached_document_children_from_host_with_reaction_policy<'s>(
|
||||
.collect::<Vec<_>>();
|
||||
let lazy_native_import = parsed.dom().len() > 1_000;
|
||||
for child in children {
|
||||
let mut imported_handles = std::collections::HashMap::new();
|
||||
let imported = unsafe { &mut *runtime_ptr }
|
||||
.dom_host_mut()
|
||||
.import_foreign_node_with_shadow_roots(document_handle, parsed, child, true)?;
|
||||
.import_foreign_node_with_shadow_roots_and_handle_map(
|
||||
document_handle,
|
||||
parsed,
|
||||
child,
|
||||
true,
|
||||
&mut imported_handles,
|
||||
)?;
|
||||
if preserve_parser_frames {
|
||||
// This is a navigation Document projection, not author importNode.
|
||||
// Preserve parser frame identity before insertion can discover its
|
||||
// child navigables. Other clone/import paths intentionally clear it.
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
for (source, destination) in imported_handles {
|
||||
if parsed
|
||||
.node(source)
|
||||
.is_some_and(|node| node.flags().parser_created())
|
||||
&& ["iframe", "frame", "object", "embed"]
|
||||
.iter()
|
||||
.any(|name| parsed.is_html_element_named(source, name))
|
||||
&& let Some(node) = runtime.dom_host_mut().node_mut(destination)
|
||||
{
|
||||
node.set_parser_created(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::native_bridge::element::queue_parser_details_toggle_events_in_subtree(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
@@ -268,7 +296,9 @@ pub(crate) fn build_detached_document_object_from_dom_host<'s>(
|
||||
kind: &str,
|
||||
parsed: DomHost,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
build_detached_document_object_from_dom_host_with_content_type(scope, kind, parsed, None, None)
|
||||
build_detached_document_object_from_dom_host_with_content_type(
|
||||
scope, kind, parsed, None, None, false,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_detached_document_object_from_dom_host_with_content_type<'s>(
|
||||
@@ -277,6 +307,7 @@ pub(crate) fn build_detached_document_object_from_dom_host_with_content_type<'s>
|
||||
parsed: DomHost,
|
||||
content_type: Option<&str>,
|
||||
character_set: Option<&str>,
|
||||
preserve_parser_frames: bool,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let url = detached_document_url(&parsed);
|
||||
let quirks_mode = parsed.dom().document()?.quirks_mode();
|
||||
@@ -292,7 +323,13 @@ pub(crate) fn build_detached_document_object_from_dom_host_with_content_type<'s>
|
||||
character_set,
|
||||
allow_declarative_shadow_roots,
|
||||
)?;
|
||||
import_detached_document_children_from_host(scope, document, &parsed)?;
|
||||
if preserve_parser_frames {
|
||||
import_detached_document_children_from_host_with_reaction_policy(
|
||||
scope, document, &parsed, false, true,
|
||||
)?;
|
||||
} else {
|
||||
import_detached_document_children_from_host(scope, document, &parsed)?;
|
||||
}
|
||||
Some(document)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) struct SessionHistoryStep(u64);
|
||||
|
||||
impl SessionHistoryStep {
|
||||
pub(crate) const INITIAL: Self = Self(0);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct JointHistoryEntry {
|
||||
pub(crate) index: u32,
|
||||
@@ -26,12 +30,103 @@ struct EntryPosition {
|
||||
step: SessionHistoryStep,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NavigableHistory {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct NavigableHistory {
|
||||
entries: Vec<EntryPosition>,
|
||||
current_key: String,
|
||||
}
|
||||
|
||||
impl NavigableHistory {
|
||||
pub(crate) fn new(
|
||||
snapshot: JointHistorySnapshot,
|
||||
first_step: SessionHistoryStep,
|
||||
) -> Option<Self> {
|
||||
let current_key = snapshot
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.index == snapshot.current_index)?
|
||||
.key
|
||||
.clone();
|
||||
Some(Self {
|
||||
entries: snapshot
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| EntryPosition {
|
||||
step: SessionHistoryStep(first_step.0 + u64::from(entry.navigation_index)),
|
||||
entry,
|
||||
})
|
||||
.collect(),
|
||||
current_key,
|
||||
})
|
||||
}
|
||||
pub(crate) fn entry_at(&self, step: SessionHistoryStep) -> Option<&JointHistoryEntry> {
|
||||
self.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|entry| entry.step <= step)
|
||||
.map(|entry| &entry.entry)
|
||||
}
|
||||
|
||||
pub(crate) fn retain_through(&mut self, step: SessionHistoryStep) {
|
||||
self.entries.retain(|entry| entry.step <= step);
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, key: &str) -> bool {
|
||||
self.entries.iter().any(|entry| entry.entry.key == key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Committed history positions survive the renderer and DOM handles that
|
||||
/// happened to present them. Child entries are restored separately when their
|
||||
/// parent Document recreates its navigables.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct JointHistoryRestoration {
|
||||
root: NavigableHistory,
|
||||
current_step: SessionHistoryStep,
|
||||
steps: BTreeSet<SessionHistoryStep>,
|
||||
}
|
||||
|
||||
impl JointHistoryRestoration {
|
||||
pub(crate) fn current_step(&self) -> SessionHistoryStep {
|
||||
self.current_step
|
||||
}
|
||||
pub(crate) fn for_navigation(
|
||||
self,
|
||||
snapshot: JointHistorySnapshot,
|
||||
navigation_type: Option<&str>,
|
||||
target_step: Option<SessionHistoryStep>,
|
||||
) -> Option<Self> {
|
||||
let root = OwnerDispatchScope::Top;
|
||||
let mut histories = JointSessionHistories::default();
|
||||
histories.restore_root(root, self);
|
||||
let joint = histories.get_mut(root)?;
|
||||
match navigation_type {
|
||||
Some("push") => {
|
||||
joint.push(root, snapshot);
|
||||
}
|
||||
Some("traverse") => {
|
||||
let key = &snapshot
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.index == snapshot.current_index)?
|
||||
.key;
|
||||
let target = joint.plan_entry(root, key)?;
|
||||
let entry_step = joint
|
||||
.navigables
|
||||
.get(&root)?
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| &entry.entry.key == key)?
|
||||
.step;
|
||||
joint.update_current(root, snapshot, Some(entry_step));
|
||||
joint.current_step = target_step.unwrap_or(target.step);
|
||||
}
|
||||
_ => joint.replace(root, snapshot),
|
||||
}
|
||||
joint.restoration(root)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct JointHistoryTarget {
|
||||
pub(crate) owner: OwnerDispatchScope,
|
||||
@@ -85,6 +180,24 @@ pub(crate) struct JointSessionHistories {
|
||||
}
|
||||
|
||||
impl JointSessionHistories {
|
||||
pub(crate) fn restore_root(
|
||||
&mut self,
|
||||
owner: OwnerDispatchScope,
|
||||
restoration: JointHistoryRestoration,
|
||||
) {
|
||||
self.histories.insert(
|
||||
owner,
|
||||
JointSessionHistory {
|
||||
current_step: restoration.current_step,
|
||||
revision: 0,
|
||||
navigables: IndexMap::from([(owner, restoration.root)]),
|
||||
retired_steps: restoration.steps,
|
||||
pending_traversal: None,
|
||||
deferred_mutations: VecDeque::new(),
|
||||
completed_traversal: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
pub(crate) fn root_for_owner(&self, owner: OwnerDispatchScope) -> Option<OwnerDispatchScope> {
|
||||
self.histories
|
||||
.iter()
|
||||
@@ -127,6 +240,33 @@ impl JointSessionHistories {
|
||||
}
|
||||
|
||||
impl JointSessionHistory {
|
||||
pub(crate) fn restoration(&self, root: OwnerDispatchScope) -> Option<JointHistoryRestoration> {
|
||||
Some(JointHistoryRestoration {
|
||||
root: self.navigables.get(&root)?.clone(),
|
||||
current_step: self.current_step,
|
||||
steps: self.used_steps().into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn navigable_history(&self, owner: OwnerDispatchScope) -> Option<NavigableHistory> {
|
||||
self.navigables.get(&owner).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn restore_child(
|
||||
&mut self,
|
||||
owner: OwnerDispatchScope,
|
||||
mut history: NavigableHistory,
|
||||
current_key: &str,
|
||||
) {
|
||||
history.current_key = current_key.to_owned();
|
||||
self.navigables.insert(owner, history);
|
||||
}
|
||||
|
||||
pub(crate) fn destination_step(&self) -> SessionHistoryStep {
|
||||
self.pending_traversal
|
||||
.as_ref()
|
||||
.map_or(self.current_step, |pending| pending.step)
|
||||
}
|
||||
pub(crate) fn reset(&mut self, snapshots: Vec<(OwnerDispatchScope, JointHistorySnapshot)>) {
|
||||
self.navigables.clear();
|
||||
self.retired_steps.clear();
|
||||
@@ -575,6 +715,63 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_root_and_children_preserve_interleaved_session_history_steps() {
|
||||
let root = OwnerDispatchScope::Top;
|
||||
let old_a = OwnerDispatchScope::Child(DomHandle::new(1));
|
||||
let old_b = OwnerDispatchScope::Child(DomHandle::new(2));
|
||||
let mut histories = JointSessionHistories::default();
|
||||
let original = histories
|
||||
.ensure_root(root, snapshot(&["parent"], 0))
|
||||
.unwrap();
|
||||
original.ensure_child(old_a, snapshot(&["a0"], 0));
|
||||
original.ensure_child(old_b, snapshot(&["b0"], 0));
|
||||
original.push(old_a, snapshot(&["a0", "a1"], 1));
|
||||
original.push(old_b, snapshot(&["b0", "b1"], 1));
|
||||
original.push(old_a, snapshot(&["a0", "a1", "a2"], 2));
|
||||
let a = original.navigable_history(old_a).unwrap();
|
||||
let b = original.navigable_history(old_b).unwrap();
|
||||
let source = original.restoration(root).unwrap();
|
||||
let away = source
|
||||
.for_navigation(snapshot(&["parent", "away"], 1), Some("push"), None)
|
||||
.unwrap();
|
||||
let back = away
|
||||
.for_navigation(
|
||||
snapshot(&["parent", "away"], 0),
|
||||
Some("traverse"),
|
||||
Some(SessionHistoryStep(3)),
|
||||
)
|
||||
.unwrap();
|
||||
drop(histories);
|
||||
|
||||
let mut histories = JointSessionHistories::default();
|
||||
histories.restore_root(root, back);
|
||||
let restored = histories.get_mut(root).unwrap();
|
||||
let new_a = OwnerDispatchScope::Child(DomHandle::new(101));
|
||||
let new_b = OwnerDispatchScope::Child(DomHandle::new(102));
|
||||
restored.restore_child(new_a, a.clone(), "a2");
|
||||
restored.restore_child(new_b, b.clone(), "b1");
|
||||
assert_eq!(restored.length(), 5);
|
||||
for (owner, key) in [(new_a, "a1"), (new_b, "b0"), (new_a, "a0")] {
|
||||
let plan = restored.plan_delta(restored.current_step(), -1).unwrap();
|
||||
assert_eq!(plan.targets.len(), 1);
|
||||
assert_eq!(plan.targets[0].owner, owner);
|
||||
assert_eq!(plan.targets[0].key, key);
|
||||
restored.begin_traversal(plan);
|
||||
restored.commit_traversal_entry(owner, key);
|
||||
}
|
||||
assert_eq!(restored.current_step(), SessionHistoryStep(0));
|
||||
assert_eq!(
|
||||
restored
|
||||
.plan_delta(restored.current_step(), 4)
|
||||
.unwrap()
|
||||
.step,
|
||||
SessionHistoryStep(4)
|
||||
);
|
||||
assert_eq!(a.entry_at(SessionHistoryStep(1)).unwrap().key, "a1");
|
||||
assert_eq!(b.entry_at(SessionHistoryStep(1)).unwrap().key, "b0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interleaved_children_share_steps_and_prune_each_others_forward_entries() {
|
||||
let root = OwnerDispatchScope::Top;
|
||||
|
||||
@@ -8,7 +8,12 @@ use moli_page_types::{
|
||||
use parking_lot::Mutex;
|
||||
use url::Url;
|
||||
|
||||
use crate::native_bridge::{NavigationHistoryEntrySeed, NavigationHistorySerializedEntry};
|
||||
use crate::native_bridge::{
|
||||
NavigationHistoryDocumentId, NavigationHistoryEntrySeed, NavigationHistorySerializedEntry,
|
||||
NestedHistoryStore, OwnerDispatchScope,
|
||||
joint_history::{JointHistoryRestoration, SessionHistoryStep},
|
||||
joint_snapshot,
|
||||
};
|
||||
|
||||
/// The committed history of a renderer's top-level Document. Publishing a
|
||||
/// snapshot happens at a history mutation, never while capturing Page state.
|
||||
@@ -17,6 +22,9 @@ use crate::native_bridge::{NavigationHistoryEntrySeed, NavigationHistorySerializ
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RendererNavigationHistory {
|
||||
snapshot: Arc<Mutex<Option<Arc<NavigationHistoryEntrySeed>>>>,
|
||||
joint: Arc<Mutex<Option<JointHistoryRestoration>>>,
|
||||
nested: NestedHistoryStore,
|
||||
selected_step: Arc<Mutex<Option<(String, SessionHistoryStep)>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for RendererNavigationHistory {
|
||||
@@ -28,6 +36,73 @@ impl PartialEq for RendererNavigationHistory {
|
||||
impl Eq for RendererNavigationHistory {}
|
||||
|
||||
impl RendererNavigationHistory {
|
||||
pub(crate) fn nested_history(&self) -> NestedHistoryStore {
|
||||
self.nested.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn current_document_id(&self) -> Option<NavigationHistoryDocumentId> {
|
||||
Some(
|
||||
current_entry(self.snapshot()?.as_ref())?
|
||||
.document_id
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn publish_joint_history(&self, joint: Option<JointHistoryRestoration>) {
|
||||
*self.joint.lock() = joint;
|
||||
}
|
||||
|
||||
pub(crate) fn select_joint_traversal(&self, key: String, step: SessionHistoryStep) {
|
||||
*self.selected_step.lock() = Some((key, step));
|
||||
}
|
||||
|
||||
pub(crate) fn restore_for(&self, seed: &NavigationHistoryEntrySeed) -> Self {
|
||||
if seed
|
||||
.activation
|
||||
.as_ref()
|
||||
.is_some_and(|activation| activation.navigation_type.as_deref() == Some("push"))
|
||||
&& let Some(joint) = self.joint.lock().as_ref()
|
||||
{
|
||||
self.nested.prune(
|
||||
joint.current_step(),
|
||||
&seed
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| entry.document_id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
let current = current_entry(seed);
|
||||
let target_step = self.selected_step.lock().as_ref().and_then(|(key, step)| {
|
||||
current
|
||||
.filter(|entry| entry.key.as_str() == key)
|
||||
.map(|_| *step)
|
||||
});
|
||||
let joint = self.joint.lock().clone().and_then(|joint| {
|
||||
joint.for_navigation(
|
||||
joint_snapshot(seed),
|
||||
seed.activation
|
||||
.as_ref()
|
||||
.and_then(|activation| activation.navigation_type.as_deref()),
|
||||
target_step,
|
||||
)
|
||||
});
|
||||
Self {
|
||||
snapshot: Arc::new(Mutex::new(Some(Arc::new(seed.clone())))),
|
||||
joint: Arc::new(Mutex::new(joint)),
|
||||
nested: self.nested.clone(),
|
||||
selected_step: Arc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn install_joint_history(
|
||||
&self,
|
||||
histories: &mut crate::native_bridge::joint_history::JointSessionHistories,
|
||||
) {
|
||||
if let Some(joint) = self.joint.lock().clone() {
|
||||
histories.restore_root(OwnerDispatchScope::Top, joint);
|
||||
}
|
||||
}
|
||||
pub(crate) fn publish(&self, seed: NavigationHistoryEntrySeed) {
|
||||
*self.snapshot.lock() = Some(Arc::new(seed));
|
||||
}
|
||||
@@ -95,6 +170,7 @@ impl RendererNavigationHistory {
|
||||
requested: Arc::new(seed),
|
||||
source_index,
|
||||
initial_empty_source: false,
|
||||
selected_step: self.selected_step.lock().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +185,7 @@ pub struct RendererNavigationHistoryRequest {
|
||||
requested: Arc<NavigationHistoryEntrySeed>,
|
||||
source_index: Option<u32>,
|
||||
initial_empty_source: bool,
|
||||
selected_step: Option<(String, SessionHistoryStep)>,
|
||||
}
|
||||
|
||||
impl PartialEq for RendererNavigationHistoryRequest {
|
||||
@@ -122,6 +199,11 @@ impl PartialEq for RendererNavigationHistoryRequest {
|
||||
impl Eq for RendererNavigationHistoryRequest {}
|
||||
|
||||
impl RendererNavigationHistoryRequest {
|
||||
pub(crate) fn source_history(&self) -> RendererNavigationHistory {
|
||||
let mut source = self.source.clone();
|
||||
source.selected_step = Arc::new(Mutex::new(self.selected_step.clone()));
|
||||
source
|
||||
}
|
||||
/// The browser owns the initial-empty Document lifecycle. Its first
|
||||
/// navigation has no activation source. Same-document URL updates do not
|
||||
/// change that lifecycle state.
|
||||
|
||||
@@ -6915,6 +6915,9 @@ impl RendererOwnerHandle {
|
||||
.as_ref()
|
||||
.map(|request| request.resolve(&final_url))
|
||||
.transpose()?,
|
||||
navigation_history_source: navigation_history
|
||||
.as_ref()
|
||||
.map(|request| request.source_history()),
|
||||
reserved_service_worker_client_id: reserved_service_worker_client
|
||||
.map(RendererReservedServiceWorkerClient::release),
|
||||
};
|
||||
@@ -7215,6 +7218,7 @@ impl RendererOwnerHandle {
|
||||
main_document_commit,
|
||||
top_level_storage_key: None,
|
||||
navigation_bootstrap_entry: navigation_history.as_ref().map(|request| request.resolve(&final_url)).transpose()?,
|
||||
navigation_history_source: navigation_history.as_ref().map(|request| request.source_history()),
|
||||
reserved_service_worker_client_id: reserved_service_worker_client
|
||||
.map(RendererReservedServiceWorkerClient::release),
|
||||
};
|
||||
|
||||
@@ -1211,6 +1211,7 @@ impl PageVm {
|
||||
root_frame_id: self.vm().root_frame_id().map(str::to_owned),
|
||||
top_level_storage_key: None,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: Some(self.vm().top_level_navigation_history()),
|
||||
reserved_service_worker_client_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1099,6 +1099,7 @@ pub(crate) struct PageVmEnvConfig {
|
||||
pub(crate) layout_policy: LayoutPolicy,
|
||||
pub(crate) wpt_extensions_enabled: bool,
|
||||
pub(crate) navigation_bootstrap_entry: Option<crate::native_bridge::NavigationHistoryEntrySeed>,
|
||||
pub(crate) navigation_history_source: Option<crate::runtime::RendererNavigationHistory>,
|
||||
pub(crate) reserved_service_worker_client_id:
|
||||
Option<crate::service_worker_runtime::ServiceWorkerClientId>,
|
||||
}
|
||||
@@ -4466,9 +4467,10 @@ impl PageVm {
|
||||
env.fetch_subresource_interception_enabled,
|
||||
env.fetch_subresource_interception_resource_type,
|
||||
);
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.install_navigation_bootstrap_entry(env.navigation_bootstrap_entry.clone());
|
||||
page_vm.vm_mut().install_navigation_bootstrap_from_history(
|
||||
env.navigation_bootstrap_entry.clone(),
|
||||
env.navigation_history_source.clone(),
|
||||
);
|
||||
Ok(page_vm)
|
||||
}
|
||||
|
||||
|
||||
@@ -699,6 +699,7 @@ fn minimal_test_page_vm_env_config() -> PageVmEnvConfig {
|
||||
layout_policy: crate::real_layout_test_policy(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1828,6 +1828,7 @@ fn test_page_vm_with_loader_dom_host_hooks_and_response_referrer_policy(
|
||||
layout_policy: crate::real_layout_test_policy(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
runtime_hooks,
|
||||
@@ -3231,6 +3232,7 @@ fn default_runtime_hooks_reject_direct_no_owner_page_vm_construction() {
|
||||
layout_policy: crate::real_layout_test_policy(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::default(),
|
||||
|
||||
@@ -2582,6 +2582,7 @@ document.body.setAttribute('data-error-state', [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
}
|
||||
}
|
||||
@@ -6510,6 +6511,7 @@ globalThis.__outerDocumentWriteScriptContinued = true;
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -15082,6 +15084,7 @@ document.body.setAttribute('data-result', [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -15262,6 +15265,7 @@ document.body.setAttribute('data-result', [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -15491,6 +15495,7 @@ document.body.setAttribute('data-result', [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -17490,6 +17495,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -17618,6 +17624,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -17756,6 +17763,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -17929,6 +17937,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -18252,6 +18261,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -18422,6 +18432,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
@@ -18524,6 +18535,7 @@ document.body.setAttribute("data-range", [
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
},
|
||||
PageVmRuntimeHooks::standalone_without_owner_reservation_for_test(),
|
||||
|
||||
@@ -960,6 +960,7 @@ mod tests {
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
navigation_history_source: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6004,14 +6004,27 @@ impl ScriptVm {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn install_navigation_bootstrap_entry(
|
||||
&mut self,
|
||||
entry_seed: Option<super::native_bridge::NavigationHistoryEntrySeed>,
|
||||
) {
|
||||
self.install_navigation_bootstrap_from_history(entry_seed, None);
|
||||
}
|
||||
|
||||
pub(super) fn install_navigation_bootstrap_from_history(
|
||||
&mut self,
|
||||
entry_seed: Option<super::native_bridge::NavigationHistoryEntrySeed>,
|
||||
source: Option<crate::runtime::RendererNavigationHistory>,
|
||||
) {
|
||||
let Some(entry_seed) = entry_seed else {
|
||||
return;
|
||||
};
|
||||
let _ = self.with_default_context_scope(|scope, _runtime_ptr| {
|
||||
let _ = self.with_default_context_scope(|scope, runtime_ptr| {
|
||||
if let Some(source) = &source {
|
||||
unsafe { &mut *runtime_ptr }
|
||||
.restore_top_level_navigation_history(source, &entry_seed);
|
||||
}
|
||||
super::context_bootstrap::install_navigation_bootstrap_entry(scope, &entry_seed);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user