mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 08:00:49 +00:00
feat(popup): adopt stable renderer-owned auxiliary pages
Create auxiliary contexts as real Pages with synchronous initial realms, stable WindowProxy handoff and one navigation owner. Preserve the Page residence across redirects, no-content responses and transport error documents.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,6 @@ use std::sync::Arc;
|
||||
|
||||
use crate::renderer::{
|
||||
RendererPageCommand, RendererPageCommandPending, RendererPageHandle, RendererPageReply,
|
||||
RendererPageState,
|
||||
};
|
||||
use anyhow::Result;
|
||||
pub use command_dispatch::{
|
||||
@@ -88,7 +87,8 @@ pub use moli_renderer_v8::{
|
||||
RendererCaptureScreencastFrameRequest, RendererCaptureScreenshotReply,
|
||||
RendererCaptureScreenshotRequest, RendererCommandTurnCompletion, RendererCommandTurnOutput,
|
||||
RendererDedicatedWorkerTargetEvent, RendererDedicatedWorkerTargetInfo,
|
||||
RendererDevToolsAgentToken, RendererDocumentHitTestResult,
|
||||
RendererDevToolsAgentToken, RendererDocumentContinuationCompletion,
|
||||
RendererDocumentContinuationObserver, RendererDocumentHitTestResult,
|
||||
RendererDocumentIsolateAccountingDiagnostics, RendererDocumentLifecycleEvent,
|
||||
RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity,
|
||||
RendererDocumentLifecycleMilestone, RendererDocumentLifecycleSnapshot,
|
||||
@@ -108,7 +108,9 @@ pub use moli_renderer_v8::{
|
||||
RendererMainDocumentCommit, RendererPageCommandPostResponseContinuation,
|
||||
RendererPageCreationArtifacts, RendererPageCreationDiagnostics,
|
||||
RendererPageDiagnosticsSnapshot, RendererPageDumpFormat, RendererPageDumpOptions,
|
||||
RendererPageDumpStripOptions, RendererPendingDownloadActivation,
|
||||
RendererPageDumpStripOptions, RendererPageReplacementCommitError,
|
||||
RendererPageReplacementCommitFailureDisposition, RendererPageReplacementReservationPending,
|
||||
RendererPageState, RendererPendingAuxiliaryPage, RendererPendingDownloadActivation,
|
||||
RendererPendingDownloadResponse, RendererPendingFileChooserActivation,
|
||||
RendererPendingJavaScriptDialog, RendererPendingPopupActivation,
|
||||
RendererPendingSameDocumentNavigation, RendererPendingTopLevelHistoryTraversal,
|
||||
@@ -207,6 +209,14 @@ impl fmt::Debug for Page {
|
||||
}
|
||||
|
||||
impl Page {
|
||||
#[doc(hidden)]
|
||||
pub fn apply_renderer_document_continuation_state(
|
||||
&mut self,
|
||||
page_state: Arc<RendererPageState>,
|
||||
) {
|
||||
self.replace_page_state(page_state);
|
||||
}
|
||||
|
||||
pub(crate) fn from_attached_handle(
|
||||
handle: RendererPageHandle,
|
||||
page_state: Arc<RendererPageState>,
|
||||
@@ -234,6 +244,27 @@ impl Page {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn adopt_renderer_page_replacement(
|
||||
&mut self,
|
||||
replacement: moli_renderer_v8::RendererPageReplacementCommit,
|
||||
) -> Result<(
|
||||
RendererPageCreationDiagnostics,
|
||||
RendererPageCreationArtifacts,
|
||||
Option<RendererPendingDownloadActivation>,
|
||||
)> {
|
||||
self.handle.adopt_page_replacement(&replacement)?;
|
||||
let (
|
||||
_renderer_devtools_agent_token,
|
||||
page_state,
|
||||
creation_diagnostics,
|
||||
creation_artifacts,
|
||||
pending_download,
|
||||
) = replacement.into_parts();
|
||||
self.page_state.replace(page_state);
|
||||
self.page_creation_artifacts = None;
|
||||
Ok((creation_diagnostics, creation_artifacts, pending_download))
|
||||
}
|
||||
|
||||
/// Takes the renderer lifecycle facts captured while this page was created.
|
||||
///
|
||||
/// Direct-fetch callers can retain these facts until the page is handed to
|
||||
@@ -264,6 +295,22 @@ impl Page {
|
||||
self.renderer_devtools_command_session_id = session_id;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub async fn reserve_renderer_document_replacement(
|
||||
&self,
|
||||
) -> Result<moli_renderer_v8::RendererPageReservationToken> {
|
||||
self.handle
|
||||
.reserve_replacement_document_for_navigation()
|
||||
.await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn start_renderer_document_replacement_reservation(
|
||||
&self,
|
||||
) -> RendererPageReplacementReservationPending {
|
||||
self.handle.start_replacement_document_reservation()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn renderer_agent_attachment_id(&self) -> Option<RendererAgentAttachmentId> {
|
||||
self.renderer_agent_attachment_id
|
||||
|
||||
@@ -44,10 +44,10 @@ pub use moli_renderer_v8::{
|
||||
RendererServiceWorkerMainResourceFetch, RendererSharedWorkerRuntimeDiagnostics,
|
||||
};
|
||||
pub use navigation_engine::{
|
||||
BuiltDocumentPage, CommittedDocumentResourceSource, NavigationEngine,
|
||||
NavigationPageStorageHandles, NavigationResourceStorageHandles, NavigationRuntimeConfig,
|
||||
PendingBuiltDocumentPage, PreparedDocumentPage, PreparedDocumentPageCommitConfiguration,
|
||||
PreparedDocumentPageCommitPermit,
|
||||
BuiltDocumentPage, CommittedDocumentPageReplacement, CommittedDocumentResourceSource,
|
||||
NavigationEngine, NavigationPageStorageHandles, NavigationResourceStorageHandles,
|
||||
NavigationRuntimeConfig, PendingBuiltDocumentPage, PreparedDocumentPage,
|
||||
PreparedDocumentPageCommitConfiguration, PreparedDocumentPageCommitPermit,
|
||||
};
|
||||
|
||||
static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
|
||||
@@ -1102,6 +1102,7 @@ impl Browser {
|
||||
source,
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1230,6 +1231,7 @@ impl Browser {
|
||||
source,
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -188,6 +188,12 @@ pub struct BuiltDocumentPage {
|
||||
pub pending_download: Option<RendererPendingDownloadActivation>,
|
||||
}
|
||||
|
||||
pub struct CommittedDocumentPageReplacement {
|
||||
pub page_creation_diagnostics: RendererPageCreationDiagnostics,
|
||||
pub page_creation_artifacts: RendererPageCreationArtifacts,
|
||||
pub pending_download: Option<RendererPendingDownloadActivation>,
|
||||
}
|
||||
|
||||
/// A renderer document whose isolate and DevTools agent are reserved, but
|
||||
/// whose parser and execution contexts have not started.
|
||||
pub struct PreparedDocumentPage {
|
||||
@@ -310,6 +316,41 @@ impl PreparedDocumentPage {
|
||||
})
|
||||
}
|
||||
|
||||
/// Commits the prepared Document into `page` without replacing its owning
|
||||
/// handle. The Page identity is checked before the renderer crosses the
|
||||
/// old-Document teardown boundary.
|
||||
pub async fn commit_page_replacement(
|
||||
self,
|
||||
permit: PreparedDocumentPageCommitPermit,
|
||||
page: &mut Page,
|
||||
) -> std::result::Result<
|
||||
CommittedDocumentPageReplacement,
|
||||
moli_renderer_v8::RendererPageReplacementCommitError,
|
||||
> {
|
||||
if self.renderer_owner_local_host_id() != page.renderer_owner_local_host_id()
|
||||
|| self.renderer_page_id() != page.renderer_page_id()
|
||||
{
|
||||
return Err(
|
||||
moli_renderer_v8::RendererPageReplacementCommitError::page_preserved(anyhow!(
|
||||
"prepared Document belongs to a different live Page"
|
||||
)),
|
||||
);
|
||||
}
|
||||
let replacement = self.prepared.commit_page_replacement(permit.permit).await?;
|
||||
let (page_creation_diagnostics, page_creation_artifacts, pending_download) = page
|
||||
.adopt_renderer_page_replacement(replacement)
|
||||
.map_err(|error| {
|
||||
moli_renderer_v8::RendererPageReplacementCommitError::page_retired(
|
||||
error.context("failed to adopt committed live Page replacement"),
|
||||
)
|
||||
})?;
|
||||
Ok(CommittedDocumentPageReplacement {
|
||||
page_creation_diagnostics,
|
||||
page_creation_artifacts,
|
||||
pending_download,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn cancel(self) -> Result<()> {
|
||||
self.prepared.cancel().await
|
||||
}
|
||||
@@ -458,6 +499,13 @@ impl NavigationEngine {
|
||||
self.js_runtime.reserve_page_for_creation()
|
||||
}
|
||||
|
||||
pub fn owns_page_reservation(
|
||||
&self,
|
||||
reservation: moli_renderer_v8::RendererPageReservationToken,
|
||||
) -> bool {
|
||||
self.js_runtime.owns_page_reservation(reservation)
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_runtime_config(NavigationRuntimeConfig::default())
|
||||
}
|
||||
@@ -663,6 +711,10 @@ impl NavigationEngine {
|
||||
self.js_runtime.renderer_owner_id_for_diagnostics()
|
||||
}
|
||||
|
||||
pub fn renderer_owner_local_host_id(&self) -> moli_renderer_v8::RendererOwnerLocalHostId {
|
||||
self.js_runtime.renderer_owner_local_host_id()
|
||||
}
|
||||
|
||||
pub fn shares_renderer_owner_with(&self, other: &Self) -> bool {
|
||||
self.js_runtime
|
||||
.shares_renderer_owner_with(&other.js_runtime)
|
||||
@@ -2435,7 +2487,10 @@ impl NavigationEngine {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CommittedDocumentResourceSource, NavigationEngine, NavigationRuntimeConfig};
|
||||
use super::{
|
||||
CommittedDocumentResourceSource, NavigationEngine, NavigationRuntimeConfig,
|
||||
PageVmInitStage, PreparedDocumentPage,
|
||||
};
|
||||
use crate::{
|
||||
LayoutPolicy, OptionalResourceFetchMask,
|
||||
network::ResourceRequestClient,
|
||||
@@ -2443,6 +2498,7 @@ mod tests {
|
||||
};
|
||||
use moli_cookie_jar::new_shared_browser_cookie_store;
|
||||
use moli_fetch::FetchConfig;
|
||||
use tokio::sync::oneshot;
|
||||
use url::Url;
|
||||
|
||||
static_assertions::assert_not_impl_any!(Browser: Send, Sync);
|
||||
@@ -2737,4 +2793,143 @@ mod tests {
|
||||
.contains("does not belong to the NavigationEngine browser resource runtime")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn core_page_adopts_prepared_renderer_replacement_without_replacing_ownership() {
|
||||
let mut engine = NavigationEngine::new();
|
||||
let initial_url =
|
||||
Url::parse("https://example.test/core-replacement/initial").expect("initial URL");
|
||||
let built = engine
|
||||
.build_inline_html_document_page_best_effort_with_inspector_session_restores_async(
|
||||
new_shared_browser_cookie_store(),
|
||||
moli_renderer_v8::RendererWebStorageHandles::ephemeral(),
|
||||
None,
|
||||
None,
|
||||
initial_url,
|
||||
None,
|
||||
"<!doctype html><body>initial core Page</body>".to_owned(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
1.0,
|
||||
Default::default(),
|
||||
None,
|
||||
false,
|
||||
Vec::new(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("initial core Page should build");
|
||||
let mut page = built.page;
|
||||
let page_id = page.renderer_page_id();
|
||||
let owner_local_host_id = page.renderer_owner_local_host_id();
|
||||
let initial_agent = page.renderer_devtools_agent_token();
|
||||
let reservation = page
|
||||
.reserve_renderer_document_replacement()
|
||||
.await
|
||||
.expect("core Page should reserve its live renderer generation");
|
||||
let replacement_url =
|
||||
Url::parse("https://example.test/core-replacement/next").expect("replacement URL");
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
let (body_tx, raw_body) =
|
||||
moli_renderer_v8::ExternalRawDocumentBodyStream::channel(completion_rx);
|
||||
body_tx
|
||||
.send(
|
||||
b"<!doctype html><body><main id='core-replacement'>replacement core Page</main></body>"
|
||||
.to_vec(),
|
||||
)
|
||||
.await
|
||||
.expect("replacement body should send");
|
||||
drop(body_tx);
|
||||
completion_tx
|
||||
.send(Ok(()))
|
||||
.expect("replacement body completion should send");
|
||||
let loader = engine
|
||||
.resource_request_client()
|
||||
.expect("engine loader should exist");
|
||||
let prepared = engine
|
||||
.js_runtime
|
||||
.prepare_streaming_raw_document_from_external_body_with_inspector_session_restores(
|
||||
reservation,
|
||||
replacement_url.clone(),
|
||||
replacement_url.clone(),
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
200,
|
||||
vec![("content-type".to_owned(), "text/html".to_owned())],
|
||||
&loader,
|
||||
moli_renderer_v8::RendererWebStorageHandles::ephemeral(),
|
||||
raw_body,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
1.0,
|
||||
Default::default(),
|
||||
None,
|
||||
false,
|
||||
Vec::new(),
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
PageVmInitStage::Load,
|
||||
moli_renderer_v8::RendererReplyBoundary::Stage,
|
||||
moli_renderer_v8::RendererTopLevelNavigationDispatch::DelegateToBrowser,
|
||||
moli_renderer_v8::RendererNavigationReplyPolicy::ReturnWithPendingNavigation,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("core replacement should prepare");
|
||||
let replacement_agent = prepared.renderer_devtools_agent_token();
|
||||
assert_ne!(replacement_agent, initial_agent);
|
||||
let prepared = PreparedDocumentPage { prepared };
|
||||
let permit = prepared.issue_commit_permit();
|
||||
let committed = prepared
|
||||
.commit_page_replacement(permit, &mut page)
|
||||
.await
|
||||
.expect("core Page should adopt its renderer replacement");
|
||||
|
||||
assert_eq!(page.renderer_page_id(), page_id);
|
||||
assert_eq!(page.renderer_owner_local_host_id(), owner_local_host_id);
|
||||
assert_eq!(page.renderer_devtools_agent_token(), replacement_agent);
|
||||
assert_eq!(page.final_url(), &replacement_url);
|
||||
assert!(committed.pending_download.is_none());
|
||||
assert!(
|
||||
committed
|
||||
.page_creation_artifacts
|
||||
.lifecycle_snapshot
|
||||
.load
|
||||
.is_some()
|
||||
);
|
||||
let value = page
|
||||
.evaluate_runtime_expression_async(
|
||||
"document.querySelector('#core-replacement').textContent",
|
||||
)
|
||||
.await
|
||||
.expect("replacement core Page should evaluate through its original owner");
|
||||
assert_eq!(
|
||||
value.get("value"),
|
||||
Some(&serde_json::json!("replacement core Page"))
|
||||
);
|
||||
page.close_async()
|
||||
.await
|
||||
.expect("replacement core Page should close through its original owner");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,6 +1033,9 @@ pub struct DocumentStartScript {
|
||||
pub source: String,
|
||||
pub world_name: Option<String>,
|
||||
pub has_bidi_channel_argument: bool,
|
||||
/// Browser-owned realm bootstrap that must not appear as author script
|
||||
/// execution to Inspector instrumentation breakpoints.
|
||||
pub browser_internal: bool,
|
||||
pub bidi_channel_handoffs: Vec<BidiPreloadChannelHandoff>,
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@ pub struct CdpRendererCommandPolicy {
|
||||
renderer_replacement: CdpRendererCommandReplacement,
|
||||
renderer_replay_dispatch: CdpRendererCommandReplayDispatch,
|
||||
executes_page_javascript: bool,
|
||||
waits_for_document_continuation: bool,
|
||||
}
|
||||
|
||||
impl CdpRendererCommandPolicy {
|
||||
@@ -254,6 +255,10 @@ impl CdpRendererCommandPolicy {
|
||||
pub const fn executes_page_javascript(self) -> bool {
|
||||
self.executes_page_javascript
|
||||
}
|
||||
|
||||
pub const fn waits_for_document_continuation(self) -> bool {
|
||||
self.waits_for_document_continuation
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed inbound CDP command plus its downstream JSON representation.
|
||||
@@ -398,6 +403,18 @@ impl ParsedCdpCommand {
|
||||
pub fn renderer_replay_dispatch(&self) -> CdpRendererCommandReplayDispatch {
|
||||
self.renderer_policy.replay_dispatch()
|
||||
}
|
||||
|
||||
/// Whether this command reads protocol-owned state that is finalized by
|
||||
/// the committed Document's typed continuation.
|
||||
///
|
||||
/// A committed renderer attachment is usable while its parser is still
|
||||
/// blocked: Runtime/DOM commands must be able to observe that `loading`
|
||||
/// Document. Keep this narrower than the main-thread renderer access
|
||||
/// classification, which protects commands from binding to the attachment
|
||||
/// being replaced.
|
||||
pub fn waits_for_document_continuation_to_finish(&self) -> bool {
|
||||
self.renderer_policy.waits_for_document_continuation()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -472,6 +489,7 @@ impl RuntimeWireAction {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum PageWireAction {
|
||||
MainThread,
|
||||
NavigationHistory,
|
||||
Crash,
|
||||
Other,
|
||||
}
|
||||
@@ -487,6 +505,7 @@ impl PageWireAction {
|
||||
| "searchInResource"
|
||||
| "getLayoutMetrics"
|
||||
| "printToPDF" => Self::MainThread,
|
||||
"getNavigationHistory" => Self::NavigationHistory,
|
||||
"crash" => Self::Crash,
|
||||
_ => Self::Other,
|
||||
}
|
||||
@@ -573,6 +592,7 @@ impl CdpRendererCommandPolicy {
|
||||
renderer_replacement: CdpRendererCommandReplacement::Replay,
|
||||
renderer_replay_dispatch: CdpRendererCommandReplayDispatch::Direct,
|
||||
executes_page_javascript: false,
|
||||
waits_for_document_continuation: false,
|
||||
};
|
||||
};
|
||||
let domain = CdpMethodDomain::parse(domain);
|
||||
@@ -612,7 +632,9 @@ impl CdpRendererCommandPolicy {
|
||||
}
|
||||
}
|
||||
CdpMethodDomain::Page => match PageWireAction::parse(action) {
|
||||
PageWireAction::MainThread => CdpRendererCommandAccess::MainThread,
|
||||
PageWireAction::MainThread | PageWireAction::NavigationHistory => {
|
||||
CdpRendererCommandAccess::MainThread
|
||||
}
|
||||
PageWireAction::Crash => CdpRendererCommandAccess::Io,
|
||||
PageWireAction::Other => CdpRendererCommandAccess::OwnerIndependent,
|
||||
},
|
||||
@@ -650,6 +672,8 @@ impl CdpRendererCommandPolicy {
|
||||
executes_page_javascript: runtime_action
|
||||
.is_some_and(RuntimeWireAction::executes_page_javascript)
|
||||
|| debugger_action.is_some_and(DebuggerWireAction::executes_page_javascript),
|
||||
waits_for_document_continuation: domain == CdpMethodDomain::Page
|
||||
&& PageWireAction::parse(action) == PageWireAction::NavigationHistory,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,6 +700,7 @@ mod tests {
|
||||
command.renderer_access(),
|
||||
CdpRendererCommandAccess::MainThread
|
||||
);
|
||||
assert!(!command.waits_for_document_continuation_to_finish());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -690,6 +715,7 @@ mod tests {
|
||||
command.renderer_access(),
|
||||
CdpRendererCommandAccess::OwnerIndependent
|
||||
);
|
||||
assert!(!command.waits_for_document_continuation_to_finish());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -855,6 +881,7 @@ mod tests {
|
||||
command.renderer_access(),
|
||||
CdpRendererCommandAccess::MainThread
|
||||
);
|
||||
assert!(!command.waits_for_document_continuation_to_finish());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1012,4 +1039,16 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_history_waits_for_committed_document_continuation() {
|
||||
let command =
|
||||
parse(r#"{"id":12,"method":"Page.getNavigationHistory","params":{},"sessionId":"s1"}"#);
|
||||
|
||||
assert_eq!(
|
||||
command.renderer_access(),
|
||||
CdpRendererCommandAccess::MainThread
|
||||
);
|
||||
assert!(command.waits_for_document_continuation_to_finish());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
sync::{Arc, atomic::AtomicU64},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -11,7 +11,8 @@ use moli_core::{
|
||||
runtime::NavigationRuntimeConfig,
|
||||
};
|
||||
use moli_protocol::{
|
||||
BackgroundNavigationCompletion, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection,
|
||||
BackgroundDocumentContinuationCompletion, BackgroundNavigationCompletion,
|
||||
BackgroundNavigationGateKey, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection,
|
||||
CdpInitialStoragePartition, CdpRendererCommandAccess, CdpSchedulerEvent,
|
||||
CdpTargetHostLifecycleObserver, CommandDispatchContext, CompletedCdpCommandDispatch,
|
||||
CompletedDeferredMainDocumentLoadCompletion, DeferredMainDocumentLoadCompletionOutputAction,
|
||||
@@ -112,6 +113,7 @@ pub(crate) enum CommandStartAction {
|
||||
|
||||
pub(crate) struct CdpScheduler {
|
||||
conn: CdpConnection,
|
||||
document_continuation_gate: DocumentContinuationGate,
|
||||
pending_navigation_background_events: VecDeque<PendingNavigationBackgroundEvent>,
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers,
|
||||
queues: SchedulerQueues,
|
||||
@@ -166,6 +168,32 @@ fn append_unique_target_ids(target_ids: &mut Vec<String>, additional: Vec<String
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct DocumentContinuationGate {
|
||||
pending: HashSet<BackgroundNavigationGateKey>,
|
||||
}
|
||||
|
||||
impl DocumentContinuationGate {
|
||||
fn note_navigation_started(&mut self, key: BackgroundNavigationGateKey) {
|
||||
self.pending.insert(key);
|
||||
}
|
||||
|
||||
fn note_navigation_completion_drained(&mut self, key: &BackgroundNavigationGateKey) {
|
||||
if !self.pending.remove(key) {
|
||||
tracing::debug!(
|
||||
?key,
|
||||
"document continuation completion did not match any pending gate"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn has_inflight_navigation_for_session_owner(&self, session_id: Option<&str>) -> bool {
|
||||
self.pending
|
||||
.iter()
|
||||
.any(|key| key.matches_session_owner(session_id))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ForegroundNavigationNetworkBarrier {
|
||||
wait_for_document_response_started: bool,
|
||||
@@ -220,10 +248,13 @@ impl ForegroundNavigationNetworkBarrier {
|
||||
pub(crate) type CdpBackgroundEventReceiver = mpsc::UnboundedReceiver<BackgroundProtocolEvent>;
|
||||
pub(crate) type CdpBackgroundNavigationCompletionReceiver =
|
||||
mpsc::UnboundedReceiver<BackgroundNavigationCompletion>;
|
||||
pub(crate) type CdpDocumentContinuationCompletionReceiver =
|
||||
mpsc::UnboundedReceiver<BackgroundDocumentContinuationCompletion>;
|
||||
pub(crate) type CdpRendererPublicationReceiver = moli_core::RendererOutputTransportReceiver;
|
||||
pub(crate) struct CdpSchedulerEventReceivers {
|
||||
pub(crate) background_event_rx: CdpBackgroundEventReceiver,
|
||||
pub(crate) background_navigation_completion_rx: CdpBackgroundNavigationCompletionReceiver,
|
||||
pub(crate) document_continuation_completion_rx: CdpDocumentContinuationCompletionReceiver,
|
||||
pub(crate) renderer_publication_rx: CdpRendererPublicationReceiver,
|
||||
}
|
||||
|
||||
@@ -244,9 +275,42 @@ pub(crate) enum CdpSchedulerInterleavedInput {
|
||||
}
|
||||
|
||||
impl CdpSchedulerEventReceivers {
|
||||
pub(crate) async fn recv_navigation_completion(
|
||||
&mut self,
|
||||
) -> Option<BackgroundNavigationCompletion> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
maybe_continuation = self.document_continuation_completion_rx.recv() => {
|
||||
maybe_continuation.map(
|
||||
BackgroundNavigationCompletion::document_continuation,
|
||||
)
|
||||
}
|
||||
maybe_completion = self.background_navigation_completion_rx.recv() => {
|
||||
maybe_completion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv_navigation_completion(
|
||||
&mut self,
|
||||
) -> Option<BackgroundNavigationCompletion> {
|
||||
self.document_continuation_completion_rx
|
||||
.try_recv()
|
||||
.ok()
|
||||
.map(BackgroundNavigationCompletion::document_continuation)
|
||||
.or_else(|| self.background_navigation_completion_rx.try_recv().ok())
|
||||
}
|
||||
|
||||
pub(crate) async fn recv_interleaved_input(&mut self) -> Option<CdpSchedulerInterleavedInput> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
maybe_continuation = self.document_continuation_completion_rx.recv() => {
|
||||
maybe_continuation.map(|completion| {
|
||||
CdpSchedulerInterleavedInput::BackgroundNavigationCompletion(
|
||||
BackgroundNavigationCompletion::document_continuation(completion),
|
||||
)
|
||||
})
|
||||
}
|
||||
maybe_completion = self.background_navigation_completion_rx.recv() => {
|
||||
maybe_completion.map(
|
||||
CdpSchedulerInterleavedInput::BackgroundNavigationCompletion,
|
||||
@@ -692,6 +756,7 @@ impl CdpScheduler {
|
||||
fn new(conn: CdpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
document_continuation_gate: DocumentContinuationGate::default(),
|
||||
pending_navigation_background_events: VecDeque::new(),
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(),
|
||||
queues: SchedulerQueues::default(),
|
||||
@@ -735,6 +800,11 @@ impl CdpScheduler {
|
||||
scheduler
|
||||
.conn
|
||||
.set_background_navigation_completion_sender(background_navigation_completion_tx);
|
||||
let (document_continuation_completion_tx, document_continuation_completion_rx) =
|
||||
mpsc::unbounded_channel();
|
||||
scheduler
|
||||
.conn
|
||||
.set_document_continuation_completion_sender(document_continuation_completion_tx);
|
||||
let (renderer_publication_tx, renderer_publication_rx) =
|
||||
moli_core::renderer_output_transport_channel();
|
||||
scheduler
|
||||
@@ -745,6 +815,7 @@ impl CdpScheduler {
|
||||
CdpSchedulerEventReceivers {
|
||||
background_event_rx,
|
||||
background_navigation_completion_rx,
|
||||
document_continuation_completion_rx,
|
||||
renderer_publication_rx,
|
||||
},
|
||||
)
|
||||
@@ -1968,10 +2039,14 @@ impl CdpScheduler {
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_navigation_flush(&self, command: &ParsedCdpCommand) -> bool {
|
||||
command.renderer_access() == CdpRendererCommandAccess::MainThread
|
||||
(command.renderer_access() == CdpRendererCommandAccess::MainThread
|
||||
&& self
|
||||
.conn
|
||||
.renderer_document_navigation_is_suspended_for_session_owner(command.session_id())
|
||||
.renderer_document_navigation_is_suspended_for_session_owner(command.session_id()))
|
||||
|| (command.waits_for_document_continuation_to_finish()
|
||||
&& self
|
||||
.document_continuation_gate
|
||||
.has_inflight_navigation_for_session_owner(command.session_id()))
|
||||
}
|
||||
|
||||
pub(crate) fn route_background_event_around_inflight_navigation(
|
||||
@@ -2102,6 +2177,9 @@ impl CdpScheduler {
|
||||
);
|
||||
}
|
||||
match event {
|
||||
CdpSchedulerEvent::DocumentContinuationStarted { key } => {
|
||||
self.document_continuation_gate.note_navigation_started(key);
|
||||
}
|
||||
CdpSchedulerEvent::ProtocolWorkPublished { work } => {
|
||||
if moli_trace::cdp_nav_timing_enabled() {
|
||||
tracing::info!(
|
||||
@@ -2669,6 +2747,10 @@ impl CdpScheduler {
|
||||
Option<moli_core::RendererOutputFence>,
|
||||
) {
|
||||
let trace_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
|
||||
if let Some(key) = completion.document_continuation_gate_key() {
|
||||
self.document_continuation_gate
|
||||
.note_navigation_completion_drained(&key);
|
||||
}
|
||||
let outcome = self
|
||||
.conn
|
||||
.drain_background_navigation_completion_turn_async(completion)
|
||||
@@ -2711,9 +2793,10 @@ impl CdpScheduler {
|
||||
self.drain_background_navigation_completion(completion)
|
||||
.await;
|
||||
assert!(
|
||||
renderer_output_predecessor.is_none(),
|
||||
"navigation completion must use its exact insertion boundary, not a command predecessor"
|
||||
renderer_output_boundary.is_none() || renderer_output_predecessor.is_none(),
|
||||
"navigation completion cannot carry both an insertion boundary and a continuation predecessor"
|
||||
);
|
||||
let renderer_output_fence = renderer_output_boundary.or(renderer_output_predecessor);
|
||||
prefix.append(completion_prefix);
|
||||
// The completion can still carry a renderer insertion boundary. While
|
||||
// that boundary is projected, later publications may contain the
|
||||
@@ -2722,7 +2805,7 @@ impl CdpScheduler {
|
||||
// prefix so those later publications cannot overtake it.
|
||||
self.append_navigation_gate_release_before_renderer_boundary(&mut prefix);
|
||||
suffix.append(self.drain_background_events_around_inflight_navigation(background_event_rx));
|
||||
(prefix, suffix, renderer_output_boundary)
|
||||
(prefix, suffix, renderer_output_fence)
|
||||
}
|
||||
|
||||
/// Completes one navigation owner turn together with the exact concrete
|
||||
|
||||
@@ -23,11 +23,11 @@ use crate::{
|
||||
use super::frontend_control::CdpFrontendControlState;
|
||||
use super::{
|
||||
CdpBackgroundEventReceiver, CdpBackgroundNavigationCompletionReceiver, CdpCookieSnapshot,
|
||||
CdpOwnerActorLifecycle, CdpRendererPublicationReceiver, CdpScheduler,
|
||||
CdpSchedulerEventReceivers, CommandDispatchState, CommandDispatchStepOutput,
|
||||
CommandOutputReleasePermit, CommandStartAction, CommandTaskStep, CommandTurnOutput,
|
||||
ProtocolAdapterScheduler, ProtocolAdapterSchedulerAdvance, ProtocolAdapterSchedulerInput,
|
||||
ProtocolOutputSequence,
|
||||
CdpDocumentContinuationCompletionReceiver, CdpOwnerActorLifecycle,
|
||||
CdpRendererPublicationReceiver, CdpScheduler, CdpSchedulerEventReceivers, CommandDispatchState,
|
||||
CommandDispatchStepOutput, CommandOutputReleasePermit, CommandStartAction, CommandTaskStep,
|
||||
CommandTurnOutput, ProtocolAdapterScheduler, ProtocolAdapterSchedulerAdvance,
|
||||
ProtocolAdapterSchedulerInput, ProtocolOutputSequence,
|
||||
};
|
||||
|
||||
struct PendingRuntimeDeferredReplyState {
|
||||
@@ -90,6 +90,7 @@ enum SchedulerInput {
|
||||
struct SchedulerInputReceivers {
|
||||
background_event_rx: CdpBackgroundEventReceiver,
|
||||
background_navigation_completion_rx: CdpBackgroundNavigationCompletionReceiver,
|
||||
document_continuation_completion_rx: CdpDocumentContinuationCompletionReceiver,
|
||||
renderer_publication_rx: CdpRendererPublicationReceiver,
|
||||
buffered_renderer_publications: VecDeque<RendererOutputTransportMessage>,
|
||||
ready_background_inputs_before_runtime_response: VecDeque<SchedulerInput>,
|
||||
@@ -100,6 +101,7 @@ impl SchedulerInputReceivers {
|
||||
Self {
|
||||
background_event_rx: receivers.background_event_rx,
|
||||
background_navigation_completion_rx: receivers.background_navigation_completion_rx,
|
||||
document_continuation_completion_rx: receivers.document_continuation_completion_rx,
|
||||
renderer_publication_rx: receivers.renderer_publication_rx,
|
||||
buffered_renderer_publications: VecDeque::new(),
|
||||
ready_background_inputs_before_runtime_response: VecDeque::new(),
|
||||
@@ -119,6 +121,12 @@ impl SchedulerInputReceivers {
|
||||
// A correlated response carries an exact concrete output cursor; the
|
||||
// cursor fence below consumes only concrete stream traffic until that
|
||||
// position is admitted.
|
||||
while let Ok(completion) = self.document_continuation_completion_rx.try_recv() {
|
||||
self.ready_background_inputs_before_runtime_response
|
||||
.push_back(SchedulerInput::BackgroundNavigationCompletion(
|
||||
BackgroundNavigationCompletion::document_continuation(completion),
|
||||
));
|
||||
}
|
||||
while let Ok(completion) = self.background_navigation_completion_rx.try_recv() {
|
||||
self.ready_background_inputs_before_runtime_response
|
||||
.push_back(SchedulerInput::BackgroundNavigationCompletion(completion));
|
||||
@@ -177,6 +185,13 @@ impl SchedulerInputReceivers {
|
||||
self.queue_ready_background_inputs_before_runtime_response(response);
|
||||
self.ready_background_inputs_before_runtime_response.pop_front()
|
||||
}
|
||||
maybe_continuation = self.document_continuation_completion_rx.recv() => {
|
||||
maybe_continuation.map(|completion| {
|
||||
SchedulerInput::BackgroundNavigationCompletion(
|
||||
BackgroundNavigationCompletion::document_continuation(completion),
|
||||
)
|
||||
})
|
||||
}
|
||||
maybe_completion = self.background_navigation_completion_rx.recv() => {
|
||||
maybe_completion.map(SchedulerInput::BackgroundNavigationCompletion)
|
||||
}
|
||||
@@ -193,6 +208,13 @@ impl SchedulerInputReceivers {
|
||||
} else {
|
||||
tokio::select! {
|
||||
biased;
|
||||
maybe_continuation = self.document_continuation_completion_rx.recv() => {
|
||||
maybe_continuation.map(|completion| {
|
||||
SchedulerInput::BackgroundNavigationCompletion(
|
||||
BackgroundNavigationCompletion::document_continuation(completion),
|
||||
)
|
||||
})
|
||||
}
|
||||
maybe_completion = self.background_navigation_completion_rx.recv() => {
|
||||
maybe_completion.map(SchedulerInput::BackgroundNavigationCompletion)
|
||||
}
|
||||
@@ -2093,11 +2115,14 @@ mod tests {
|
||||
let (background_event_tx, background_event_rx) = mpsc::unbounded_channel();
|
||||
let (_background_navigation_tx, background_navigation_completion_rx) =
|
||||
mpsc::unbounded_channel();
|
||||
let (_document_continuation_tx, document_continuation_completion_rx) =
|
||||
mpsc::unbounded_channel();
|
||||
let (_renderer_publication_tx, renderer_publication_rx) =
|
||||
moli_core::renderer_output_transport_channel();
|
||||
let mut receivers = SchedulerInputReceivers::new(CdpSchedulerEventReceivers {
|
||||
background_event_rx,
|
||||
background_navigation_completion_rx,
|
||||
document_continuation_completion_rx,
|
||||
renderer_publication_rx,
|
||||
});
|
||||
let (runtime_response_tx, mut runtime_response_rx) = mpsc::unbounded_channel();
|
||||
|
||||
@@ -993,11 +993,14 @@ async fn closed_renderer_transport_fails_an_unprojected_command_fence() {
|
||||
let (_background_event_tx, background_event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (_navigation_tx, background_navigation_completion_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
let (_document_continuation_tx, document_continuation_completion_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
let (renderer_tx, renderer_publication_rx) = moli_core::renderer_output_transport_channel();
|
||||
drop(renderer_tx);
|
||||
let mut receivers = super::CdpSchedulerEventReceivers {
|
||||
background_event_rx,
|
||||
background_navigation_completion_rx,
|
||||
document_continuation_completion_rx,
|
||||
renderer_publication_rx,
|
||||
};
|
||||
let stream =
|
||||
|
||||
@@ -1990,8 +1990,10 @@ async fn websocket_cdp_raw_client_runtime_evaluate_immediately_after_page_naviga
|
||||
// Send Page.navigate then Runtime.evaluate back-to-back without
|
||||
// waiting for any lifecycle event. This mirrors what raw-CDP clients
|
||||
// do. The socket loop must drain the pending background navigation
|
||||
// completion BEFORE dispatching Runtime.evaluate, otherwise evaluate
|
||||
// would observe `NoDocumentLoaded`.
|
||||
// attachment BEFORE dispatching Runtime.evaluate, otherwise evaluate
|
||||
// would observe `NoDocumentLoaded`. The committed Document's parser
|
||||
// continuation is deliberately independent, so its body may still be
|
||||
// incomplete when evaluate runs.
|
||||
socket
|
||||
.send(WsMessage::Text(
|
||||
json!({
|
||||
@@ -2012,7 +2014,7 @@ async fn websocket_cdp_raw_client_runtime_evaluate_immediately_after_page_naviga
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": session_id,
|
||||
"params": {
|
||||
"expression": "document.querySelector('#link') ? document.querySelector('#link').textContent : ''",
|
||||
"expression": "JSON.stringify({ url: document.URL, ready: document.readyState, hasLink: document.querySelector('#link') !== null })",
|
||||
"returnByValue": true
|
||||
}
|
||||
})
|
||||
@@ -2034,10 +2036,27 @@ async fn websocket_cdp_raw_client_runtime_evaluate_immediately_after_page_naviga
|
||||
let value = evaluate_response["result"]["result"]["value"]
|
||||
.as_str()
|
||||
.expect("string value from evaluate");
|
||||
let observed: serde_json::Value =
|
||||
serde_json::from_str(value).expect("Runtime.evaluate should return serialized JSON");
|
||||
assert_eq!(
|
||||
value, "link",
|
||||
"evaluate must observe the new document body (got `{value}`)"
|
||||
observed["url"],
|
||||
json!(fixture_url),
|
||||
"evaluate must observe the newly committed Document: {observed}"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
observed["ready"].as_str(),
|
||||
Some("loading" | "interactive" | "complete")
|
||||
),
|
||||
"the committed Document must expose a valid readyState: {observed}"
|
||||
);
|
||||
if observed["ready"] != json!("loading") {
|
||||
assert_eq!(
|
||||
observed["hasLink"],
|
||||
json!(true),
|
||||
"a parser-complete Document must contain its body link: {observed}"
|
||||
);
|
||||
}
|
||||
|
||||
let _ = socket.close(None).await;
|
||||
abort_test_cdp_server(protocol_server).await;
|
||||
@@ -2143,9 +2162,9 @@ async fn websocket_cdp_runtime_control_command_waits_for_navigation_attachment_c
|
||||
let new_isolate_id = resumed["result"]["id"]
|
||||
.as_str()
|
||||
.expect("replacement Runtime.getIsolateId");
|
||||
assert_ne!(
|
||||
assert_eq!(
|
||||
new_isolate_id, old_isolate_id,
|
||||
"queued command must bind to the replacement document's renderer attachment"
|
||||
"stable Page replacement must preserve its script-agent isolate while the queued command crosses the renderer attachment cutover"
|
||||
);
|
||||
|
||||
let _ = socket.close(None).await;
|
||||
@@ -3881,11 +3900,17 @@ document.addEventListener('readystatechange', () => {
|
||||
.iter()
|
||||
.find(|message| message["id"] == json!(7_u64))
|
||||
.expect("Runtime.evaluate response");
|
||||
assert_eq!(
|
||||
response["result"]["result"]["value"],
|
||||
json!(
|
||||
"{\"marker\":\"committed-source\",\"ready\":\"interactive\",\"deferExecuted\":false}"
|
||||
)
|
||||
let observed: serde_json::Value = serde_json::from_str(
|
||||
response["result"]["result"]["value"]
|
||||
.as_str()
|
||||
.expect("serialized Runtime.evaluate result"),
|
||||
)
|
||||
.expect("Runtime.evaluate should return serialized JSON");
|
||||
assert_eq!(observed["marker"], json!("committed-source"));
|
||||
assert_eq!(observed["deferExecuted"], json!(false));
|
||||
assert!(
|
||||
matches!(observed["ready"].as_str(), Some("loading" | "interactive")),
|
||||
"the parser may race the command after starting the deferred fetch, but load must remain pending: {observed}"
|
||||
);
|
||||
|
||||
release_defer.notify_one();
|
||||
|
||||
@@ -195,6 +195,18 @@ async fn handle_bidi_session_socket_local(
|
||||
break;
|
||||
}
|
||||
}
|
||||
maybe_continuation = receivers.document_continuation_completion_rx.recv() => {
|
||||
let Some(continuation) = maybe_continuation else {
|
||||
break;
|
||||
};
|
||||
if !actor.handle_background_navigation_completion(
|
||||
&mut scheduler,
|
||||
&mut receivers,
|
||||
moli_protocol::BackgroundNavigationCompletion::document_continuation(continuation),
|
||||
).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
maybe_completion = receivers.background_navigation_completion_rx.recv() => {
|
||||
let Some(completion) = maybe_completion else {
|
||||
break;
|
||||
@@ -3173,7 +3185,7 @@ async fn drain_bidi_background_navigation_before_command(
|
||||
) -> Result<BidiDevToolsEventSources, BidiRendererOutputTransportFailure> {
|
||||
let mut event_sources = drain_ready_bidi_background_navigation(scheduler, receivers).await?;
|
||||
while scheduler.has_inflight_background_navigation() {
|
||||
let Some(completion) = receivers.background_navigation_completion_rx.recv().await else {
|
||||
let Some(completion) = receivers.recv_navigation_completion().await else {
|
||||
return Ok(event_sources);
|
||||
};
|
||||
match scheduler
|
||||
@@ -3202,7 +3214,7 @@ async fn drain_ready_bidi_background_navigation(
|
||||
scheduler
|
||||
.drain_background_events_around_inflight_navigation(&mut receivers.background_event_rx),
|
||||
);
|
||||
while let Ok(completion) = receivers.background_navigation_completion_rx.try_recv() {
|
||||
while let Some(completion) = receivers.try_recv_navigation_completion() {
|
||||
match scheduler
|
||||
.drain_background_navigation_completion_with_progress_barrier(completion, receivers)
|
||||
.await
|
||||
|
||||
@@ -1362,6 +1362,19 @@ async fn classic_session_runtime_loop(
|
||||
adapter_scheduler.schedule_turn_if_needed(&scheduler, page_javascript_blocked);
|
||||
tokio::select! {
|
||||
biased;
|
||||
continuation = receivers.document_continuation_completion_rx.recv() => {
|
||||
let Some(continuation) = continuation else {
|
||||
break;
|
||||
};
|
||||
if !attached.actor.handle_background_navigation_completion(
|
||||
&mut scheduler,
|
||||
&mut receivers,
|
||||
moli_protocol::BackgroundNavigationCompletion::document_continuation(continuation),
|
||||
).await
|
||||
{
|
||||
detach_bidi = true;
|
||||
}
|
||||
}
|
||||
completion = receivers.background_navigation_completion_rx.recv() => {
|
||||
let Some(completion) = completion else {
|
||||
break;
|
||||
@@ -1497,6 +1510,21 @@ async fn classic_session_runtime_loop(
|
||||
adapter_scheduler.schedule_turn_if_needed(&scheduler, page_javascript_blocked);
|
||||
tokio::select! {
|
||||
biased;
|
||||
continuation = receivers.document_continuation_completion_rx.recv() => {
|
||||
let Some(continuation) = continuation else {
|
||||
break;
|
||||
};
|
||||
if scheduler
|
||||
.drain_background_navigation_completion_with_progress_barrier(
|
||||
moli_protocol::BackgroundNavigationCompletion::document_continuation(continuation),
|
||||
&mut receivers,
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
completion = receivers.background_navigation_completion_rx.recv() => {
|
||||
let Some(completion) = completion else {
|
||||
break;
|
||||
|
||||
+97
-17
@@ -506,6 +506,7 @@ pub(crate) use output::{
|
||||
pub(crate) use page_state::{LoadedNavigationPageCommit, LoadedNavigationRendererAttachmentCommit};
|
||||
pub(crate) use popup_activation_work::PopupTargetActivationAction;
|
||||
pub(crate) use popup_navigation_work::{
|
||||
PopupTargetNavigationAuthorityState, PopupTargetNavigationClaimIdentity,
|
||||
PopupTargetNavigationKind, PopupTargetNavigationOwnerAction,
|
||||
};
|
||||
pub(crate) use runtime_eval::{
|
||||
@@ -526,13 +527,16 @@ pub use runtime_eval::{
|
||||
pub(crate) use runtime_load::decode_data_url_response;
|
||||
pub(crate) use runtime_load::{
|
||||
BackgroundNavigationBodyCompletionSink, BackgroundNavigationEarlyResult,
|
||||
BackgroundNavigationLoadJob, CompletedInitialDocumentPageBuild, FailedInitialDocumentPageBuild,
|
||||
InitialDocumentPageInstallResult, InitialDocumentPageOwner, PausedResponsePreparedDocument,
|
||||
PendingInitialDocumentPageBuild, ResponseCommitReady,
|
||||
BackgroundNavigationLoadJob, CommittedStablePageNavigation, CompletedInitialDocumentPageBuild,
|
||||
FailedInitialDocumentPageBuild, InitialDocumentPageInstallResult, InitialDocumentPageOwner,
|
||||
PausedResponsePreparedDocument, PendingInitialDocumentPageBuild, ResponseCommitReady,
|
||||
StablePageNavigationCommitTarget,
|
||||
};
|
||||
use scheduler_hooks::CdpSchedulerHooks;
|
||||
use scheduler_state::CdpConnectionSchedulerState;
|
||||
pub use scheduler_state::{CdpRendererOwnerTurnOutcome, CdpSchedulerEvent, CdpTurnOutcome};
|
||||
pub use scheduler_state::{
|
||||
BackgroundNavigationGateKey, CdpRendererOwnerTurnOutcome, CdpSchedulerEvent, CdpTurnOutcome,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use site_data_manager_surface::{
|
||||
BrowserContextReservedSiteDataOwnerState, BrowserContextSiteDataManagerOwnerState,
|
||||
@@ -542,8 +546,9 @@ pub use state::{
|
||||
DocumentStartScript, DownloadNavigation, EmulatedDeviceMetrics, EmulatedGeolocationOverride,
|
||||
EmulatedGeolocationOverrideState, EmulatedMediaOverrides, IsolatedWorldDefinition,
|
||||
LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome, NavigationRequestLoadPolicy,
|
||||
PageNavigationHistoryEntry, ParkedFetchState, ParkedNetworkArtifacts, ParkedPageSessionState,
|
||||
PendingNavigationHistoryUpdate, RuntimeBindingDefinition, TargetInfo, URL_BASE,
|
||||
NoCommitResponseNavigation, PageNavigationHistoryEntry, ParkedFetchState,
|
||||
ParkedNetworkArtifacts, ParkedPageSessionState, PendingNavigationHistoryUpdate,
|
||||
RuntimeBindingDefinition, TargetInfo, URL_BASE,
|
||||
};
|
||||
pub(crate) use state::{
|
||||
BrowserContextPageStorageHandles, BrowserContextResourceStorageHandles,
|
||||
@@ -1422,6 +1427,16 @@ impl CdpConnection {
|
||||
.set_background_navigation_completion_sender(sender);
|
||||
}
|
||||
|
||||
pub fn set_document_continuation_completion_sender(
|
||||
&mut self,
|
||||
sender: tokio::sync::mpsc::UnboundedSender<
|
||||
crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
>,
|
||||
) {
|
||||
self.scheduler_hooks
|
||||
.set_document_continuation_completion_sender(sender);
|
||||
}
|
||||
|
||||
pub fn set_renderer_publication_sender(
|
||||
&mut self,
|
||||
sender: moli_core::RendererOutputTransportSender,
|
||||
@@ -2321,9 +2336,13 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
fn can_run_background_navigation_for_session_owner(&self, session_id: Option<&str>) -> bool {
|
||||
if !self
|
||||
.scheduler_hooks
|
||||
.has_background_navigation_completion_sender()
|
||||
if !self.scheduler_hooks.has_background_event_sender()
|
||||
|| !self
|
||||
.scheduler_hooks
|
||||
.has_background_navigation_completion_sender()
|
||||
|| !self
|
||||
.scheduler_hooks
|
||||
.has_document_continuation_completion_sender()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -2332,9 +2351,13 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
fn can_run_background_navigation_for_active_session(&self) -> bool {
|
||||
if !self
|
||||
.scheduler_hooks
|
||||
.has_background_navigation_completion_sender()
|
||||
if !self.scheduler_hooks.has_background_event_sender()
|
||||
|| !self
|
||||
.scheduler_hooks
|
||||
.has_background_navigation_completion_sender()
|
||||
|| !self
|
||||
.scheduler_hooks
|
||||
.has_document_continuation_completion_sender()
|
||||
|| !self.inactive_browser_contexts.is_empty()
|
||||
{
|
||||
return false;
|
||||
@@ -2401,6 +2424,32 @@ impl CdpConnection {
|
||||
}
|
||||
return command_context.take_protocol_events();
|
||||
}
|
||||
crate::domains::page::BackgroundNavigationCompletion::DocumentContinuation(
|
||||
completion,
|
||||
) => {
|
||||
let gate_key = completion.document_continuation_gate_key();
|
||||
let target_id = gate_key.target_id().map(str::to_owned);
|
||||
let session_id = gate_key.session_id().map(str::to_owned);
|
||||
let (renderer_output_predecessor, renderer_page_state) =
|
||||
completion.into_renderer_completion_parts();
|
||||
if let Some(predecessor) = renderer_output_predecessor {
|
||||
command_context.set_renderer_output_predecessor(predecessor);
|
||||
}
|
||||
let page_state_applied = renderer_page_state.is_some_and(|page_state| {
|
||||
self.apply_renderer_document_continuation_page_state(&gate_key, page_state)
|
||||
});
|
||||
if page_state_applied {
|
||||
let _ = self
|
||||
.target_session_owner_navigation_history_snapshot(session_id.as_deref());
|
||||
if let Some(target_id) = target_id {
|
||||
command_context.protocol_events_mut().extend(
|
||||
self.exact_target_info_changed_event_plan_for_target_delta(&target_id)
|
||||
.into_background_events(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return command_context.take_protocol_events();
|
||||
}
|
||||
};
|
||||
let previous_none_session_owner_route =
|
||||
completion.navigate_session_id().is_none().then(|| {
|
||||
@@ -2440,6 +2489,31 @@ impl CdpConnection {
|
||||
protocol_events
|
||||
}
|
||||
|
||||
pub(crate) fn schedule_background_document_continuation_completion(
|
||||
&mut self,
|
||||
completion: crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
observer: Option<moli_core::page::RendererDocumentContinuationObserver>,
|
||||
) -> Option<moli_core::page::RendererDocumentContinuationObserver> {
|
||||
let Some(sender) = self
|
||||
.scheduler_hooks
|
||||
.document_continuation_completion_sender()
|
||||
else {
|
||||
return observer;
|
||||
};
|
||||
let key = completion.document_continuation_gate_key();
|
||||
self.scheduler_state
|
||||
.push_scheduler_event(CdpSchedulerEvent::DocumentContinuationStarted { key });
|
||||
let Some(observer) = observer else {
|
||||
let _ = sender.send(completion);
|
||||
return None;
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
let renderer_completion = observer.wait().await;
|
||||
let _ = sender.send(completion.with_renderer_completion(renderer_completion));
|
||||
});
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn replace_navigation_engine(&mut self, engine: NavigationEngine) {
|
||||
let engine = engine;
|
||||
self.apply_scheduler_senders_to_navigation_engine(&engine);
|
||||
@@ -2850,9 +2924,13 @@ impl CdpConnection {
|
||||
);
|
||||
}
|
||||
let is_current = completion.is_current_for_connection(self);
|
||||
let (token, state, navigation, engine) = completion.into_parts();
|
||||
let (token, state, navigation, engine, background_document_continuation) =
|
||||
completion.into_parts();
|
||||
if !is_current {
|
||||
crate::domains::page::push_superseded_navigation_result(out, &state);
|
||||
if let Some(completion) = background_document_continuation {
|
||||
let _ = self.schedule_background_document_continuation_completion(completion, None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let navigation_session_id = state.navigate_session_id.clone();
|
||||
@@ -2863,6 +2941,7 @@ impl CdpConnection {
|
||||
state,
|
||||
navigation,
|
||||
command_context,
|
||||
background_document_continuation,
|
||||
)
|
||||
.await;
|
||||
if let Some(engine) = engine {
|
||||
@@ -3098,11 +3177,12 @@ impl CdpConnection {
|
||||
"documentIsolateAccounting": document_isolate_accounting,
|
||||
"estimatedWorkerIsolateCount": estimated_worker_isolate_count,
|
||||
"estimatedLiveV8IsolateCount": estimated_live_v8_isolate_count,
|
||||
"runtimeGetHeapUsageV8HeapScope": "page-vm-document-isolate",
|
||||
"runtimeGetHeapUsageV8HeapIsTargetLocal": true,
|
||||
"runtimeGetHeapUsageV8HeapScope": "script-agent-document-isolate",
|
||||
"runtimeGetHeapUsageV8HeapIsTargetLocal": false,
|
||||
"runtimeGetHeapUsageV8HeapMayIncludeRelatedPages": true,
|
||||
"runtimeGetHeapUsageMoliCountersScope": "target-document",
|
||||
"runtimeCollectGarbageScope": "page-vm-document-isolate",
|
||||
"v8ForegroundTaskWakeScope": "page-vm-document-isolate",
|
||||
"runtimeCollectGarbageScope": "script-agent-document-isolate",
|
||||
"v8ForegroundTaskWakeScope": "script-agent-document-isolate",
|
||||
"v8ForegroundTaskWakeContextGroupIdAvailable": false,
|
||||
"v8ForegroundTaskWakeInternalPolicy": "page-runtime-queue-and-owner-page-tick",
|
||||
"v8ForegroundTaskWakeExternalPolicy": "page-owner-runtime-wake",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::session_owner::TargetSessionOwner;
|
||||
use super::*;
|
||||
use crate::conn::state::{
|
||||
BrowserContextPageStorageHandles, BrowserContextResourceStorageHandles, DevToolsSessionState,
|
||||
PageNavigationHistoryEntry, RendererMainDocumentCommitSeed, TargetFetchConfig,
|
||||
TargetNetworkPolicyState, TargetOwnerState, TargetPageAbsenceReason,
|
||||
TargetPageResidenceIdentity, TargetRuntimeSessionState, TargetRuntimeSlot,
|
||||
page_bypass_csp_enabled_for_devtools_sessions,
|
||||
BrowserContextPageStorageHandles, BrowserContextResourceStorageHandles,
|
||||
CommittedRendererAgentAttachment, DevToolsSessionState, PageNavigationHistoryEntry,
|
||||
RendererMainDocumentCommitSeed, TargetFetchConfig, TargetNetworkPolicyState, TargetOwnerState,
|
||||
TargetPageAbsenceReason, TargetPageResidenceIdentity, TargetRuntimeSessionState,
|
||||
TargetRuntimeSlot, page_bypass_csp_enabled_for_devtools_sessions,
|
||||
prepare_renderer_call_replacements_for_devtools_sessions, runtime_bindings_for_renderer,
|
||||
};
|
||||
use crate::conn::{
|
||||
@@ -205,6 +205,10 @@ pub(crate) struct TargetNavigationLoadInputs {
|
||||
}
|
||||
|
||||
impl TargetNavigationLoadInputs {
|
||||
pub(crate) fn has_main_document_commit_seed(&self) -> bool {
|
||||
self.main_document_commit_seed.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn with_main_document_commit_seed(
|
||||
mut self,
|
||||
seed: RendererMainDocumentCommitSeed,
|
||||
@@ -2242,6 +2246,31 @@ impl<'a> TargetSessionOwnerMut<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_no_commit_response_navigation_history(&mut self) -> Option<()> {
|
||||
match self {
|
||||
Self::ActiveTarget {
|
||||
browser_context, ..
|
||||
} => {
|
||||
browser_context
|
||||
.active_target
|
||||
.owner_state
|
||||
.resolve_no_commit_response_navigation_history();
|
||||
Some(())
|
||||
}
|
||||
Self::BackgroundTarget {
|
||||
browser_context,
|
||||
target_id,
|
||||
..
|
||||
} => {
|
||||
browser_context.mutate_parked_target_owner_state(target_id, |owner_state| {
|
||||
owner_state.resolve_no_commit_response_navigation_history()
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
Self::NoLoadedBrowserContext => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn commit_loaded_navigation_page_async(
|
||||
&mut self,
|
||||
mut page: Page,
|
||||
@@ -2385,6 +2414,102 @@ impl<'a> TargetSessionOwnerMut<'a> {
|
||||
Self::NoLoadedBrowserContext => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn commit_loaded_navigation_document_replacement(
|
||||
&mut self,
|
||||
renderer_attachment_commit: CommittedRendererAgentAttachment,
|
||||
history_url: &Url,
|
||||
) -> Option<anyhow::Result<LoadedNavigationPageCommit>> {
|
||||
match self {
|
||||
Self::ActiveTarget {
|
||||
browser_context, ..
|
||||
} => {
|
||||
if let Some(target_id) = browser_context.active_target_id_owned() {
|
||||
browser_context.mark_target_initial_empty_document_exited(&target_id);
|
||||
}
|
||||
Some(
|
||||
browser_context.commit_loaded_navigation_document_replacement(
|
||||
renderer_attachment_commit,
|
||||
history_url,
|
||||
),
|
||||
)
|
||||
}
|
||||
Self::BackgroundTarget {
|
||||
browser_context,
|
||||
target_id,
|
||||
..
|
||||
} => {
|
||||
let primary_session_id = browser_context
|
||||
.background_target(target_id)?
|
||||
.session_id()
|
||||
.map(str::to_owned);
|
||||
let page_snapshot = browser_context
|
||||
.background_target(target_id)?
|
||||
.loaded_page()
|
||||
.map(|page| (history_url.to_string(), page.document_title()))?;
|
||||
browser_context.mutate_parked_target_owner_state(target_id, |owner_state| {
|
||||
owner_state.mark_initial_empty_document_exited();
|
||||
owner_state.record_loaded_page_navigation_history(page_snapshot);
|
||||
owner_state.clear_committed_document_navigation_state();
|
||||
});
|
||||
browser_context.mutate_parked_page_session_state(target_id, |state| {
|
||||
clear_parked_page_loaded_document_session_state(state);
|
||||
});
|
||||
let (
|
||||
previous_attachment,
|
||||
new_attachment_id,
|
||||
committed_document_post_response_continuation,
|
||||
) = {
|
||||
let target = browser_context.background_target_mut(target_id)?;
|
||||
let previous_attachment = match target
|
||||
.runtime_slot
|
||||
.commit_loaded_navigation_document_replacement(&renderer_attachment_commit)
|
||||
{
|
||||
Ok(previous) => previous,
|
||||
Err(error) => return Some(Err(error.into())),
|
||||
};
|
||||
let page = target
|
||||
.runtime_slot
|
||||
.loaded_page_mut()
|
||||
.expect("committed stable navigation must retain its Page");
|
||||
let new_attachment_id = page
|
||||
.renderer_agent_attachment_id()
|
||||
.expect("committed navigation Page must have a renderer attachment");
|
||||
let continuation = page.take_committed_document_post_response_continuation();
|
||||
(previous_attachment, new_attachment_id, continuation)
|
||||
};
|
||||
if let Some(previous_attachment) = previous_attachment
|
||||
&& previous_attachment.id() != new_attachment_id
|
||||
{
|
||||
let replacements =
|
||||
match browser_context.mutate_parked_page_session_state(target_id, |state| {
|
||||
prepare_renderer_call_replacements_for_devtools_sessions(
|
||||
primary_session_id.as_deref(),
|
||||
&mut state.devtools_session_state,
|
||||
&mut state.auxiliary_devtools_session_states,
|
||||
previous_attachment.id(),
|
||||
new_attachment_id,
|
||||
)
|
||||
}) {
|
||||
Ok(replacements) => replacements,
|
||||
Err(error) => return Some(Err(error.into())),
|
||||
};
|
||||
browser_context
|
||||
.background_target_mut(target_id)?
|
||||
.runtime_slot
|
||||
.install_pending_renderer_call_replacements(replacements);
|
||||
}
|
||||
let target = browser_context.background_target_mut(target_id)?;
|
||||
target.runtime_slot.reset_subresource_cursor();
|
||||
target.runtime_slot.clear_websocket_artifacts();
|
||||
Some(Ok(LoadedNavigationPageCommit {
|
||||
replaced_page_owner: None,
|
||||
committed_document_post_response_continuation,
|
||||
}))
|
||||
}
|
||||
Self::NoLoadedBrowserContext => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CdpConnection {
|
||||
@@ -2483,6 +2608,16 @@ impl CdpConnection {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn commit_loaded_navigation_document_replacement_for_session_owner(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
renderer_attachment_commit: CommittedRendererAgentAttachment,
|
||||
history_url: &Url,
|
||||
) -> Option<anyhow::Result<LoadedNavigationPageCommit>> {
|
||||
self.target_session_owner_mut(session_id)?
|
||||
.commit_loaded_navigation_document_replacement(renderer_attachment_commit, history_url)
|
||||
}
|
||||
|
||||
pub(crate) fn initial_document_page_owner_for_session(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
@@ -2590,6 +2725,14 @@ impl CdpConnection {
|
||||
.clear_pending_navigation_history_update()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_no_commit_response_navigation_history_for_session_owner(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
) -> Option<()> {
|
||||
self.target_session_owner_mut(session_id)?
|
||||
.resolve_no_commit_response_navigation_history()
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_target_crashed_for_session_owner_async(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
@@ -3508,6 +3651,34 @@ impl CdpConnection {
|
||||
.frame_tree_loader_id()
|
||||
}
|
||||
|
||||
pub(crate) fn apply_renderer_document_continuation_page_state(
|
||||
&mut self,
|
||||
gate_key: &crate::conn::BackgroundNavigationGateKey,
|
||||
page_state: std::sync::Arc<moli_core::page::RendererPageState>,
|
||||
) -> bool {
|
||||
let session_id = gate_key.session_id();
|
||||
let Some((_, routed_target_id)) = self.target_owner_identity_for_session(session_id) else {
|
||||
return false;
|
||||
};
|
||||
if gate_key.target_id() != routed_target_id.as_deref()
|
||||
|| self
|
||||
.target_session_owner_frame_tree_loader_id(session_id)
|
||||
.as_deref()
|
||||
!= Some(gate_key.loader_id())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(page) = self
|
||||
.target_session_owner_mut(session_id)
|
||||
.and_then(TargetSessionOwnerMut::into_runtime_slot_mut)
|
||||
.and_then(TargetRuntimeSlot::loaded_page_mut)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
page.apply_renderer_document_continuation_state(page_state);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn target_session_owner_emulated_device_metrics(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
@@ -4367,6 +4538,7 @@ mod tests {
|
||||
source: "globalThis.fromParkedPreload = true;".to_owned(),
|
||||
world_name: Some("utility".to_owned()),
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -67,6 +67,22 @@ fn bidi_fetch_command_context() -> DevToolsCommandContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_navigation_result_loader_id_for_test(
|
||||
result: &crate::devtools_runtime::DevToolsNavigateResult,
|
||||
) -> Option<String> {
|
||||
result
|
||||
.loader_id
|
||||
.as_ref()
|
||||
.map(|loader_id| loader_id.as_str().to_owned())
|
||||
.or_else(|| {
|
||||
result
|
||||
.navigation_id
|
||||
.as_ref()
|
||||
.and_then(|navigation_id| navigation_id.as_str().strip_prefix("navigation-"))
|
||||
.map(str::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_direct_devtools_command_through_renderer_fence_for_test(
|
||||
ctx: &mut crate::testing::TestContext,
|
||||
command: DevToolsCommand,
|
||||
@@ -3446,12 +3462,29 @@ async fn devtools_command_executes_child_frame_navigation_without_cdp_response_s
|
||||
wait: DevToolsNavigationWait::Load,
|
||||
}))
|
||||
.await;
|
||||
let (parent_result, _, _, parent_predecessor) = parent_outcome.into_complete_parts();
|
||||
let (parent_result, parent_scheduler_events, parent_protocol_events, parent_predecessor) =
|
||||
parent_outcome.into_complete_parts();
|
||||
ctx.route_direct_command_output_for_test(Vec::new(), parent_scheduler_events)
|
||||
.await;
|
||||
if let Some(predecessor) = parent_predecessor {
|
||||
ctx.route_direct_command_renderer_predecessor_for_test(predecessor)
|
||||
.await;
|
||||
}
|
||||
parent_result.expect("parent navigation should succeed");
|
||||
ctx.route_direct_command_output_for_test(parent_protocol_events, Vec::new())
|
||||
.await;
|
||||
let DevToolsCommandResult::Navigate(parent_navigation) =
|
||||
parent_result.expect("parent navigation should succeed")
|
||||
else {
|
||||
panic!("expected parent navigation result");
|
||||
};
|
||||
let parent_loader_id = direct_navigation_result_loader_id_for_test(&parent_navigation)
|
||||
.expect("parent navigation should expose a loader identity");
|
||||
ctx.wait_for_direct_navigation_load_for_test(
|
||||
&target_context,
|
||||
&parent_loader_id,
|
||||
"protocol-neutral parent frame navigation load",
|
||||
)
|
||||
.await;
|
||||
|
||||
let (frame_tree_result, _) = ctx
|
||||
.conn
|
||||
@@ -3492,8 +3525,10 @@ async fn devtools_command_executes_child_frame_navigation_without_cdp_response_s
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let (navigate_result, _scheduler_events, protocol_events, child_predecessor) =
|
||||
let (navigate_result, child_scheduler_events, protocol_events, child_predecessor) =
|
||||
child_outcome.into_complete_parts();
|
||||
ctx.route_direct_command_output_for_test(Vec::new(), child_scheduler_events)
|
||||
.await;
|
||||
if let Some(predecessor) = child_predecessor {
|
||||
ctx.route_direct_command_renderer_predecessor_for_test(predecessor)
|
||||
.await;
|
||||
@@ -5512,57 +5547,56 @@ async fn devtools_command_executes_input_key_command_without_cdp_sidecar() {
|
||||
target_id: Some(target_id.clone()),
|
||||
..context.clone()
|
||||
};
|
||||
// Programmatic focus is part of parser execution. In contrast, `autofocus`
|
||||
// is a post-DOMContentLoaded rendering update and can race the first input
|
||||
// command sent to this intentionally inactive target.
|
||||
let url = "data:text/html,<input id='field'><script>document.getElementById('field').focus()</script>";
|
||||
let (navigate_result, scheduler_events, protocol_events) = ctx
|
||||
.conn
|
||||
.execute_devtools_command(DevToolsCommand::Navigate(DevToolsNavigateCommand {
|
||||
let url = "data:text/html,<input id='field'>";
|
||||
let navigate_result = execute_direct_devtools_command_through_renderer_fence_for_test(
|
||||
&mut ctx,
|
||||
DevToolsCommand::Navigate(DevToolsNavigateCommand {
|
||||
context: target_context.clone(),
|
||||
url: url.to_owned(),
|
||||
referrer: None,
|
||||
wait: DevToolsNavigationWait::Load,
|
||||
}))
|
||||
.await
|
||||
.into_parts_with_protocol_events();
|
||||
navigate_result.expect("navigation should succeed");
|
||||
ctx.sent
|
||||
.extend(crate::testing::protocol_events_into_internal_messages(
|
||||
protocol_events,
|
||||
));
|
||||
ctx.route_direct_command_output_for_test(Vec::new(), scheduler_events)
|
||||
.await;
|
||||
ctx.wait_for_direct_command_work_completion_for_test(
|
||||
"protocol-neutral navigation load owner action",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
navigate_result.expect("navigation should succeed");
|
||||
execute_direct_devtools_command_through_renderer_fence_for_test(
|
||||
&mut ctx,
|
||||
DevToolsCommand::EvaluateScript(DevToolsEvaluateScriptCommand {
|
||||
context: target_context.clone(),
|
||||
realm_id: None,
|
||||
world_name: None,
|
||||
expression: "document.getElementById('field').focus()".to_owned(),
|
||||
await_promise: false,
|
||||
user_gesture: false,
|
||||
webdriver_bidi_file_prompt_handler: None,
|
||||
result_ownership: DevToolsResultOwnership::None,
|
||||
preserve_remote_metadata: false,
|
||||
materialize_bidi_script_result: false,
|
||||
serialization_options: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("input focus setup should succeed");
|
||||
ctx.sent.clear();
|
||||
|
||||
let (key_result, _scheduler_events, protocol_events) = ctx
|
||||
.conn
|
||||
.execute_devtools_command(DevToolsCommand::DispatchKeyEvent(
|
||||
DevToolsDispatchKeyEventCommand {
|
||||
context: target_context.clone(),
|
||||
event_type: DevToolsKeyEventType::KeyPress,
|
||||
key: "Z".to_owned(),
|
||||
code: "KeyZ".to_owned(),
|
||||
text: "Z".to_owned(),
|
||||
modifiers: 0,
|
||||
auto_repeat: false,
|
||||
should_insert_text: true,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.into_parts_with_protocol_events();
|
||||
let key_result = execute_direct_devtools_command_through_renderer_fence_for_test(
|
||||
&mut ctx,
|
||||
DevToolsCommand::DispatchKeyEvent(DevToolsDispatchKeyEventCommand {
|
||||
context: target_context.clone(),
|
||||
event_type: DevToolsKeyEventType::KeyPress,
|
||||
key: "Z".to_owned(),
|
||||
code: "KeyZ".to_owned(),
|
||||
text: "Z".to_owned(),
|
||||
modifiers: 0,
|
||||
auto_repeat: false,
|
||||
should_insert_text: true,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
key_result.expect("key dispatch should succeed"),
|
||||
DevToolsCommandResult::Empty
|
||||
);
|
||||
assert!(
|
||||
protocol_events.is_empty(),
|
||||
"direct input key dispatch must not emit CDP-shaped sidecar messages: {protocol_events:?}"
|
||||
);
|
||||
|
||||
let value_result = ctx
|
||||
.execute_devtools_command_through_renderer_fence_for_test(DevToolsCommand::EvaluateScript(
|
||||
|
||||
@@ -164,6 +164,32 @@ impl CdpConnection {
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub(crate) fn commit_renderer_agent_candidate_for_stable_page_replacement_for_session_owner(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
candidate: PreparedRendererAgentAttachment,
|
||||
expected: &super::StablePageNavigationCommitTarget,
|
||||
) -> Result<CommittedRendererAgentAttachment, String> {
|
||||
self.validate_navigation_target_owner(session_id, candidate.navigation())?;
|
||||
if !self.target_page_residence_identity_is_current_for_session(
|
||||
session_id,
|
||||
expected.target_page(),
|
||||
) || self.renderer_page_residence_identity_for_session_owner(session_id)
|
||||
!= Some(expected.renderer_page())
|
||||
{
|
||||
return Err("StableNavigationPageSuperseded".to_owned());
|
||||
}
|
||||
let transaction = self
|
||||
.runtime_session_owner_slot_mut(session_id)?
|
||||
.commit_renderer_agent_candidate_transaction(candidate, expected.renderer_page())
|
||||
.map_err(|error| error.to_string())?;
|
||||
self.bind_renderer_page_output_owner(
|
||||
expected.renderer_page(),
|
||||
expected.target_page().clone(),
|
||||
);
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub(crate) fn rollback_committed_renderer_agent_candidate_for_session_owner(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
|
||||
@@ -316,6 +316,65 @@ impl BrowserContext {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn commit_loaded_navigation_document_replacement(
|
||||
&mut self,
|
||||
renderer_attachment_commit: CommittedRendererAgentAttachment,
|
||||
history_url: &Url,
|
||||
) -> anyhow::Result<LoadedNavigationPageCommit> {
|
||||
let primary_session_id = self.active_session_id_owned();
|
||||
let previous_attachment = self
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.commit_loaded_navigation_document_replacement(&renderer_attachment_commit)?;
|
||||
let page = self
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.loaded_page_mut()
|
||||
.expect("committed stable navigation must retain its Page");
|
||||
let new_attachment_id = page
|
||||
.renderer_agent_attachment_id()
|
||||
.expect("committed navigation Page must have a renderer attachment");
|
||||
let committed_document_post_response_continuation =
|
||||
page.take_committed_document_post_response_continuation();
|
||||
if let Some(previous_attachment) = previous_attachment
|
||||
&& previous_attachment.id() != new_attachment_id
|
||||
{
|
||||
let replacements = prepare_renderer_call_replacements_for_devtools_sessions(
|
||||
primary_session_id.as_deref(),
|
||||
&mut self.devtools_session_state,
|
||||
&mut self.auxiliary_devtools_session_states,
|
||||
previous_attachment.id(),
|
||||
new_attachment_id,
|
||||
)?;
|
||||
self.active_target
|
||||
.runtime_slot
|
||||
.install_pending_renderer_call_replacements(replacements);
|
||||
}
|
||||
let page_snapshot = self
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.loaded_page()
|
||||
.map(|page| (history_url.to_string(), page.document_title()))
|
||||
.expect("committed stable navigation must retain its Page");
|
||||
self.active_target
|
||||
.owner_state
|
||||
.record_loaded_page_navigation_history(page_snapshot);
|
||||
self.reset_subresource_network_cursor();
|
||||
self.clear_websocket_network_artifacts();
|
||||
self.active_target
|
||||
.owner_state
|
||||
.clear_committed_document_navigation_state();
|
||||
self.clear_active_target_loaded_document_session_state();
|
||||
self.clear_active_target_runtime_remote_object_tracking();
|
||||
Ok(LoadedNavigationPageCommit {
|
||||
// A renderer Document replacement keeps the same typed Page
|
||||
// attachment in the public repository. Page-owned work therefore
|
||||
// remains resident; Document currentness is tracked separately.
|
||||
replaced_page_owner: None,
|
||||
committed_document_post_response_continuation,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_active_target_session_scoped_state_async(
|
||||
&mut self,
|
||||
) -> Result<(), String> {
|
||||
|
||||
@@ -40,6 +40,7 @@ impl BrowserContext {
|
||||
initial_empty_document_url,
|
||||
creator,
|
||||
None,
|
||||
None,
|
||||
session_storage_namespace,
|
||||
);
|
||||
}
|
||||
@@ -51,6 +52,7 @@ impl BrowserContext {
|
||||
url: String,
|
||||
initial_empty_document_url: Option<String>,
|
||||
creator: Option<TargetInitialEmptyDocumentCreator>,
|
||||
pending_auxiliary_page: Option<moli_core::page::RendererPendingAuxiliaryPage>,
|
||||
session_storage_store: Option<SharedWebStorageStore>,
|
||||
initial_empty_document_storage_key: Option<moli_storage_key::MoliStorageKey>,
|
||||
) {
|
||||
@@ -63,6 +65,7 @@ impl BrowserContext {
|
||||
url,
|
||||
initial_empty_document_url,
|
||||
creator,
|
||||
pending_auxiliary_page,
|
||||
initial_empty_document_storage_key,
|
||||
session_storage_namespace,
|
||||
);
|
||||
@@ -88,6 +91,7 @@ impl BrowserContext {
|
||||
url: String,
|
||||
initial_empty_document_url: Option<String>,
|
||||
creator: Option<TargetInitialEmptyDocumentCreator>,
|
||||
pending_auxiliary_page: Option<moli_core::page::RendererPendingAuxiliaryPage>,
|
||||
initial_empty_document_storage_key: Option<moli_storage_key::MoliStorageKey>,
|
||||
session_storage_namespace: Option<TargetSessionStorageNamespace>,
|
||||
) {
|
||||
@@ -101,6 +105,14 @@ impl BrowserContext {
|
||||
);
|
||||
});
|
||||
let mut target = BackgroundTarget::with_identity(target_id, session_id, target_identity);
|
||||
if let Some(pending_auxiliary_page) = pending_auxiliary_page {
|
||||
assert!(
|
||||
target
|
||||
.runtime_slot
|
||||
.stage_pending_auxiliary_page(pending_auxiliary_page),
|
||||
"new popup target must accept its renderer-owned auxiliary Page reservation"
|
||||
);
|
||||
}
|
||||
if let Some(namespace) = session_storage_namespace {
|
||||
target.replace_session_storage_namespace(namespace);
|
||||
}
|
||||
@@ -2284,6 +2296,7 @@ mod tests {
|
||||
source: "globalThis.fromDocumentStart = true".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -2414,6 +2427,7 @@ mod tests {
|
||||
source: "globalThis.demoted = true".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -612,6 +612,7 @@ impl BrowserContext {
|
||||
source,
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: true,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,57 +1,23 @@
|
||||
use super::{CdpConnection, CdpSessionRoute, CommandOwnerScope};
|
||||
use super::{CdpConnection, CommandOwnerScope, TargetPageResidenceIdentity};
|
||||
|
||||
/// Navigation requested by an already-accepted auxiliary browsing-context
|
||||
/// action.
|
||||
/// Immutable authority for one popup destination navigation.
|
||||
///
|
||||
/// Creating or resolving the target is part of the renderer output that
|
||||
/// precedes the causing Runtime response. Loading the requested URL is not.
|
||||
/// Blink's `LocalDOMWindow::open()` resolves the target, invokes `Navigate()`,
|
||||
/// and returns the Window without waiting for the network load or Document
|
||||
/// commit. Moli's navigation helper can itself become asynchronous, so
|
||||
/// protocol projection must hand that work to the owner scheduler instead of
|
||||
/// awaiting it while the opener's output cursor is being projected. Keeping
|
||||
/// the frozen URL and exact target route in this move-only action makes that
|
||||
/// boundary explicit.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PopupTargetNavigationOwnerAction {
|
||||
owner_scope: CommandOwnerScope,
|
||||
/// A target id is not sufficient authorization: CDP sessions and target-local
|
||||
/// state can survive replacement of the installed renderer `Page`. Retaining
|
||||
/// the exact residence generation makes an activation captured for the
|
||||
/// initial empty Document incapable of navigating a later replacement Page.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct PopupTargetNavigationClaimIdentity {
|
||||
page_owner: TargetPageResidenceIdentity,
|
||||
browser_context_id: String,
|
||||
target_id: String,
|
||||
url: String,
|
||||
kind: PopupTargetNavigationKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PopupTargetNavigationKind {
|
||||
InitialDocument,
|
||||
NamedTargetReuse,
|
||||
}
|
||||
|
||||
impl PopupTargetNavigationOwnerAction {
|
||||
pub(crate) fn capture(
|
||||
conn: &CdpConnection,
|
||||
browser_context_id: &str,
|
||||
target_id: &str,
|
||||
url: String,
|
||||
kind: PopupTargetNavigationKind,
|
||||
) -> Option<Self> {
|
||||
let route = conn.target_session_route_for_target_id(target_id)?;
|
||||
(route.browser_context_id() == Some(browser_context_id)).then(|| Self {
|
||||
// Activation has an independent owner action. Unlike a frozen
|
||||
// BackgroundTarget route, AuxiliaryTarget keeps this exact target
|
||||
// addressable if that action changes its residence first.
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
None,
|
||||
Some(CdpSessionRoute::AuxiliaryTarget {
|
||||
browser_context_id: browser_context_id.to_owned(),
|
||||
target_id: target_id.to_owned(),
|
||||
}),
|
||||
),
|
||||
browser_context_id: browser_context_id.to_owned(),
|
||||
target_id: target_id.to_owned(),
|
||||
url,
|
||||
kind,
|
||||
})
|
||||
impl PopupTargetNavigationClaimIdentity {
|
||||
pub(crate) fn page_owner(&self) -> &TargetPageResidenceIdentity {
|
||||
&self.page_owner
|
||||
}
|
||||
|
||||
pub(crate) fn browser_context_id(&self) -> &str {
|
||||
@@ -69,22 +35,177 @@ impl PopupTargetNavigationOwnerAction {
|
||||
pub(crate) fn kind(&self) -> PopupTargetNavigationKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(
|
||||
self,
|
||||
) -> (
|
||||
CommandOwnerScope,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
PopupTargetNavigationKind,
|
||||
) {
|
||||
(
|
||||
self.owner_scope,
|
||||
self.browser_context_id,
|
||||
self.target_id,
|
||||
self.url,
|
||||
self.kind,
|
||||
)
|
||||
/// Navigation requested by an already-accepted auxiliary browsing-context
|
||||
/// action.
|
||||
///
|
||||
/// Creating or resolving the target is part of the renderer output that
|
||||
/// precedes the causing Runtime response. Loading the requested URL is not.
|
||||
/// Blink's `LocalDOMWindow::open()` resolves the target, invokes `Navigate()`,
|
||||
/// and returns the Window without waiting for the network load or Document
|
||||
/// commit. Moli's navigation helper can itself become asynchronous, so
|
||||
/// protocol projection must hand that work to the owner scheduler instead of
|
||||
/// awaiting it while the opener's output cursor is being projected. Keeping
|
||||
/// the frozen URL and exact target route in this move-only action makes that
|
||||
/// boundary explicit.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PopupTargetNavigationOwnerAction {
|
||||
owner_scope: CommandOwnerScope,
|
||||
claim: PopupTargetNavigationClaimIdentity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PopupTargetNavigationKind {
|
||||
InitialDocument,
|
||||
NamedTargetReuse,
|
||||
}
|
||||
|
||||
/// Target-local lifecycle of the initial popup navigation authority.
|
||||
///
|
||||
/// `Consumed` is deliberately retained as a tombstone. Once a captured
|
||||
/// activation is stale, generic entry points such as `Page.enable` must not
|
||||
/// reconstruct it from the mutable target URL and apply it to a newer Page.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum PopupTargetNavigationAuthorityState {
|
||||
Held(PopupTargetNavigationOwnerAction),
|
||||
Published(PopupTargetNavigationClaimIdentity),
|
||||
Consumed(PopupTargetNavigationClaimIdentity),
|
||||
}
|
||||
|
||||
impl PopupTargetNavigationAuthorityState {
|
||||
pub(crate) fn claim_identity(&self) -> &PopupTargetNavigationClaimIdentity {
|
||||
match self {
|
||||
Self::Held(action) => action.claim_identity(),
|
||||
Self::Published(claim) | Self::Consumed(claim) => claim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupTargetNavigationOwnerAction {
|
||||
pub(crate) fn capture(
|
||||
conn: &mut CdpConnection,
|
||||
browser_context_id: &str,
|
||||
target_id: &str,
|
||||
url: String,
|
||||
kind: PopupTargetNavigationKind,
|
||||
) -> Option<Self> {
|
||||
let route = conn.target_session_route_for_target_id(target_id)?;
|
||||
if route.browser_context_id() != Some(browser_context_id) {
|
||||
return None;
|
||||
}
|
||||
let owner_scope =
|
||||
CommandOwnerScope::from_session_and_owner_route(None, Some(route.clone()));
|
||||
let page_owner = {
|
||||
let mut route_scope = conn.scoped_none_session_owner_route_override(route);
|
||||
route_scope
|
||||
.conn_mut()
|
||||
.target_page_residence_identity_for_session(None)?
|
||||
};
|
||||
if page_owner.browser_context_id() != browser_context_id
|
||||
|| page_owner.target_id() != Some(target_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
owner_scope,
|
||||
claim: PopupTargetNavigationClaimIdentity {
|
||||
page_owner,
|
||||
browser_context_id: browser_context_id.to_owned(),
|
||||
target_id: target_id.to_owned(),
|
||||
url,
|
||||
kind,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn browser_context_id(&self) -> &str {
|
||||
self.claim.browser_context_id()
|
||||
}
|
||||
|
||||
pub(crate) fn target_id(&self) -> &str {
|
||||
self.claim.target_id()
|
||||
}
|
||||
|
||||
pub(crate) fn url(&self) -> &str {
|
||||
self.claim.url()
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> PopupTargetNavigationKind {
|
||||
self.claim.kind()
|
||||
}
|
||||
|
||||
pub(crate) fn page_owner(&self) -> &TargetPageResidenceIdentity {
|
||||
self.claim.page_owner()
|
||||
}
|
||||
|
||||
pub(crate) fn claim_identity(&self) -> &PopupTargetNavigationClaimIdentity {
|
||||
&self.claim
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (CommandOwnerScope, PopupTargetNavigationClaimIdentity) {
|
||||
(self.owner_scope, self.claim)
|
||||
}
|
||||
}
|
||||
|
||||
impl CdpConnection {
|
||||
/// Installs the initial popup destination as target-local held authority.
|
||||
/// The action is accepted only while its exact Page residence is current.
|
||||
pub(crate) fn stage_initial_popup_target_navigation_owner_action(
|
||||
&mut self,
|
||||
action: PopupTargetNavigationOwnerAction,
|
||||
) -> bool {
|
||||
if action.kind() != PopupTargetNavigationKind::InitialDocument {
|
||||
return false;
|
||||
}
|
||||
let owner_scope = action.owner_scope.clone();
|
||||
let page_owner = action.page_owner().clone();
|
||||
let mut route_scope = owner_scope.enter(self);
|
||||
let conn = route_scope.conn_mut();
|
||||
if !conn.target_page_residence_identity_is_current_for_session(None, &page_owner) {
|
||||
return false;
|
||||
}
|
||||
conn.runtime_session_owner_slot_mut(None)
|
||||
.is_ok_and(|slot| slot.stage_initial_popup_target_navigation_owner_action(action))
|
||||
}
|
||||
|
||||
/// Releases a held action through the exact target route used at target
|
||||
/// admission. The target slot retains a published claim until completion.
|
||||
pub(crate) fn take_held_popup_target_navigation_owner_action_for_target(
|
||||
&mut self,
|
||||
target_id: &str,
|
||||
) -> Option<PopupTargetNavigationOwnerAction> {
|
||||
let route = self.target_session_route_for_target_id(target_id)?;
|
||||
let mut route_scope = self.scoped_none_session_owner_route_override(route);
|
||||
route_scope
|
||||
.conn_mut()
|
||||
.take_held_popup_target_navigation_owner_action_for_session_owner(None)
|
||||
}
|
||||
|
||||
/// Releases the held action addressed by a concrete Runtime/Page session.
|
||||
pub(crate) fn take_held_popup_target_navigation_owner_action_for_session_owner(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
) -> Option<PopupTargetNavigationOwnerAction> {
|
||||
self.runtime_session_owner_slot_mut(session_id)
|
||||
.ok()?
|
||||
.take_held_initial_popup_target_navigation_owner_action()
|
||||
}
|
||||
|
||||
pub(crate) fn consume_published_popup_target_navigation_claim_for_session_owner(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
expected: &PopupTargetNavigationClaimIdentity,
|
||||
) -> bool {
|
||||
self.runtime_session_owner_slot_mut(session_id)
|
||||
.is_ok_and(|slot| slot.consume_published_popup_target_navigation_claim(expected))
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_session_owner_has_popup_target_navigation_authority(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
self.runtime_session_owner_slot(session_id)
|
||||
.is_ok_and(|slot| slot.has_popup_target_navigation_authority())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::{collections::hash_map::Entry, future::Future, pin::Pin};
|
||||
use std::{
|
||||
collections::{HashSet, hash_map::Entry},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use moli_page_types::{FrontendCommandId, RendererCallId, RendererInspectorResponseDelivery};
|
||||
use moli_protocol_cdp::CdpRendererCommandReplayDispatch;
|
||||
@@ -1791,9 +1795,8 @@ impl CdpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fail_pending_inspector_awaits_for_session_owner_background_events_into(
|
||||
pub(crate) fn fail_claimed_pending_inspector_awaits_for_session_owner_background_events_into(
|
||||
&mut self,
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
claimed_background_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
session_id: Option<&str>,
|
||||
reason: &'static str,
|
||||
@@ -1804,6 +1807,20 @@ impl CdpConnection {
|
||||
claimed,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn fail_pending_inspector_awaits_for_session_owner_background_events_into(
|
||||
&mut self,
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
claimed_background_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
session_id: Option<&str>,
|
||||
reason: &'static str,
|
||||
) {
|
||||
self.fail_claimed_pending_inspector_awaits_for_session_owner_background_events_into(
|
||||
claimed_background_events,
|
||||
session_id,
|
||||
reason,
|
||||
);
|
||||
if let Some(owner_session_id) = session_id
|
||||
&& self.shared_worker_target_for_session(session_id).is_some()
|
||||
{
|
||||
@@ -4274,6 +4291,8 @@ impl CdpConnection {
|
||||
let mut isolated_world_context_count = 0;
|
||||
let mut child_default_context_count = 0;
|
||||
let mut failed_page_snapshot_count = 0;
|
||||
let mut loaded_document_script_agent_ids = HashSet::new();
|
||||
let mut loaded_document_missing_script_agent_id_count = 0;
|
||||
|
||||
for completed in completed.completed {
|
||||
let Some(page) = self
|
||||
@@ -4299,6 +4318,12 @@ impl CdpConnection {
|
||||
failed_page_snapshot_count += 1;
|
||||
continue;
|
||||
};
|
||||
match snapshot.diagnostics.script_agent_id {
|
||||
Some(script_agent_id) => {
|
||||
loaded_document_script_agent_ids.insert(script_agent_id);
|
||||
}
|
||||
None => loaded_document_missing_script_agent_id_count += 1,
|
||||
}
|
||||
document_context_count += snapshot.diagnostics.document_context_count;
|
||||
isolated_world_context_count += snapshot.diagnostics.isolated_world_context_count;
|
||||
child_default_context_count += snapshot.diagnostics.child_default_context_count;
|
||||
@@ -4308,13 +4333,15 @@ impl CdpConnection {
|
||||
.dedicated_worker_running_worker_isolate_count;
|
||||
}
|
||||
|
||||
let estimated_document_isolate_count = self
|
||||
let pending_document_page_build_count = self
|
||||
.browser_contexts()
|
||||
.map(|browser_context| {
|
||||
browser_context.loaded_document_page_count()
|
||||
+ browser_context.pending_document_page_build_count()
|
||||
})
|
||||
.map(BrowserContext::pending_document_page_build_count)
|
||||
.sum::<usize>();
|
||||
let loaded_document_script_agent_count = loaded_document_script_agent_ids.len();
|
||||
let estimated_document_isolate_count = loaded_document_script_agent_count
|
||||
+ loaded_document_missing_script_agent_id_count
|
||||
+ failed_page_snapshot_count
|
||||
+ pending_document_page_build_count;
|
||||
let shared_worker_running_worker_isolate_count = self
|
||||
.browser_contexts()
|
||||
.map(|browser_context| {
|
||||
@@ -4334,6 +4361,12 @@ impl CdpConnection {
|
||||
json!(isolated_world_context_count);
|
||||
diagnostics["isolateScope"]["childDefaultContextCount"] =
|
||||
json!(child_default_context_count);
|
||||
diagnostics["isolateScope"]["loadedDocumentScriptAgentCount"] =
|
||||
json!(loaded_document_script_agent_count);
|
||||
diagnostics["isolateScope"]["loadedDocumentMissingScriptAgentIdCount"] =
|
||||
json!(loaded_document_missing_script_agent_id_count);
|
||||
diagnostics["isolateScope"]["estimatedDocumentIsolateCount"] =
|
||||
json!(estimated_document_isolate_count);
|
||||
diagnostics["isolateScope"]["dedicatedWorkerLoadingCount"] =
|
||||
json!(dedicated_worker_loading_count);
|
||||
diagnostics["isolateScope"]["dedicatedWorkerRunningWorkerIsolateCount"] =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,11 @@ pub(super) struct CdpSchedulerHooks {
|
||||
background_navigation_completion_sender: Option<
|
||||
tokio::sync::mpsc::UnboundedSender<crate::domains::page::BackgroundNavigationCompletion>,
|
||||
>,
|
||||
document_continuation_completion_sender: Option<
|
||||
tokio::sync::mpsc::UnboundedSender<
|
||||
crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
>,
|
||||
>,
|
||||
renderer_publication_sender: Option<RendererOutputTransportSender>,
|
||||
runtime_inspector_response_ready_sender: Option<RuntimeInspectorResponseReadySender>,
|
||||
}
|
||||
@@ -21,6 +26,10 @@ impl CdpSchedulerHooks {
|
||||
self.background_event_sender.clone()
|
||||
}
|
||||
|
||||
pub(super) fn has_background_event_sender(&self) -> bool {
|
||||
self.background_event_sender.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn set_runtime_inspector_response_ready_sender(
|
||||
&mut self,
|
||||
sender: RuntimeInspectorResponseReadySender,
|
||||
@@ -55,6 +64,29 @@ impl CdpSchedulerHooks {
|
||||
self.background_navigation_completion_sender.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn set_document_continuation_completion_sender(
|
||||
&mut self,
|
||||
sender: tokio::sync::mpsc::UnboundedSender<
|
||||
crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
>,
|
||||
) {
|
||||
self.document_continuation_completion_sender = Some(sender);
|
||||
}
|
||||
|
||||
pub(super) fn document_continuation_completion_sender(
|
||||
&self,
|
||||
) -> Option<
|
||||
tokio::sync::mpsc::UnboundedSender<
|
||||
crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
>,
|
||||
> {
|
||||
self.document_continuation_completion_sender.clone()
|
||||
}
|
||||
|
||||
pub(super) fn has_document_continuation_completion_sender(&self) -> bool {
|
||||
self.document_continuation_completion_sender.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn set_renderer_publication_sender(
|
||||
&mut self,
|
||||
sender: RendererOutputTransportSender,
|
||||
|
||||
@@ -7,8 +7,77 @@ use serde_json::{Value, json};
|
||||
|
||||
const RECENT_ACTIVITY_TRACE_LIMIT: usize = 128;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct BackgroundNavigationGateKey {
|
||||
target_id: Option<String>,
|
||||
session_id: Option<String>,
|
||||
frame_id: String,
|
||||
loader_id: String,
|
||||
navigation_request_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl BackgroundNavigationGateKey {
|
||||
pub(crate) fn for_navigation(
|
||||
token: &super::state::DocumentNavigationToken,
|
||||
state: &super::state::NavigationDispatchState,
|
||||
) -> Self {
|
||||
// Keep this key limited to fields that are stable from background task
|
||||
// spawn through lifecycle completion. `navigate_id` is intentionally
|
||||
// excluded because early Page.navigate replies clear it before the
|
||||
// completion is sent.
|
||||
Self {
|
||||
target_id: Some(token.target_id.clone()),
|
||||
session_id: state
|
||||
.navigate_session_id
|
||||
.clone()
|
||||
.or_else(|| state.session_id.clone()),
|
||||
frame_id: state.frame_id.clone(),
|
||||
loader_id: token.loader_id.clone(),
|
||||
navigation_request_id: Some(token.request_id.get()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports whether this gate belongs to the renderer/session owner used by
|
||||
/// an incoming CDP command.
|
||||
pub fn matches_session_owner(&self, session_id: Option<&str>) -> bool {
|
||||
self.session_id.as_deref() == session_id
|
||||
}
|
||||
|
||||
pub(crate) fn target_id(&self) -> Option<&str> {
|
||||
self.target_id.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self) -> Option<&str> {
|
||||
self.session_id.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn loader_id(&self) -> &str {
|
||||
&self.loader_id
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub(crate) fn from_test_parts(
|
||||
target_id: Option<String>,
|
||||
session_id: Option<String>,
|
||||
frame_id: String,
|
||||
loader_id: String,
|
||||
navigation_request_id: Option<u64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
target_id,
|
||||
session_id,
|
||||
frame_id,
|
||||
loader_id,
|
||||
navigation_request_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CdpSchedulerEvent {
|
||||
DocumentContinuationStarted {
|
||||
key: BackgroundNavigationGateKey,
|
||||
},
|
||||
ProtocolWorkPublished {
|
||||
work: crate::domains::activity::ProtocolSchedulerWork,
|
||||
},
|
||||
|
||||
@@ -42,6 +42,10 @@ impl NavigationRequestId {
|
||||
"navigation request id",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn get(self) -> u64 {
|
||||
self.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_nonzero_u64(counter: &AtomicU64, name: &str) -> NonZeroU64 {
|
||||
|
||||
@@ -138,7 +138,7 @@ pub(crate) use emulation::{
|
||||
pub(crate) use navigation_outcome::{CompletedDownloadBody, CompletedDownloadBodyArtifact};
|
||||
pub use navigation_outcome::{
|
||||
DownloadNavigation, LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome,
|
||||
NavigationRequestLoadPolicy, TargetInfo,
|
||||
NavigationRequestLoadPolicy, NoCommitResponseNavigation, TargetInfo,
|
||||
};
|
||||
pub(crate) use navigation_outcome::{
|
||||
NETWORK_ERROR_PAGE_URL, NavigationResultProjection, NavigationSourceDocumentSecurityContext,
|
||||
|
||||
@@ -125,6 +125,15 @@ impl TargetNavigationHistoryState {
|
||||
self.pending_update = None;
|
||||
}
|
||||
|
||||
pub(crate) fn retain_pending_initial_empty_document_replacement(&mut self) {
|
||||
if !matches!(
|
||||
self.pending_update,
|
||||
Some(PendingNavigationHistoryUpdate::ReplaceInitialEmptyDocument)
|
||||
) {
|
||||
self.pending_update = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn entry_url(&self, entry_id: i32) -> Option<String> {
|
||||
self.entries
|
||||
.iter()
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::conn::ResponseCommitReady;
|
||||
use crate::devtools_runtime::DevToolsProtocol;
|
||||
use crate::domains::network::{
|
||||
CompletedDocumentProgressTransfer, CompletedDownloadProgressTransfer,
|
||||
CompletedNoCommitResponseProgressTransfer,
|
||||
};
|
||||
|
||||
use super::browser_context::BrowserContext;
|
||||
@@ -193,6 +194,8 @@ pub struct LoadedNavigation {
|
||||
pub initial_runtime_realms: Vec<RendererRuntimeRealmInfo>,
|
||||
pub renderer_output_predecessor: Option<moli_core::RendererOutputFence>,
|
||||
pub(crate) main_document_commit: Option<Arc<RendererMainDocumentCommit>>,
|
||||
pub(crate) document_continuation_observer:
|
||||
Option<moli_core::page::RendererDocumentContinuationObserver>,
|
||||
pub(crate) document_progress_transfer: CompletedDocumentProgressTransfer,
|
||||
pub(crate) navigation_engine: Option<NavigationEngine>,
|
||||
pub(crate) network_error_page: Option<NetworkErrorPageNavigation>,
|
||||
@@ -228,11 +231,24 @@ pub struct DownloadNavigation {
|
||||
pub(crate) progress_transfer: CompletedDownloadProgressTransfer,
|
||||
}
|
||||
|
||||
/// A navigation response that is observable on the network but cannot commit
|
||||
/// a new Document.
|
||||
///
|
||||
/// HTTP 204 and 205 are the current producers. Keeping this distinct from a
|
||||
/// transport failure is important: the response and redirect chain remain
|
||||
/// observable, while the previously committed Document must stay resident.
|
||||
#[derive(Debug)]
|
||||
pub struct NoCommitResponseNavigation {
|
||||
pub final_url: Url,
|
||||
pub(crate) progress_transfer: CompletedNoCommitResponseProgressTransfer,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NavigationLoadOutcome {
|
||||
ResponseCommitReady(Box<ResponseCommitReady>),
|
||||
Loaded(Box<LoadedNavigation>),
|
||||
Download(Box<DownloadNavigation>),
|
||||
NoCommitResponse(Box<NoCommitResponseNavigation>),
|
||||
NetworkFailure(String),
|
||||
}
|
||||
|
||||
@@ -249,6 +265,10 @@ impl NavigationLoadOutcome {
|
||||
Self::Download(Box::new(navigation))
|
||||
}
|
||||
|
||||
pub(crate) fn no_commit_response(navigation: NoCommitResponseNavigation) -> Self {
|
||||
Self::NoCommitResponse(Box::new(navigation))
|
||||
}
|
||||
|
||||
pub(crate) fn network_failure(error_text: String) -> Self {
|
||||
Self::NetworkFailure(error_text)
|
||||
}
|
||||
@@ -260,6 +280,7 @@ impl NavigationLoadOutcome {
|
||||
}
|
||||
Self::Loaded(navigation) => Self::loaded(navigation.with_navigation_engine(engine)),
|
||||
Self::Download(navigation) => Self::Download(navigation),
|
||||
Self::NoCommitResponse(navigation) => Self::NoCommitResponse(navigation),
|
||||
Self::NetworkFailure(error_text) => Self::NetworkFailure(error_text),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ use moli_core::page::{
|
||||
RendererDocumentLifecycleSnapshot, RendererDocumentLifecycleWaitOutcome,
|
||||
RendererDocumentLifecycleWaiter, RendererDocumentToken, RendererFrameToken,
|
||||
RendererLifecycleEpoch, RendererLifecycleEventStamp, RendererLifecycleStartReason,
|
||||
RendererLifecycleTerminationStamp, RendererPageCreationArtifacts,
|
||||
RendererLifecycleTerminationStamp, RendererPageCreationArtifacts, RendererPendingAuxiliaryPage,
|
||||
};
|
||||
use moli_core::runtime::RendererPageReservationToken;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::document_lifecycle_observer::{
|
||||
@@ -381,6 +382,8 @@ pub(crate) struct TargetPageSlot {
|
||||
renderer_document_lifecycle_waiters: Vec<RegisteredRendererDocumentLifecycleWaiter>,
|
||||
root_post_load_observation: Option<RootPostLoadObservation>,
|
||||
initial_document_page_build_completion: Option<watch::Sender<Option<Result<(), String>>>>,
|
||||
auxiliary_browsing_context_id: Option<u64>,
|
||||
initial_document_page_reservation: Option<RendererPageReservationToken>,
|
||||
pending_renderer_page: Option<PendingRendererPageBinding>,
|
||||
}
|
||||
|
||||
@@ -451,6 +454,37 @@ impl TargetPageSlot {
|
||||
self.initial_document_page_build_completion = Some(sender);
|
||||
}
|
||||
|
||||
pub(crate) fn stage_pending_auxiliary_page(
|
||||
&mut self,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
) -> bool {
|
||||
if self.loaded_page.is_some()
|
||||
|| self.loaded_page_absence_reason
|
||||
!= TargetPageAbsenceReason::InitialDocumentPageBuildPending
|
||||
|| self.auxiliary_browsing_context_id.is_some()
|
||||
|| self.initial_document_page_reservation.is_some()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.auxiliary_browsing_context_id = Some(pending.browsing_context_id());
|
||||
self.initial_document_page_reservation = Some(pending.page_reservation());
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn take_initial_document_page_reservation(
|
||||
&mut self,
|
||||
) -> Option<RendererPageReservationToken> {
|
||||
self.initial_document_page_reservation.take()
|
||||
}
|
||||
|
||||
pub(crate) fn auxiliary_browsing_context_id(&self) -> Option<u64> {
|
||||
self.auxiliary_browsing_context_id
|
||||
}
|
||||
|
||||
pub(crate) fn has_staged_initial_document_page_reservation(&self) -> bool {
|
||||
self.initial_document_page_reservation.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn bind_initial_document_page_build_renderer_page(
|
||||
&mut self,
|
||||
renderer_page: RendererPageResidenceIdentity,
|
||||
|
||||
@@ -105,6 +105,13 @@ impl BackgroundTarget {
|
||||
self.session_id.is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn auxiliary_browsing_context_id(&self) -> Option<u64> {
|
||||
self.runtime_slot
|
||||
.page_slot()
|
||||
.auxiliary_browsing_context_id()
|
||||
}
|
||||
|
||||
pub(crate) fn is_session(&self, session_id: &str) -> bool {
|
||||
self.session_id() == Some(session_id)
|
||||
}
|
||||
@@ -1115,6 +1122,15 @@ impl TargetOwnerState {
|
||||
self.navigation_history_state.clear_pending_update();
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_no_commit_response_navigation_history(&mut self) {
|
||||
if self.is_on_initial_empty_document() == Some(true) {
|
||||
self.navigation_history_state
|
||||
.retain_pending_initial_empty_document_replacement();
|
||||
} else {
|
||||
self.navigation_history_state.clear_pending_update();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn navigation_history_entry_url(
|
||||
&mut self,
|
||||
page_snapshot: Option<(String, String)>,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use moli_core::page::{
|
||||
Page, RendererAgentAttachmentId, RendererDevToolsAgentToken, RendererDocumentLifecycleIdentity,
|
||||
RendererRuntimeInspectorMessageBatch, ScriptNetworkOutputItem, SubresourceNetworkRequestHandle,
|
||||
RendererPendingAuxiliaryPage, RendererRuntimeInspectorMessageBatch, ScriptNetworkOutputItem,
|
||||
SubresourceNetworkRequestHandle,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use moli_core::page::{RendererPageDiagnosticsSnapshot, RendererRuntimeObservableSourceSummary};
|
||||
use moli_core::runtime::RendererPageReservationToken;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
conn::{CapturedBody, ConnectionNetworkRequestIdAllocator},
|
||||
conn::{
|
||||
CapturedBody, ConnectionNetworkRequestIdAllocator, PopupTargetNavigationAuthorityState,
|
||||
PopupTargetNavigationClaimIdentity, PopupTargetNavigationOwnerAction,
|
||||
},
|
||||
domains::{
|
||||
log_output_state::{TargetLogOutputQueueState, TargetNetworkLogEntry},
|
||||
network::{
|
||||
@@ -56,6 +61,7 @@ pub(in crate::conn::state) struct TargetNetworkRequestCounters {
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct TargetRuntimeSlot {
|
||||
page_slot: TargetPageSlot,
|
||||
popup_target_navigation_authority: Option<PopupTargetNavigationAuthorityState>,
|
||||
devtools_renderer_channel: DevToolsRendererChannel,
|
||||
pending_renderer_call_replacements: PreparedRendererCallReplacements,
|
||||
javascript_dialog_scope: TargetJavaScriptDialogScope,
|
||||
@@ -151,6 +157,59 @@ impl TargetRuntimeSlot {
|
||||
self.page_slot.has_loaded_page()
|
||||
}
|
||||
|
||||
pub(crate) fn stage_initial_popup_target_navigation_owner_action(
|
||||
&mut self,
|
||||
action: PopupTargetNavigationOwnerAction,
|
||||
) -> bool {
|
||||
if self.popup_target_navigation_authority.is_some() {
|
||||
return false;
|
||||
}
|
||||
self.popup_target_navigation_authority =
|
||||
Some(PopupTargetNavigationAuthorityState::Held(action));
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn take_held_initial_popup_target_navigation_owner_action(
|
||||
&mut self,
|
||||
) -> Option<PopupTargetNavigationOwnerAction> {
|
||||
let authority = self.popup_target_navigation_authority.take()?;
|
||||
match authority {
|
||||
PopupTargetNavigationAuthorityState::Held(action) => {
|
||||
self.popup_target_navigation_authority = Some(
|
||||
PopupTargetNavigationAuthorityState::Published(action.claim_identity().clone()),
|
||||
);
|
||||
Some(action)
|
||||
}
|
||||
authority => {
|
||||
self.popup_target_navigation_authority = Some(authority);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn consume_published_popup_target_navigation_claim(
|
||||
&mut self,
|
||||
expected: &PopupTargetNavigationClaimIdentity,
|
||||
) -> bool {
|
||||
let matches = matches!(
|
||||
self.popup_target_navigation_authority.as_ref(),
|
||||
Some(PopupTargetNavigationAuthorityState::Published(actual)) if actual == expected
|
||||
);
|
||||
if matches {
|
||||
self.popup_target_navigation_authority = Some(
|
||||
PopupTargetNavigationAuthorityState::Consumed(expected.clone()),
|
||||
);
|
||||
}
|
||||
matches
|
||||
}
|
||||
|
||||
pub(crate) fn has_popup_target_navigation_authority(&self) -> bool {
|
||||
self.popup_target_navigation_authority
|
||||
.as_ref()
|
||||
.map(PopupTargetNavigationAuthorityState::claim_identity)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_initial_document_page_build(&self) -> bool {
|
||||
matches!(
|
||||
self.page_slot.loaded_page_absence_reason(),
|
||||
@@ -176,6 +235,19 @@ impl TargetRuntimeSlot {
|
||||
self.page_slot.start_initial_document_page_build();
|
||||
}
|
||||
|
||||
pub(crate) fn stage_pending_auxiliary_page(
|
||||
&mut self,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
) -> bool {
|
||||
self.page_slot.stage_pending_auxiliary_page(pending)
|
||||
}
|
||||
|
||||
pub(crate) fn take_initial_document_page_reservation(
|
||||
&mut self,
|
||||
) -> Option<RendererPageReservationToken> {
|
||||
self.page_slot.take_initial_document_page_reservation()
|
||||
}
|
||||
|
||||
pub(crate) fn bind_initial_document_page_build_renderer_page(
|
||||
&mut self,
|
||||
renderer_page: RendererPageResidenceIdentity,
|
||||
@@ -438,6 +510,27 @@ impl TargetRuntimeSlot {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_loaded_navigation_document_replacement(
|
||||
&mut self,
|
||||
transaction: &CommittedRendererAgentAttachment,
|
||||
) -> Result<Option<RendererAgentAttachment>, DevToolsRendererChannelError> {
|
||||
let current = transaction.current();
|
||||
let page = self
|
||||
.page_slot
|
||||
.loaded_page_mut()
|
||||
.ok_or(DevToolsRendererChannelError::CommittedCandidateMismatch)?;
|
||||
if self.devtools_renderer_channel.current() != Some(current)
|
||||
|| page.renderer_devtools_agent_token() != current.agent_token()
|
||||
{
|
||||
return Err(DevToolsRendererChannelError::CommittedCandidateMismatch);
|
||||
}
|
||||
page.bind_renderer_agent_attachment(current.id());
|
||||
self.javascript_dialog_scope.retire();
|
||||
self.reset_document_output_state();
|
||||
self.ingest_owner_page_observable_output_updates();
|
||||
Ok(transaction.previous())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_loaded_navigation_renderer_attachment(
|
||||
&mut self,
|
||||
page: &mut Page,
|
||||
@@ -604,6 +697,10 @@ impl TargetRuntimeSlot {
|
||||
.loaded_page_absence_reason()
|
||||
.map(TargetPageAbsenceReason::label),
|
||||
"pageAttachmentId": self.page_attachment_id().map(TargetPageAttachmentId::get),
|
||||
"auxiliaryBrowsingContextId": self.page_slot.auxiliary_browsing_context_id(),
|
||||
"hasStagedInitialDocumentPageReservation": self
|
||||
.page_slot
|
||||
.has_staged_initial_document_page_reservation(),
|
||||
"hasPendingDocumentNavigation": self.has_pending_document_navigation(),
|
||||
"rendererChannelClosed": self.devtools_renderer_channel.is_closed(),
|
||||
"rendererChannelHasCurrentAttachment":
|
||||
|
||||
@@ -1274,6 +1274,7 @@ fn committed_document_navigation_state_clears_document_local_runtime_state() {
|
||||
source: "globalThis.fromPreload = true;".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -1333,6 +1334,7 @@ fn target_parking_store_mutates_target_owner_state_with_default_elision() {
|
||||
source: "globalThis.fromPreload = true;".to_owned(),
|
||||
world_name: Some("utility".to_owned()),
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -1540,6 +1542,11 @@ fn active_target_initial_empty_document_record_tracks_navigation_lifecycle() {
|
||||
.materialized()
|
||||
);
|
||||
|
||||
// Model an auxiliary target whose requested initial navigation must
|
||||
// replace its internally materialized about:blank entry when it finally
|
||||
// commits, even if an earlier response did not commit a Document.
|
||||
context.mark_target_initial_url_replaces_empty_document("TID-initial-active");
|
||||
|
||||
let token = context
|
||||
.start_document_navigation_for_active_target("LOADER-initial-active".to_owned())
|
||||
.expect("active target should start document navigation");
|
||||
@@ -1555,6 +1562,10 @@ fn active_target_initial_empty_document_record_tracks_navigation_lifecycle() {
|
||||
Some("TID-initial-active"),
|
||||
"LOADER-initial-active",
|
||||
);
|
||||
context
|
||||
.active_target
|
||||
.owner_state
|
||||
.resolve_no_commit_response_navigation_history();
|
||||
let cleared = context
|
||||
.active_target
|
||||
.owner_state
|
||||
@@ -1566,6 +1577,13 @@ fn active_target_initial_empty_document_record_tracks_navigation_lifecycle() {
|
||||
let committed = context
|
||||
.start_document_navigation_for_active_target("LOADER-initial-active-2".to_owned())
|
||||
.expect("active target should restart document navigation");
|
||||
context
|
||||
.active_target
|
||||
.owner_state
|
||||
.record_loaded_page_navigation_history((
|
||||
"https://example.test/committed".to_owned(),
|
||||
"committed".to_owned(),
|
||||
));
|
||||
context.commit_document_navigation_if_matches(&committed);
|
||||
let exited = context
|
||||
.active_target
|
||||
@@ -1579,6 +1597,13 @@ fn active_target_initial_empty_document_record_tracks_navigation_lifecycle() {
|
||||
!context.accepts_pending_document_navigation_event(&token),
|
||||
"cleared navigation must stay rejected"
|
||||
);
|
||||
let (current_index, entries) = context
|
||||
.active_target
|
||||
.owner_state
|
||||
.navigation_history_snapshot(None);
|
||||
assert_eq!(current_index, 0);
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].url, "https://example.test/committed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::devtools_runtime::{
|
||||
DevToolsTargetId, NavigationFrameEvent, NavigationFrameEventKind,
|
||||
};
|
||||
use crate::domains::network::{
|
||||
FailedNavigationDocumentPolicy, FailedNavigationResponseMode,
|
||||
FailedNavigationDocumentPolicy, FailedNavigationHistoryPolicy, FailedNavigationResponseMode,
|
||||
MaterializedFailedDocumentProgress, MaterializedNavigationLoadOutcome,
|
||||
empty_main_document_progress_gate_for_test,
|
||||
};
|
||||
@@ -249,8 +249,12 @@ fn background_navigation_completion_sender_routes_explicit_session_owners() {
|
||||
inactive.attach_active_session("SID-inactive");
|
||||
conn.inactive_browser_contexts.push(inactive);
|
||||
|
||||
let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
conn.set_background_navigation_completion_sender(sender);
|
||||
let (event_sender, _event_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (completion_sender, _completion_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (continuation_sender, _continuation_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
conn.set_background_event_sender(event_sender);
|
||||
conn.set_background_navigation_completion_sender(completion_sender);
|
||||
conn.set_document_continuation_completion_sender(continuation_sender);
|
||||
|
||||
assert!(
|
||||
conn.background_navigation_completion_sender_for_session_owner(Some("SID-active"))
|
||||
@@ -414,7 +418,7 @@ async fn removing_active_browser_context_switches_engine_to_promoted_context() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn memory_diagnostics_reports_page_vm_document_isolate_model() {
|
||||
async fn memory_diagnostics_reports_script_agent_document_isolate_model() {
|
||||
let mut conn = CdpConnection::new();
|
||||
conn.replace_navigation_engine(
|
||||
NavigationEngine::new_with_page_vm_document_isolate_for_diagnostics(),
|
||||
@@ -465,7 +469,7 @@ async fn memory_diagnostics_reports_page_vm_document_isolate_model() {
|
||||
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["documentIsolateModel"],
|
||||
json!("page-vm")
|
||||
json!("script-agent")
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["loadedDocumentPageCount"],
|
||||
@@ -482,7 +486,7 @@ async fn memory_diagnostics_reports_page_vm_document_isolate_model() {
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["estimatedDocumentIsolateCount"],
|
||||
json!(2),
|
||||
"page-vm diagnostics should count one document isolate per loaded page"
|
||||
"independent Pages should retain distinct document script agents"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["estimatedWorkerIsolateCount"],
|
||||
@@ -1229,6 +1233,7 @@ async fn materialized_navigation_completion_drops_stale_token() {
|
||||
MaterializedNavigationLoadOutcome::Failed(MaterializedFailedDocumentProgress {
|
||||
error_text: "stale navigation should not emit".to_owned(),
|
||||
document_policy: FailedNavigationDocumentPolicy::InvalidateCommittedDocument,
|
||||
history_policy: FailedNavigationHistoryPolicy::DiscardPendingUpdate,
|
||||
response_mode: FailedNavigationResponseMode::ProtocolError,
|
||||
progress_gate: empty_main_document_progress_gate_for_test(),
|
||||
});
|
||||
@@ -1281,6 +1286,7 @@ async fn materialized_navigation_completion_drops_stale_token_without_navigate_i
|
||||
MaterializedNavigationLoadOutcome::Failed(MaterializedFailedDocumentProgress {
|
||||
error_text: "stale navigation should not emit without a navigate id".to_owned(),
|
||||
document_policy: FailedNavigationDocumentPolicy::InvalidateCommittedDocument,
|
||||
history_policy: FailedNavigationHistoryPolicy::DiscardPendingUpdate,
|
||||
response_mode: FailedNavigationResponseMode::ProtocolError,
|
||||
progress_gate: empty_main_document_progress_gate_for_test(),
|
||||
});
|
||||
@@ -1315,6 +1321,7 @@ async fn materialized_navigation_completion_drains_current_token() {
|
||||
MaterializedNavigationLoadOutcome::Failed(MaterializedFailedDocumentProgress {
|
||||
error_text: "current navigation should emit".to_owned(),
|
||||
document_policy: FailedNavigationDocumentPolicy::InvalidateCommittedDocument,
|
||||
history_policy: FailedNavigationHistoryPolicy::DiscardPendingUpdate,
|
||||
response_mode: FailedNavigationResponseMode::ProtocolError,
|
||||
progress_gate: empty_main_document_progress_gate_for_test(),
|
||||
});
|
||||
|
||||
@@ -58,6 +58,9 @@ async fn commit_navigation_outcome_for_test(
|
||||
NavigationLoadOutcome::Download(_) => {
|
||||
panic!("test navigation should not resolve to a download")
|
||||
}
|
||||
NavigationLoadOutcome::NoCommitResponse(_) => {
|
||||
panic!("test navigation should not resolve to a no-commit response")
|
||||
}
|
||||
NavigationLoadOutcome::NetworkFailure(error_text) => {
|
||||
panic!("test navigation should not fail: {error_text}")
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ impl fmt::Debug for ProtocolSchedulerWork {
|
||||
debug
|
||||
.field("browser_context_id", &action.browser_context_id())
|
||||
.field("target_id", &action.target_id())
|
||||
.field("page_owner", &action.page_owner())
|
||||
.field("url", &action.url())
|
||||
.field("navigation_kind", &action.kind());
|
||||
}
|
||||
|
||||
@@ -330,6 +330,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn debugger_instrumentation_navigation_publishes_new_context_with_bound_origin() {
|
||||
let mut ctx = TestContext::new();
|
||||
ctx.enable_document_continuation_scheduler_for_test();
|
||||
with_loaded_document(&mut ctx).await;
|
||||
|
||||
let runtime = command(&mut ctx, json!({"id": 34, "method": "Runtime.enable"}), 34).await;
|
||||
|
||||
@@ -424,6 +424,7 @@ pub(super) async fn complete_tokened_materialized_navigation_into_buffer_async(
|
||||
navigation_state,
|
||||
navigation,
|
||||
&mut command_context,
|
||||
None,
|
||||
))
|
||||
.await;
|
||||
if let Some(predecessor) = command_context.take_renderer_output_predecessor() {
|
||||
|
||||
@@ -443,6 +443,11 @@ async fn continue_with_auth_and_intercept_response_pauses_before_authorized_body
|
||||
json!({ "frameId": "TID-1", "loaderId": LOADER_ID }),
|
||||
Some("SID-1"),
|
||||
);
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some("SID-1"),
|
||||
"authorized response-stage navigation continuation",
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
loaded_page_html_for_test(&mut ctx)
|
||||
.await
|
||||
|
||||
@@ -105,6 +105,11 @@ async fn response_stage_pause_happens_before_navigation_body_eof() {
|
||||
json!({ "frameId": "TID-1", "loaderId": LOADER_ID }),
|
||||
Some("SID-1"),
|
||||
);
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some("SID-1"),
|
||||
"response-stage navigation continuation",
|
||||
)
|
||||
.await;
|
||||
|
||||
{
|
||||
let page = ctx
|
||||
@@ -416,6 +421,11 @@ lateBinding("author-script");
|
||||
ctx.expect_result(36_506, json!({}), Some("SID-1"));
|
||||
let navigate = take_response_by_id(&mut ctx, 36_502);
|
||||
assert_eq!(navigate["result"]["frameId"], json!("TID-1"));
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some("SID-1"),
|
||||
"response-stage configured Document continuation",
|
||||
)
|
||||
.await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 36_507,
|
||||
@@ -890,6 +900,31 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document()
|
||||
.runtime_slot
|
||||
.enable_primary_network_events();
|
||||
ctx.conn.browser_context = Some(bc);
|
||||
ctx.install_navigation_fixture_for_session_owner("about:blank", Some("SID-1"))
|
||||
.await;
|
||||
let target_page_before = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1"))
|
||||
.expect("interleaved navigation target Page residence");
|
||||
let renderer_page_before = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1"))
|
||||
.expect("interleaved navigation renderer Page residence");
|
||||
let page_attachment_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id)
|
||||
.expect("interleaved navigation target Page attachment");
|
||||
let devtools_agent_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::loaded_page)
|
||||
.map(moli_core::page::Page::renderer_devtools_agent_token)
|
||||
.expect("interleaved navigation initial renderer agent");
|
||||
ctx.enable_page_events_for_test(Some("SID-1"));
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 364,
|
||||
@@ -980,6 +1015,35 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document()
|
||||
attachment_before_continue,
|
||||
"a superseded response head must not switch the renderer channel"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1")),
|
||||
Some(target_page_before.clone()),
|
||||
"a superseded response head must preserve the target Page residence"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1")),
|
||||
Some(renderer_page_before),
|
||||
"a superseded response head must preserve the renderer Page residence"
|
||||
);
|
||||
let browser_context = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("interleaved navigation browser context");
|
||||
assert_eq!(
|
||||
browser_context.page_attachment_id(),
|
||||
Some(page_attachment_before)
|
||||
);
|
||||
assert_eq!(
|
||||
browser_context
|
||||
.loaded_page()
|
||||
.expect("superseded navigation must preserve the loaded Page")
|
||||
.renderer_devtools_agent_token(),
|
||||
devtools_agent_before,
|
||||
"a superseded response head must preserve the live renderer agent"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 368,
|
||||
@@ -995,6 +1059,7 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document()
|
||||
current_navigation["result"]["loaderId"],
|
||||
second_pause["params"]["networkId"]
|
||||
);
|
||||
wait_until_frame_stopped_loading(&mut ctx, "TID-1").await;
|
||||
|
||||
let page = ctx
|
||||
.conn
|
||||
@@ -1002,8 +1067,28 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document()
|
||||
.as_ref()
|
||||
.and_then(|bc| bc.loaded_page())
|
||||
.expect("current navigation should commit a page");
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1")),
|
||||
Some(target_page_before),
|
||||
"the current response head must replace the Document in the same target Page"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1")),
|
||||
Some(renderer_page_before),
|
||||
"the current response head must retain the renderer Page residence"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id),
|
||||
Some(page_attachment_before)
|
||||
);
|
||||
assert_eq!(page.renderer_devtools_agent_token(), second_agent);
|
||||
assert_ne!(page.renderer_devtools_agent_token(), first_agent);
|
||||
assert_ne!(page.renderer_devtools_agent_token(), devtools_agent_before);
|
||||
let html = page
|
||||
.serialize_html_async()
|
||||
.await
|
||||
@@ -1389,6 +1474,548 @@ async fn continue_response_can_override_status_and_headers() {
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum NoCommitResponseStageAction {
|
||||
Fulfill,
|
||||
ContinueOverride,
|
||||
ContinueOriginal,
|
||||
}
|
||||
|
||||
async fn assert_response_stage_no_commit_case(
|
||||
ctx: &mut TestContext,
|
||||
navigate_id: u64,
|
||||
terminal_id: u64,
|
||||
url: &str,
|
||||
original_status: u16,
|
||||
effective_status: u16,
|
||||
action: NoCommitResponseStageAction,
|
||||
) {
|
||||
assert!(
|
||||
ctx.sent.is_empty(),
|
||||
"case must start with an empty event queue"
|
||||
);
|
||||
let target_page_before = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1"))
|
||||
.expect("no-commit navigation target Page residence");
|
||||
let renderer_page_before = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1"))
|
||||
.expect("no-commit navigation renderer Page residence");
|
||||
let page_attachment_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id)
|
||||
.expect("no-commit navigation target Page attachment");
|
||||
let devtools_agent_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::loaded_page)
|
||||
.map(moli_core::page::Page::renderer_devtools_agent_token)
|
||||
.expect("no-commit navigation initial renderer agent");
|
||||
let html_before = loaded_page_html_for_test(ctx).await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": navigate_id,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-1",
|
||||
"params": { "url": url }
|
||||
}))
|
||||
.await;
|
||||
|
||||
let paused = take_main_document_request_pause(ctx).await;
|
||||
assert_eq!(paused["params"]["responseStatusCode"], original_status);
|
||||
let fetch_request_id = paused["params"]["requestId"]
|
||||
.as_str()
|
||||
.expect("response-stage Fetch request id")
|
||||
.to_owned();
|
||||
let network_request_id = paused["params"]["networkId"]
|
||||
.as_str()
|
||||
.expect("response-stage Network request id")
|
||||
.to_owned();
|
||||
let prepared_agent = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|browser_context| {
|
||||
browser_context
|
||||
.active_target
|
||||
.fetch_owner
|
||||
.pending_fetch_response_prepared_renderer_agent_for_test(&fetch_request_id)
|
||||
});
|
||||
assert_eq!(
|
||||
prepared_agent.is_some(),
|
||||
!matches!(original_status, 204 | 205),
|
||||
"only a response that can commit should reserve a renderer agent at response pause"
|
||||
);
|
||||
assert!(
|
||||
!ctx.sent
|
||||
.iter()
|
||||
.any(|message| message["id"] == json!(navigate_id)),
|
||||
"Page.navigate must remain pending while the effective response is undecided: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
|
||||
let (method, params) = match action {
|
||||
NoCommitResponseStageAction::Fulfill => (
|
||||
"Fetch.fulfillRequest",
|
||||
json!({
|
||||
"requestId": fetch_request_id,
|
||||
"responseCode": effective_status,
|
||||
"responseHeaders": [
|
||||
{ "name": "content-type", "value": "text/html; charset=utf-8" }
|
||||
]
|
||||
}),
|
||||
),
|
||||
NoCommitResponseStageAction::ContinueOverride => (
|
||||
"Fetch.continueResponse",
|
||||
json!({
|
||||
"requestId": fetch_request_id,
|
||||
"responseCode": effective_status,
|
||||
"responseHeaders": [
|
||||
{ "name": "content-type", "value": "text/html; charset=utf-8" }
|
||||
],
|
||||
"responsePhrase": "No Content"
|
||||
}),
|
||||
),
|
||||
NoCommitResponseStageAction::ContinueOriginal => (
|
||||
"Fetch.continueResponse",
|
||||
json!({ "requestId": fetch_request_id }),
|
||||
),
|
||||
};
|
||||
ctx.process_async(json!({
|
||||
"id": terminal_id,
|
||||
"method": method,
|
||||
"sessionId": "SID-1",
|
||||
"params": params
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(terminal_id, json!({}), Some("SID-1"));
|
||||
|
||||
let navigate_response = take_response_by_id(ctx, navigate_id);
|
||||
assert_eq!(navigate_response["sessionId"], "SID-1");
|
||||
assert_eq!(navigate_response["result"]["frameId"], "TID-1");
|
||||
assert!(navigate_response["result"]["loaderId"].as_str().is_some());
|
||||
assert_eq!(
|
||||
navigate_response["result"]["errorText"], "net::ERR_ABORTED",
|
||||
"unexpected Page.navigate terminal: {navigate_response:?}; remaining={:?}",
|
||||
ctx.sent
|
||||
);
|
||||
assert_eq!(navigate_response["result"]["isDownload"], false);
|
||||
|
||||
let response_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.responseReceived")
|
||||
&& message["params"]["requestId"].as_str() == Some(network_request_id.as_str())
|
||||
&& message["params"]["response"]["status"] == json!(effective_status)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"missing effective {effective_status} response for {url}: {:?}",
|
||||
ctx.sent
|
||||
)
|
||||
});
|
||||
let failed_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.loadingFailed")
|
||||
&& message["params"]["requestId"].as_str() == Some(network_request_id.as_str())
|
||||
&& message["params"]["errorText"] == json!("net::ERR_ABORTED")
|
||||
&& message["params"]["canceled"] == json!(true)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing no-commit loadingFailed for {url}: {:?}", ctx.sent));
|
||||
assert!(
|
||||
response_index < failed_index,
|
||||
"the effective response must be observed before its canceled terminal"
|
||||
);
|
||||
assert!(!ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Network.loadingFinished")
|
||||
&& message["params"]["requestId"].as_str() == Some(network_request_id.as_str())
|
||||
}));
|
||||
assert!(!ctx.sent.iter().any(|message| {
|
||||
(message["method"] == json!("Page.frameNavigated")
|
||||
&& message["params"]["frame"]["url"] == json!(url))
|
||||
|| matches!(
|
||||
message["method"].as_str(),
|
||||
Some("DOM.documentUpdated" | "Page.domContentEventFired" | "Page.loadEventFired")
|
||||
)
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1")),
|
||||
Some(target_page_before),
|
||||
"a no-commit response must retain the target Page residence"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1")),
|
||||
Some(renderer_page_before),
|
||||
"a no-commit response must retain the renderer Page residence"
|
||||
);
|
||||
let browser_context = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("no-commit browser context");
|
||||
assert_eq!(
|
||||
browser_context.page_attachment_id(),
|
||||
Some(page_attachment_before)
|
||||
);
|
||||
assert_eq!(
|
||||
browser_context
|
||||
.loaded_page()
|
||||
.expect("the committed Document must remain installed")
|
||||
.renderer_devtools_agent_token(),
|
||||
devtools_agent_before,
|
||||
"a no-commit response must not replace the Document realm"
|
||||
);
|
||||
assert_eq!(loaded_page_html_for_test(ctx).await, html_before);
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.has_pending_document_navigation_for_session_owner(Some("SID-1")),
|
||||
"the no-commit terminal must clear its pending navigation"
|
||||
);
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("no-commit browser context")
|
||||
.active_target
|
||||
.fetch_owner
|
||||
.pending_fetch_response_transfer_is_pending_for_test(&fetch_request_id),
|
||||
"the response-stage transfer must be consumed exactly once"
|
||||
);
|
||||
ctx.sent.clear();
|
||||
}
|
||||
|
||||
// Chromium sources:
|
||||
// content/browser/renderer_host/navigation_request.cc
|
||||
// third_party/blink/public/devtools_protocol/domains/Fetch.pdl
|
||||
// third_party/blink/web_tests/http/tests/inspector-protocol/fetch/
|
||||
// fetch-continue-response-with-overrides.js
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn response_stage_effective_no_content_statuses_abort_without_committing() {
|
||||
async fn ok_page() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE.as_str(), "text/html")],
|
||||
"<!doctype html><main>network-body</main>",
|
||||
)
|
||||
}
|
||||
|
||||
async fn no_content() -> StatusCode {
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
axum::serve(
|
||||
listener,
|
||||
Router::new()
|
||||
.route("/ok", get(ok_page))
|
||||
.route("/no-content", get(no_content)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let mut ctx = TestContext::new();
|
||||
let mut browser_context = attached_browser_context();
|
||||
browser_context
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.enable_primary_network_events();
|
||||
ctx.conn.browser_context = Some(browser_context);
|
||||
ctx.install_navigation_fixture_for_session_owner("about:blank", Some("SID-1"))
|
||||
.await;
|
||||
ctx.enable_page_events_for_test(Some("SID-1"));
|
||||
ctx.enable_dom_events_for_test(Some("SID-1"));
|
||||
wait_until_scheduler_message(&mut ctx, "initial Page.loadEventFired", |message| {
|
||||
message["method"] == json!("Page.loadEventFired") && message["sessionId"] == json!("SID-1")
|
||||
})
|
||||
.await;
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 43_000,
|
||||
"method": "Fetch.enable",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"patterns": [{
|
||||
"urlPattern": "*",
|
||||
"requestStage": "Response",
|
||||
"resourceType": "Document"
|
||||
}]
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(43_000, json!({}), Some("SID-1"));
|
||||
|
||||
assert_response_stage_no_commit_case(
|
||||
&mut ctx,
|
||||
43_001,
|
||||
43_002,
|
||||
&format!("http://{addr}/ok"),
|
||||
200,
|
||||
204,
|
||||
NoCommitResponseStageAction::Fulfill,
|
||||
)
|
||||
.await;
|
||||
assert_response_stage_no_commit_case(
|
||||
&mut ctx,
|
||||
43_003,
|
||||
43_004,
|
||||
&format!("http://{addr}/ok"),
|
||||
200,
|
||||
205,
|
||||
NoCommitResponseStageAction::ContinueOverride,
|
||||
)
|
||||
.await;
|
||||
assert_response_stage_no_commit_case(
|
||||
&mut ctx,
|
||||
43_005,
|
||||
43_006,
|
||||
&format!("http://{addr}/no-content"),
|
||||
204,
|
||||
204,
|
||||
NoCommitResponseStageAction::ContinueOriginal,
|
||||
)
|
||||
.await;
|
||||
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn response_stage_fulfill_can_replace_original_no_content_with_committable_response() {
|
||||
async fn no_content() -> StatusCode {
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
axum::serve(
|
||||
listener,
|
||||
Router::new().route("/no-content", get(no_content)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let mut ctx = TestContext::new();
|
||||
let mut browser_context = attached_browser_context();
|
||||
browser_context
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.enable_primary_network_events();
|
||||
ctx.conn.browser_context = Some(browser_context);
|
||||
ctx.install_navigation_fixture_for_session_owner("about:blank", Some("SID-1"))
|
||||
.await;
|
||||
let target_page_before = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1"))
|
||||
.expect("fulfilled navigation target Page residence");
|
||||
let renderer_page_before = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1"))
|
||||
.expect("fulfilled navigation renderer Page residence");
|
||||
let page_attachment_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id)
|
||||
.expect("fulfilled navigation target Page attachment");
|
||||
let devtools_agent_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::loaded_page)
|
||||
.map(moli_core::page::Page::renderer_devtools_agent_token)
|
||||
.expect("fulfilled navigation initial renderer agent");
|
||||
ctx.enable_page_events_for_test(Some("SID-1"));
|
||||
ctx.enable_dom_events_for_test(Some("SID-1"));
|
||||
wait_until_scheduler_message(&mut ctx, "initial Page.loadEventFired", |message| {
|
||||
message["method"] == json!("Page.loadEventFired") && message["sessionId"] == json!("SID-1")
|
||||
})
|
||||
.await;
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 43_010,
|
||||
"method": "Fetch.enable",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"patterns": [{
|
||||
"urlPattern": "*",
|
||||
"requestStage": "Response",
|
||||
"resourceType": "Document"
|
||||
}]
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(43_010, json!({}), Some("SID-1"));
|
||||
|
||||
let url = format!("http://{addr}/no-content");
|
||||
ctx.process_async(json!({
|
||||
"id": 43_011,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-1",
|
||||
"params": { "url": url }
|
||||
}))
|
||||
.await;
|
||||
let paused = take_main_document_request_pause(&mut ctx).await;
|
||||
assert_eq!(paused["params"]["responseStatusCode"], 204);
|
||||
let fetch_request_id = paused["params"]["requestId"]
|
||||
.as_str()
|
||||
.expect("response-stage Fetch request id")
|
||||
.to_owned();
|
||||
let network_request_id = paused["params"]["networkId"]
|
||||
.as_str()
|
||||
.expect("response-stage Network request id")
|
||||
.to_owned();
|
||||
assert!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|browser_context| {
|
||||
browser_context
|
||||
.active_target
|
||||
.fetch_owner
|
||||
.pending_fetch_response_prepared_renderer_agent_for_test(&fetch_request_id)
|
||||
})
|
||||
.is_none(),
|
||||
"the original 204 response must not reserve a renderer agent before interception"
|
||||
);
|
||||
|
||||
let synthetic_body = base64::engine::general_purpose::STANDARD
|
||||
.encode("<!doctype html><main>rescued-from-204</main>");
|
||||
ctx.process_async(json!({
|
||||
"id": 43_012,
|
||||
"method": "Fetch.fulfillRequest",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"requestId": fetch_request_id,
|
||||
"responseCode": 200,
|
||||
"responseHeaders": [
|
||||
{ "name": "content-type", "value": "text/html; charset=utf-8" }
|
||||
],
|
||||
"body": synthetic_body
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(43_012, json!({}), Some("SID-1"));
|
||||
ctx.expect_result(
|
||||
43_011,
|
||||
json!({ "frameId": "TID-1", "loaderId": network_request_id }),
|
||||
Some("SID-1"),
|
||||
);
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some("SID-1"),
|
||||
"fulfilled response-stage Document continuation",
|
||||
)
|
||||
.await;
|
||||
wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"fulfilled response-stage Network.loadingFinished",
|
||||
|message| {
|
||||
message["method"] == json!("Network.loadingFinished")
|
||||
&& message["params"]["requestId"] == json!(network_request_id)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Network.responseReceived")
|
||||
&& message["params"]["requestId"] == json!(network_request_id)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing synthetic response: {:?}", ctx.sent));
|
||||
assert_eq!(response["params"]["response"]["status"], 200);
|
||||
assert!(ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Network.loadingFinished")
|
||||
&& message["params"]["requestId"] == json!(network_request_id)
|
||||
}));
|
||||
assert!(!ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Network.loadingFailed")
|
||||
&& message["params"]["requestId"] == json!(network_request_id)
|
||||
}));
|
||||
assert_eq!(
|
||||
ctx.sent
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message["method"] == json!("Page.frameNavigated")
|
||||
&& message["params"]["frame"]["url"] == json!(url)
|
||||
})
|
||||
.count(),
|
||||
1,
|
||||
"the effective 200 response must publish exactly one Document commit"
|
||||
);
|
||||
assert!(
|
||||
ctx.sent
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("DOM.documentUpdated"))
|
||||
);
|
||||
assert!(
|
||||
ctx.sent
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
);
|
||||
assert!(
|
||||
ctx.sent
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.loadEventFired"))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1")),
|
||||
Some(target_page_before),
|
||||
"the fulfilled navigation must retain the target Page residence"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1")),
|
||||
Some(renderer_page_before),
|
||||
"the fulfilled navigation must retain the renderer Page residence"
|
||||
);
|
||||
let browser_context = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("fulfilled navigation browser context");
|
||||
assert_eq!(
|
||||
browser_context.page_attachment_id(),
|
||||
Some(page_attachment_before)
|
||||
);
|
||||
assert_ne!(
|
||||
browser_context
|
||||
.loaded_page()
|
||||
.expect("the synthetic 200 Document must be installed")
|
||||
.renderer_devtools_agent_token(),
|
||||
devtools_agent_before,
|
||||
"the effective 200 response must install a new Document realm"
|
||||
);
|
||||
assert!(
|
||||
loaded_page_html_for_test(&mut ctx)
|
||||
.await
|
||||
.contains("rescued-from-204")
|
||||
);
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.has_pending_document_navigation_for_session_owner(Some("SID-1"))
|
||||
);
|
||||
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn continue_response_header_override_keeps_streaming_parser_body() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
@@ -2819,6 +3446,29 @@ async fn fulfill_request_completes_navigation_with_synthetic_response() {
|
||||
.runtime_slot
|
||||
.enable_primary_network_events();
|
||||
ctx.conn.browser_context = Some(bc);
|
||||
ctx.install_navigation_fixture_for_session_owner("about:blank", Some("SID-1"))
|
||||
.await;
|
||||
let target_page_before = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1"))
|
||||
.expect("fulfilled navigation target Page residence");
|
||||
let renderer_page_before = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1"))
|
||||
.expect("fulfilled navigation renderer Page residence");
|
||||
let page_attachment_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id)
|
||||
.expect("fulfilled navigation target Page attachment");
|
||||
let devtools_agent_before = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::loaded_page)
|
||||
.map(moli_core::page::Page::renderer_devtools_agent_token)
|
||||
.expect("fulfilled navigation initial renderer agent");
|
||||
ctx.enable_page_events_for_test(Some("SID-1"));
|
||||
ctx.enable_dom_events_for_test(Some("SID-1"));
|
||||
|
||||
@@ -2890,6 +3540,36 @@ async fn fulfill_request_completes_navigation_with_synthetic_response() {
|
||||
assert_eq!(ctx.take_one()["method"], "Page.loadEventFired");
|
||||
assert_eq!(ctx.take_one()["method"], "Page.frameStoppedLoading");
|
||||
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1")),
|
||||
Some(target_page_before),
|
||||
"Fetch.fulfillRequest must replace the Document without replacing the target Page"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1")),
|
||||
Some(renderer_page_before),
|
||||
"Fetch.fulfillRequest must retain the renderer Page residence"
|
||||
);
|
||||
let browser_context = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("fulfilled navigation browser context");
|
||||
assert_eq!(
|
||||
browser_context.page_attachment_id(),
|
||||
Some(page_attachment_before)
|
||||
);
|
||||
assert_ne!(
|
||||
browser_context
|
||||
.loaded_page()
|
||||
.expect("fulfilled navigation loaded Page")
|
||||
.renderer_devtools_agent_token(),
|
||||
devtools_agent_before,
|
||||
"Fetch.fulfillRequest must install a new renderer Document agent"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 39,
|
||||
"method": "Network.getResponseBody",
|
||||
|
||||
@@ -92,17 +92,19 @@ pub(crate) use main_document_progress::FailedNavigationDocumentPolicy;
|
||||
pub(crate) use main_document_progress::empty_main_document_progress_gate_for_test;
|
||||
pub(crate) use main_document_progress::{
|
||||
CompletedDocumentProgressTransfer, CompletedDownloadProgressTransfer,
|
||||
CompletedMainDocumentNetworkEvents, FailedNavigationResponseMode,
|
||||
MainDocumentBodyNetworkProgress, MainDocumentBodyProgressSource,
|
||||
MainDocumentProgressBackgroundEventBarrier, MainDocumentProgressGate,
|
||||
MaterializedDownloadDocumentProgress, MaterializedFailedDocumentProgress,
|
||||
MaterializedLoadedDocumentProgress, MaterializedNavigationLoadOutcome,
|
||||
CompletedMainDocumentNetworkEvents, CompletedNoCommitResponseProgressTransfer,
|
||||
FailedNavigationHistoryPolicy, FailedNavigationResponseMode, MainDocumentBodyNetworkProgress,
|
||||
MainDocumentBodyProgressSource, MainDocumentProgressBackgroundEventBarrier,
|
||||
MainDocumentProgressGate, MaterializedDownloadDocumentProgress,
|
||||
MaterializedFailedDocumentProgress, MaterializedLoadedDocumentProgress,
|
||||
MaterializedNavigationLoadOutcome, MaterializedStablePageDocumentProgress,
|
||||
emit_child_document_navigation_network_background_events,
|
||||
emit_fetch_navigation_initial_request_for_pause_background_events,
|
||||
materialize_loaded_navigation_progress,
|
||||
materialize_navigation_failure_preserving_committed_document,
|
||||
materialize_navigation_load_result, record_completed_main_document_response_body,
|
||||
record_failed_main_document_response_body, record_main_document_request_body,
|
||||
materialize_navigation_load_result, materialize_stable_page_navigation_progress,
|
||||
record_completed_main_document_response_body, record_failed_main_document_response_body,
|
||||
record_main_document_request_body,
|
||||
response_stage_main_document_navigation_network_progress,
|
||||
start_observed_main_document_navigation_progress_background_events,
|
||||
};
|
||||
|
||||
@@ -137,6 +137,11 @@ impl MainDocumentProgressGate {
|
||||
Self { queue }
|
||||
}
|
||||
|
||||
pub(super) fn make_body_finished_visible(&mut self) {
|
||||
self.queue
|
||||
.mark_output_visible_until(MainDocumentProgressOutputBoundary::BodyFinishedVisible);
|
||||
}
|
||||
|
||||
fn drain_visible_until_into_background_events(
|
||||
&mut self,
|
||||
boundary: MainDocumentProgressOutputBoundary,
|
||||
|
||||
@@ -18,8 +18,9 @@ use url::Url;
|
||||
|
||||
use crate::conn::{
|
||||
BackgroundEventSender, BackgroundProtocolEvent, CapturedBody, CdpConnection,
|
||||
CompletedDownloadBodyArtifact, DownloadNavigation, LoadedNavigation, NavigationDispatchState,
|
||||
NavigationLoadOutcome, ResponseCommitReady, TargetRuntimeSlot,
|
||||
CommittedStablePageNavigation, CompletedDownloadBodyArtifact, DownloadNavigation,
|
||||
LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome, NoCommitResponseNavigation,
|
||||
ResponseCommitReady, StablePageNavigationCommitTarget, TargetRuntimeSlot,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -41,6 +42,26 @@ pub(crate) struct MaterializedLoadedDocumentProgress {
|
||||
pub(crate) initial_runtime_realms: Vec<RendererRuntimeRealmInfo>,
|
||||
pub(crate) renderer_output_predecessor: Option<RendererOutputFence>,
|
||||
pub(crate) main_document_commit: Option<Arc<RendererMainDocumentCommit>>,
|
||||
pub(crate) document_continuation_observer:
|
||||
Option<moli_core::page::RendererDocumentContinuationObserver>,
|
||||
pub(crate) progress_gate: MainDocumentProgressGate,
|
||||
pub(crate) navigation_engine: Option<NavigationEngine>,
|
||||
pub(crate) network_error_page: Option<crate::conn::NetworkErrorPageNavigation>,
|
||||
}
|
||||
|
||||
pub(crate) struct MaterializedStablePageDocumentProgress {
|
||||
pub(crate) stable_page_target: StablePageNavigationCommitTarget,
|
||||
pub(crate) pending_download: Option<RendererPendingDownloadActivation>,
|
||||
pub(crate) page_creation_artifacts: RendererPageCreationArtifacts,
|
||||
pub(crate) final_url: Url,
|
||||
pub(crate) response_headers: Vec<(String, String)>,
|
||||
pub(crate) response_from_cache: bool,
|
||||
pub(crate) main_document_body: Option<CapturedBody>,
|
||||
pub(crate) initial_runtime_realms: Vec<RendererRuntimeRealmInfo>,
|
||||
pub(crate) renderer_output_predecessor: Option<RendererOutputFence>,
|
||||
pub(crate) main_document_commit: Option<Arc<RendererMainDocumentCommit>>,
|
||||
pub(crate) document_continuation_observer:
|
||||
Option<moli_core::page::RendererDocumentContinuationObserver>,
|
||||
pub(crate) progress_gate: MainDocumentProgressGate,
|
||||
pub(crate) navigation_engine: Option<NavigationEngine>,
|
||||
pub(crate) network_error_page: Option<crate::conn::NetworkErrorPageNavigation>,
|
||||
@@ -55,6 +76,7 @@ pub(crate) struct MaterializedDownloadDocumentProgress {
|
||||
pub(crate) struct MaterializedFailedDocumentProgress {
|
||||
pub(crate) error_text: String,
|
||||
pub(crate) document_policy: FailedNavigationDocumentPolicy,
|
||||
pub(crate) history_policy: FailedNavigationHistoryPolicy,
|
||||
pub(crate) response_mode: FailedNavigationResponseMode,
|
||||
pub(crate) progress_gate: MainDocumentProgressGate,
|
||||
}
|
||||
@@ -71,6 +93,12 @@ pub(crate) enum FailedNavigationDocumentPolicy {
|
||||
PreserveCommittedDocument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FailedNavigationHistoryPolicy {
|
||||
DiscardPendingUpdate,
|
||||
RetainInitialEmptyDocumentReplacement,
|
||||
}
|
||||
|
||||
impl FailedNavigationDocumentPolicy {
|
||||
pub(crate) fn invalidates_committed_document(self) -> bool {
|
||||
matches!(self, Self::InvalidateCommittedDocument)
|
||||
@@ -172,6 +200,41 @@ pub(crate) struct CompletedDownloadProgressTransfer {
|
||||
network_events: CompletedMainDocumentNetworkEvents,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CompletedNoCommitResponseProgressTransfer {
|
||||
network_progress: MainDocumentBodyNetworkProgress,
|
||||
}
|
||||
|
||||
impl CompletedNoCommitResponseProgressTransfer {
|
||||
pub(crate) fn new(network_progress: MainDocumentBodyNetworkProgress) -> Self {
|
||||
Self { network_progress }
|
||||
}
|
||||
|
||||
fn into_progress_gate(
|
||||
self,
|
||||
conn: &mut CdpConnection,
|
||||
state: &NavigationDispatchState,
|
||||
final_url: &Url,
|
||||
error_text: &str,
|
||||
) -> MainDocumentProgressGate {
|
||||
record_failed_main_document_response_body(conn, state, error_text.to_owned());
|
||||
let mut progress_gate =
|
||||
MainDocumentProgressGate::from_queue(completed_no_commit_response_progress_queue(
|
||||
self.network_progress,
|
||||
conn,
|
||||
state,
|
||||
final_url,
|
||||
error_text,
|
||||
));
|
||||
// Unlike a committed Document, a no-commit response has no renderer
|
||||
// lifecycle boundary that can reveal response/body phases later. Its
|
||||
// terminal command turn must release responseReceived and
|
||||
// loadingFailed together, in source order.
|
||||
progress_gate.make_body_finished_visible();
|
||||
progress_gate
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CompletedDownloadProgressBody {
|
||||
Buffered(Vec<u8>),
|
||||
@@ -288,6 +351,7 @@ fn materialize_failed_navigation_progress(
|
||||
MaterializedFailedDocumentProgress {
|
||||
error_text,
|
||||
document_policy,
|
||||
history_policy: FailedNavigationHistoryPolicy::DiscardPendingUpdate,
|
||||
response_mode,
|
||||
progress_gate,
|
||||
}
|
||||
@@ -308,6 +372,7 @@ pub(crate) fn materialize_loaded_navigation_progress(
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
main_document_commit,
|
||||
document_continuation_observer,
|
||||
document_progress_transfer,
|
||||
navigation_engine,
|
||||
network_error_page,
|
||||
@@ -329,6 +394,50 @@ pub(crate) fn materialize_loaded_navigation_progress(
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
main_document_commit,
|
||||
document_continuation_observer,
|
||||
progress_gate,
|
||||
navigation_engine,
|
||||
network_error_page,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn materialize_stable_page_navigation_progress(
|
||||
conn: &mut CdpConnection,
|
||||
state: &NavigationDispatchState,
|
||||
navigation: CommittedStablePageNavigation,
|
||||
) -> MaterializedStablePageDocumentProgress {
|
||||
let CommittedStablePageNavigation {
|
||||
stable_page_target,
|
||||
pending_download,
|
||||
page_creation_artifacts,
|
||||
final_url,
|
||||
response_headers,
|
||||
response_from_cache,
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
main_document_commit,
|
||||
document_continuation_observer,
|
||||
document_progress_transfer,
|
||||
navigation_engine,
|
||||
network_error_page,
|
||||
} = navigation;
|
||||
let main_document_body = document_progress_transfer.captured_body();
|
||||
let progress_gate = match network_error_page.as_ref() {
|
||||
Some(error_page) => network_error_page_progress_gate(conn, state, error_page.error_text()),
|
||||
None => document_progress_transfer.into_progress_gate(conn, state, &final_url),
|
||||
};
|
||||
MaterializedStablePageDocumentProgress {
|
||||
stable_page_target,
|
||||
pending_download,
|
||||
page_creation_artifacts,
|
||||
final_url,
|
||||
response_headers,
|
||||
response_from_cache,
|
||||
main_document_body,
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
main_document_commit,
|
||||
document_continuation_observer,
|
||||
progress_gate,
|
||||
navigation_engine,
|
||||
network_error_page,
|
||||
@@ -352,6 +461,23 @@ fn materialize_navigation_load_outcome(
|
||||
NavigationLoadOutcome::Download(navigation) => MaterializedNavigationLoadOutcome::Download(
|
||||
materialize_download_navigation_progress(conn, state, *navigation),
|
||||
),
|
||||
NavigationLoadOutcome::NoCommitResponse(navigation) => {
|
||||
let NoCommitResponseNavigation {
|
||||
final_url,
|
||||
progress_transfer,
|
||||
} = *navigation;
|
||||
let error_text = moli_fetch::NET_ERR_ABORTED_ERROR_TEXT.to_owned();
|
||||
let progress_gate =
|
||||
progress_transfer.into_progress_gate(conn, state, &final_url, &error_text);
|
||||
MaterializedNavigationLoadOutcome::Failed(MaterializedFailedDocumentProgress {
|
||||
error_text,
|
||||
document_policy: FailedNavigationDocumentPolicy::PreserveCommittedDocument,
|
||||
history_policy:
|
||||
FailedNavigationHistoryPolicy::RetainInitialEmptyDocumentReplacement,
|
||||
response_mode: FailedNavigationResponseMode::CdpErrorTextResult,
|
||||
progress_gate,
|
||||
})
|
||||
}
|
||||
NavigationLoadOutcome::NetworkFailure(error_text) => {
|
||||
MaterializedNavigationLoadOutcome::Failed(materialize_failed_navigation_progress(
|
||||
conn,
|
||||
@@ -502,6 +628,36 @@ fn completed_download_body_progress_queue(
|
||||
)
|
||||
}
|
||||
|
||||
fn completed_no_commit_response_progress_queue(
|
||||
progress: MainDocumentBodyNetworkProgress,
|
||||
conn: &CdpConnection,
|
||||
state: &NavigationDispatchState,
|
||||
final_url: &Url,
|
||||
error_text: &str,
|
||||
) -> MainDocumentProgressQueueHandle {
|
||||
let Some(events) = progress.into_completed_body_events() else {
|
||||
return MainDocumentProgressQueueHandle::from_source(
|
||||
MainDocumentProgressSource::streaming(),
|
||||
);
|
||||
};
|
||||
let network_observed = main_document_network_observed(conn, state.session_id.as_deref());
|
||||
let context = CompletedMainDocumentProgressContext::new(
|
||||
main_document_network_event_session_ids(conn, state.session_id.as_deref()),
|
||||
completed_body_main_document_network_request_id(network_observed, state.request_id.clone()),
|
||||
state.request_announced,
|
||||
state.requested_url.clone(),
|
||||
state.request_method.clone(),
|
||||
state.request_body.clone(),
|
||||
state.request_headers.clone(),
|
||||
state.loader_id.clone(),
|
||||
state.frame_id.clone(),
|
||||
state.timestamp,
|
||||
);
|
||||
MainDocumentProgressQueueHandle::from_source(MainDocumentProgressSource::completed_body(
|
||||
context.no_commit_response_event_batches(&events, final_url, error_text),
|
||||
))
|
||||
}
|
||||
|
||||
fn completed_or_streaming_document_progress_queue(
|
||||
conn: &CdpConnection,
|
||||
state: &NavigationDispatchState,
|
||||
@@ -773,6 +929,12 @@ impl MainDocumentBodyProgressSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_body_failed(&self, error_text: &str) {
|
||||
if let Some(live_source) = self.live_source.as_ref() {
|
||||
live_source.emit_body_failed(error_text);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn body_network_progress_for_completed_events(
|
||||
&self,
|
||||
completed_events: CompletedMainDocumentNetworkEvents,
|
||||
@@ -1221,6 +1383,16 @@ impl MainDocumentLiveNetworkProgressSource {
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_body_failed(&self, error_text: &str) {
|
||||
self.send_progress_events(
|
||||
MainDocumentProgressPhase::BodyFinished,
|
||||
vec![MainDocumentNavigationProgressEvent::LoadingFailed {
|
||||
target: self.progress_target(),
|
||||
error_text: error_text.to_owned(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
fn progress_target(&self) -> MainDocumentProgressEventTarget {
|
||||
MainDocumentProgressEventTarget {
|
||||
session_ids: self.session_ids.clone(),
|
||||
@@ -1599,6 +1771,19 @@ impl CompletedMainDocumentProgressContext {
|
||||
)
|
||||
}
|
||||
|
||||
fn no_commit_response_event_batches(
|
||||
&self,
|
||||
events: &CompletedMainDocumentNetworkEvents,
|
||||
final_url: &Url,
|
||||
error_text: &str,
|
||||
) -> MainDocumentNavigationProgressEventBatches {
|
||||
MainDocumentNavigationProgressEventBatches::new(
|
||||
self.request_and_redirect_progress_events(events),
|
||||
self.response_received_progress_events(events, final_url, 0),
|
||||
self.loading_failed_progress_events(error_text),
|
||||
)
|
||||
}
|
||||
|
||||
fn request_and_redirect_progress_events(
|
||||
&self,
|
||||
events: &CompletedMainDocumentNetworkEvents,
|
||||
@@ -1792,6 +1977,19 @@ impl CompletedMainDocumentProgressContext {
|
||||
}]
|
||||
}
|
||||
|
||||
fn loading_failed_progress_events(
|
||||
&self,
|
||||
error_text: &str,
|
||||
) -> Vec<MainDocumentNavigationProgressEvent> {
|
||||
let Some(target) = self.progress_target() else {
|
||||
return Vec::new();
|
||||
};
|
||||
vec![MainDocumentNavigationProgressEvent::LoadingFailed {
|
||||
target,
|
||||
error_text: error_text.to_owned(),
|
||||
}]
|
||||
}
|
||||
|
||||
fn progress_target(&self) -> Option<MainDocumentProgressEventTarget> {
|
||||
Some(MainDocumentProgressEventTarget {
|
||||
session_ids: self.session_ids.clone(),
|
||||
|
||||
@@ -1372,6 +1372,64 @@ fn progress_output_barrier_drains_source_generated_progress_before_cdp_output()
|
||||
assert_eq!(out[2]["method"], json!("Network.loadingFinished"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_no_commit_response_progress_orders_response_before_canceled_failure() {
|
||||
let final_url = Url::parse("http://example.test/no-content").unwrap();
|
||||
let events = CompletedMainDocumentNetworkEvents::new(
|
||||
"GET".to_owned(),
|
||||
vec![("Accept".to_owned(), "text/html".to_owned())],
|
||||
None,
|
||||
204,
|
||||
vec![("Content-Type".to_owned(), "text/plain".to_owned())],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
let batches = completed_progress_context().no_commit_response_event_batches(
|
||||
&events,
|
||||
&final_url,
|
||||
"net::ERR_ABORTED",
|
||||
);
|
||||
let mut drain =
|
||||
MainDocumentProgressDrain::from_source(MainDocumentProgressSource::completed_body(batches));
|
||||
|
||||
let mut out = drain_into_protocol_messages(&mut drain);
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0]["method"], json!("Network.requestWillBeSent"));
|
||||
|
||||
drain.mark_output_visible_until(MainDocumentProgressOutputBoundary::ResponseMetadataVisible);
|
||||
out = drain_into_protocol_messages(&mut drain);
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0]["method"], json!("Network.responseReceived"));
|
||||
assert_eq!(out[0]["params"]["response"]["status"], json!(204));
|
||||
|
||||
drain.mark_output_visible_until(MainDocumentProgressOutputBoundary::BodyFinishedVisible);
|
||||
out = drain_into_protocol_messages(&mut drain);
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0]["method"], json!("Network.loadingFailed"));
|
||||
assert_eq!(out[0]["params"]["errorText"], json!("net::ERR_ABORTED"));
|
||||
assert_eq!(out[0]["params"]["canceled"], json!(true));
|
||||
|
||||
let batches = completed_progress_context().no_commit_response_event_batches(
|
||||
&events,
|
||||
&final_url,
|
||||
"net::ERR_ABORTED",
|
||||
);
|
||||
let mut gate = MainDocumentProgressGate::new(MainDocumentProgressDrain::from_source(
|
||||
MainDocumentProgressSource::completed_body(batches),
|
||||
));
|
||||
gate.make_body_finished_visible();
|
||||
let mut terminal = Vec::new();
|
||||
MainDocumentProgressBackgroundEventBarrier::background_events(&mut terminal, &mut gate)
|
||||
.drain_progress();
|
||||
let terminal = protocol_messages_from_background_events(terminal);
|
||||
assert_eq!(terminal.len(), 3);
|
||||
assert_eq!(terminal[0]["method"], json!("Network.requestWillBeSent"));
|
||||
assert_eq!(terminal[1]["method"], json!("Network.responseReceived"));
|
||||
assert_eq!(terminal[2]["method"], json!("Network.loadingFailed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_navigation_progress_drain_prequeues_loading_failed() {
|
||||
let target = MainDocumentProgressEventTarget {
|
||||
|
||||
@@ -44,6 +44,19 @@ fn consume_main_document_navigation_start(ctx: &mut TestContext) {
|
||||
assert_eq!(ctx.take_one()["method"], "Page.frameStartedNavigating");
|
||||
assert_eq!(ctx.take_one()["method"], "Page.frameStartedLoading");
|
||||
}
|
||||
async fn wait_for_network_error_document_load(ctx: &mut TestContext, session_id: &str) {
|
||||
wait_until_messages(
|
||||
ctx,
|
||||
Some(session_id),
|
||||
"network error Document stopped loading",
|
||||
|messages| {
|
||||
messages
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.frameStoppedLoading"))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
async fn flush_until_subresource_finished(
|
||||
ctx: &mut TestContext,
|
||||
resource_type: &str,
|
||||
|
||||
@@ -1410,17 +1410,7 @@ async fn first_main_document_transport_failure_commits_error_document() {
|
||||
"params": { "url": url }
|
||||
}))
|
||||
.await;
|
||||
wait_until_messages(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"first error Document load",
|
||||
|messages| {
|
||||
messages
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.loadEventFired"))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
wait_for_network_error_document_load(&mut ctx, "SID-1").await;
|
||||
|
||||
let messages = ctx.take_all();
|
||||
let request_index = messages
|
||||
@@ -1469,7 +1459,6 @@ async fn first_main_document_transport_failure_commits_error_document() {
|
||||
response["result"]["errorText"],
|
||||
failed["params"]["errorText"]
|
||||
);
|
||||
|
||||
let frame_index = messages
|
||||
.iter()
|
||||
.position(|message| {
|
||||
@@ -1645,12 +1634,7 @@ async fn main_document_navigation_failure_emits_one_failed_and_finished_terminal
|
||||
}))
|
||||
.await;
|
||||
|
||||
wait_until_messages(&mut ctx, Some("SID-1"), "error Document load", |messages| {
|
||||
messages
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.loadEventFired"))
|
||||
})
|
||||
.await;
|
||||
wait_for_network_error_document_load(&mut ctx, "SID-1").await;
|
||||
let messages = ctx.take_all();
|
||||
assert!(!messages.iter().any(|message| {
|
||||
matches!(
|
||||
|
||||
@@ -98,10 +98,10 @@ pub(in crate::domains) use main_document_commit::{
|
||||
MainDocumentCommitPreparedOutput, append_renderer_main_document_commit_to_output_sink,
|
||||
project_main_document_commit_async,
|
||||
};
|
||||
pub use navigation::BackgroundNavigationCompletion;
|
||||
#[cfg(test)]
|
||||
pub(crate) use navigation::emit_prepared_child_frame_tree_background_events;
|
||||
pub(crate) use navigation::navigation_cookie_access_report;
|
||||
pub use navigation::{BackgroundDocumentContinuationCompletion, BackgroundNavigationCompletion};
|
||||
pub(crate) use navigation::{
|
||||
MaterializedNavigationCompletion, complete_materialized_navigation_into_buffer_async,
|
||||
emit_prepared_child_frame_activity, push_superseded_navigation_result,
|
||||
@@ -2560,6 +2560,23 @@ mod producer_tests {
|
||||
work
|
||||
}
|
||||
|
||||
fn take_popup_target_navigation_work_for_test(
|
||||
conn: &mut CdpConnection,
|
||||
) -> crate::domains::activity::ProtocolSchedulerWork {
|
||||
let [event]: [crate::conn::CdpSchedulerEvent; 1] = conn
|
||||
.take_scheduler_events()
|
||||
.try_into()
|
||||
.expect("popup activation should publish one concrete scheduler action");
|
||||
let crate::conn::CdpSchedulerEvent::ProtocolWorkPublished { work } = event else {
|
||||
panic!("popup activation must publish a protocol owner action");
|
||||
};
|
||||
assert_eq!(
|
||||
work.kind(),
|
||||
crate::domains::activity::ProtocolSchedulerWorkKind::PopupTargetNavigationOwnerAction
|
||||
);
|
||||
work
|
||||
}
|
||||
|
||||
fn root_document_attachment_for_test(
|
||||
conn: &CdpConnection,
|
||||
session_id: &str,
|
||||
@@ -4807,6 +4824,103 @@ mod producer_tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn popup_navigation_owner_action_rejects_replaced_page_and_cannot_be_rescanned() {
|
||||
let mut conn = CdpConnection::default();
|
||||
conn.set_root_target_discovery_enabled(true);
|
||||
let mut bc = BrowserContext::new("BID-stale-popup".into());
|
||||
bc.set_active_target_id("TID-stale-opener");
|
||||
bc.attach_active_session("SID-stale-opener");
|
||||
conn.browser_context = Some(bc);
|
||||
let page_owner = page_residence_identity_for_test(&mut conn, "SID-stale-opener");
|
||||
let source_document = renderer_document_identity_for_test(1, 1);
|
||||
let popup_url = "data:text/html,%3Cmain%3Estale-popup%3C/main%3E";
|
||||
let mut prepared =
|
||||
ProtocolOutputPayloads::from_slot(super::PagePreparedOutputSlot::from_outputs(
|
||||
super::PagePreparedOutputs::from_popup_activations_for_test(
|
||||
page_owner,
|
||||
vec![RendererPendingPopupActivation::window(
|
||||
source_document,
|
||||
RendererWindowDocumentSource::RootFrame,
|
||||
true,
|
||||
None,
|
||||
popup_url.to_owned(),
|
||||
"_blank".to_owned(),
|
||||
)],
|
||||
),
|
||||
));
|
||||
let mut out = Vec::new();
|
||||
|
||||
super::emit_popup_activity_background_events_async(
|
||||
&mut conn,
|
||||
&mut out,
|
||||
Some("SID-stale-opener"),
|
||||
Some(&mut prepared),
|
||||
)
|
||||
.await;
|
||||
|
||||
let popup_target_id = out
|
||||
.into_iter()
|
||||
.map(BackgroundProtocolEvent::into_protocol_message)
|
||||
.find(|message| message["method"] == json!("Target.targetCreated"))
|
||||
.and_then(|message| {
|
||||
message["params"]["targetInfo"]["targetId"]
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.expect("popup target should be observable before its navigation runs");
|
||||
let work = take_popup_target_navigation_work_for_test(&mut conn);
|
||||
|
||||
let route = conn
|
||||
.target_session_route_for_target_id(&popup_target_id)
|
||||
.expect("popup target should retain an exact route");
|
||||
{
|
||||
let mut route_scope = conn.scoped_none_session_owner_route_override(route);
|
||||
route_scope
|
||||
.conn_mut()
|
||||
.runtime_session_owner_slot_mut(None)
|
||||
.expect("popup target should retain its runtime slot")
|
||||
.replace_page_attachment_id_for_test();
|
||||
}
|
||||
|
||||
let outcome = conn.complete_ready_protocol_scheduler_work_turn(work).await;
|
||||
let (protocol_events, scheduler_events) = outcome.into_parts();
|
||||
assert!(
|
||||
protocol_events.is_empty(),
|
||||
"an activation captured for the retired Page residence must not navigate its replacement: {protocol_events:?}"
|
||||
);
|
||||
assert!(
|
||||
scheduler_events.is_empty(),
|
||||
"rejecting stale popup authority must not manufacture replacement work: {scheduler_events:?}"
|
||||
);
|
||||
|
||||
let route = conn
|
||||
.target_session_route_for_target_id(&popup_target_id)
|
||||
.expect("popup target should remain addressable after stale work is rejected");
|
||||
let mut rescanned_events = Vec::new();
|
||||
let restarted = {
|
||||
let mut route_scope = conn.scoped_none_session_owner_route_override(route);
|
||||
crate::domains::target::start_initial_document_target_url_navigation_if_needed_background_events_async(
|
||||
route_scope.conn_mut(),
|
||||
&mut rescanned_events,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
};
|
||||
assert!(
|
||||
!restarted && rescanned_events.is_empty(),
|
||||
"generic initial-navigation entry points must respect the consumed popup authority tombstone"
|
||||
);
|
||||
assert!(
|
||||
conn.browser_context
|
||||
.as_ref()
|
||||
.and_then(|browser_context| { browser_context.background_target(&popup_target_id) })
|
||||
.and_then(|target| target.loaded_page())
|
||||
.is_some_and(|page| moli_url::is_about_blank(page.final_url())),
|
||||
"neither the stale claim nor a later target-URL scan may navigate the replacement Page"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn popup_activation_publishes_automation_lifecycle_without_cdp_discovery() {
|
||||
let mut conn = CdpConnection::default();
|
||||
|
||||
@@ -47,7 +47,10 @@ use super::{
|
||||
emit_child_frame_document_opened_background_events, emit_child_frame_lifecycle_terminal,
|
||||
emit_child_frame_navigation_commit, emit_navigation_started_background_events,
|
||||
},
|
||||
navigation_commit::{commit_download_navigation_async, commit_loaded_navigation_async},
|
||||
navigation_commit::{
|
||||
commit_download_navigation_async, commit_loaded_navigation_async,
|
||||
commit_stable_page_navigation_async,
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) struct PendingNavigateLoadCommand {
|
||||
@@ -394,6 +397,7 @@ pub(crate) struct MaterializedNavigationCompletion {
|
||||
state: NavigationDispatchState,
|
||||
navigation: network::MaterializedNavigationLoadOutcome,
|
||||
engine: Option<NavigationEngine>,
|
||||
background_document_continuation: Option<BackgroundDocumentContinuationCompletion>,
|
||||
}
|
||||
|
||||
impl MaterializedNavigationCompletion {
|
||||
@@ -407,6 +411,7 @@ impl MaterializedNavigationCompletion {
|
||||
state,
|
||||
navigation,
|
||||
engine: None,
|
||||
background_document_continuation: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +420,14 @@ impl MaterializedNavigationCompletion {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_background_document_continuation(
|
||||
mut self,
|
||||
completion: BackgroundDocumentContinuationCompletion,
|
||||
) -> Self {
|
||||
self.background_document_continuation = Some(completion);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn is_current_for_connection(&self, conn: &CdpConnection) -> bool {
|
||||
conn.accepts_pending_document_navigation_for_session_owner(
|
||||
self.state.navigate_session_id.as_deref(),
|
||||
@@ -441,8 +454,15 @@ impl MaterializedNavigationCompletion {
|
||||
NavigationDispatchState,
|
||||
network::MaterializedNavigationLoadOutcome,
|
||||
Option<NavigationEngine>,
|
||||
Option<BackgroundDocumentContinuationCompletion>,
|
||||
) {
|
||||
(self.token, self.state, self.navigation, self.engine)
|
||||
(
|
||||
self.token,
|
||||
self.state,
|
||||
self.navigation,
|
||||
self.engine,
|
||||
self.background_document_continuation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,6 +557,7 @@ impl BackgroundMainDocumentBodyCompletion {
|
||||
pub enum BackgroundNavigationCompletion {
|
||||
Lifecycle(Box<BackgroundNavigationLifecycleCompletion>),
|
||||
MainDocumentBody(Box<BackgroundMainDocumentBodyCompletion>),
|
||||
DocumentContinuation(Box<BackgroundDocumentContinuationCompletion>),
|
||||
}
|
||||
|
||||
impl BackgroundNavigationCompletion {
|
||||
@@ -544,6 +565,7 @@ impl BackgroundNavigationCompletion {
|
||||
match self {
|
||||
Self::Lifecycle(completion) => completion.state.requested_url.as_str(),
|
||||
Self::MainDocumentBody(completion) => completion.state.requested_url.as_str(),
|
||||
Self::DocumentContinuation(completion) => completion.requested_url.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,8 +573,101 @@ impl BackgroundNavigationCompletion {
|
||||
match self {
|
||||
Self::Lifecycle(_) => "lifecycle",
|
||||
Self::MainDocumentBody(_) => "main_document_body",
|
||||
Self::DocumentContinuation(_) => "document_continuation",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background_navigation_gate_key(
|
||||
&self,
|
||||
) -> Option<crate::conn::BackgroundNavigationGateKey> {
|
||||
match self {
|
||||
Self::DocumentContinuation(completion) => {
|
||||
completion.background_navigation_gate_key.clone()
|
||||
}
|
||||
Self::Lifecycle(_) | Self::MainDocumentBody(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn document_continuation_gate_key(
|
||||
&self,
|
||||
) -> Option<crate::conn::BackgroundNavigationGateKey> {
|
||||
match self {
|
||||
Self::DocumentContinuation(completion) => {
|
||||
Some(completion.document_continuation_gate_key.clone())
|
||||
}
|
||||
Self::Lifecycle(_) | Self::MainDocumentBody(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed terminal for the post-commit renderer continuation.
|
||||
///
|
||||
/// `Page.navigate` may expose its response and install the committed renderer
|
||||
/// attachment before parser/document-start work reaches DOMContentLoaded. The
|
||||
/// navigation gate remains resident until that exact Document reaches the
|
||||
/// milestone or terminates/supersedes. This lets Debugger/Fetch control
|
||||
/// commands resume a commit-time V8 pause without allowing ordinary
|
||||
/// document-observing commands to overtake the continuation.
|
||||
pub struct BackgroundDocumentContinuationCompletion {
|
||||
document_continuation_gate_key: crate::conn::BackgroundNavigationGateKey,
|
||||
background_navigation_gate_key: Option<crate::conn::BackgroundNavigationGateKey>,
|
||||
requested_url: String,
|
||||
renderer_output_predecessor: Option<moli_core::RendererOutputFence>,
|
||||
renderer_page_state: Option<std::sync::Arc<moli_core::page::RendererPageState>>,
|
||||
}
|
||||
|
||||
impl BackgroundDocumentContinuationCompletion {
|
||||
pub(crate) fn new(
|
||||
gate_key: crate::conn::BackgroundNavigationGateKey,
|
||||
requested_url: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
document_continuation_gate_key: gate_key.clone(),
|
||||
background_navigation_gate_key: Some(gate_key),
|
||||
requested_url,
|
||||
renderer_output_predecessor: None,
|
||||
renderer_page_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn for_foreground_navigation(
|
||||
token: &DocumentNavigationToken,
|
||||
state: &NavigationDispatchState,
|
||||
) -> Self {
|
||||
Self {
|
||||
document_continuation_gate_key:
|
||||
crate::conn::BackgroundNavigationGateKey::for_navigation(token, state),
|
||||
background_navigation_gate_key: None,
|
||||
requested_url: state.requested_url.as_str().to_owned(),
|
||||
renderer_output_predecessor: None,
|
||||
renderer_page_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn document_continuation_gate_key(
|
||||
&self,
|
||||
) -> crate::conn::BackgroundNavigationGateKey {
|
||||
self.document_continuation_gate_key.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn with_renderer_completion(
|
||||
mut self,
|
||||
completion: moli_core::page::RendererDocumentContinuationCompletion,
|
||||
) -> Self {
|
||||
let (predecessor, page_state) = completion.into_parts();
|
||||
self.renderer_output_predecessor = predecessor;
|
||||
self.renderer_page_state = page_state;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn into_renderer_completion_parts(
|
||||
self,
|
||||
) -> (
|
||||
Option<moli_core::RendererOutputFence>,
|
||||
Option<std::sync::Arc<moli_core::page::RendererPageState>>,
|
||||
) {
|
||||
(self.renderer_output_predecessor, self.renderer_page_state)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BackgroundNavigationLifecycleCompletion {
|
||||
@@ -605,6 +720,13 @@ impl BackgroundNavigationLifecycleCompletion {
|
||||
self.state.requested_url.as_str()
|
||||
}
|
||||
|
||||
pub(crate) fn continuation_completion(&self) -> BackgroundDocumentContinuationCompletion {
|
||||
BackgroundDocumentContinuationCompletion::new(
|
||||
crate::conn::BackgroundNavigationGateKey::for_navigation(&self.token, &self.state),
|
||||
self.state.requested_url.as_str().to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn ready_elapsed_ms(&self) -> u128 {
|
||||
self.ready_at.elapsed().as_millis()
|
||||
}
|
||||
@@ -620,6 +742,7 @@ impl BackgroundNavigationLifecycleCompletion {
|
||||
conn: &mut CdpConnection,
|
||||
retain_engine: bool,
|
||||
) -> MaterializedNavigationCompletion {
|
||||
let document_continuation = self.continuation_completion();
|
||||
let Self {
|
||||
token,
|
||||
state,
|
||||
@@ -635,7 +758,8 @@ impl BackgroundNavigationLifecycleCompletion {
|
||||
| NavigationLoadOutcome::Loaded(_))
|
||||
);
|
||||
let navigation = network::materialize_navigation_load_result(conn, &state, navigation);
|
||||
let completion = MaterializedNavigationCompletion::new(token, state, navigation);
|
||||
let completion = MaterializedNavigationCompletion::new(token, state, navigation)
|
||||
.with_background_document_continuation(document_continuation);
|
||||
if should_retain_engine {
|
||||
completion.with_navigation_engine(engine)
|
||||
} else {
|
||||
@@ -684,6 +808,10 @@ impl BackgroundNavigationCompletion {
|
||||
response_from_cache,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn document_continuation(completion: BackgroundDocumentContinuationCompletion) -> Self {
|
||||
Self::DocumentContinuation(Box::new(completion))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_navigation_initiator_url(browser_context: &BrowserContext) -> Option<Url> {
|
||||
@@ -3384,64 +3512,158 @@ pub(crate) async fn complete_materialized_navigation_into_buffer_async(
|
||||
state: NavigationDispatchState,
|
||||
navigation: network::MaterializedNavigationLoadOutcome,
|
||||
command_context: &mut crate::conn::CommandDispatchContext,
|
||||
mut background_document_continuation: Option<BackgroundDocumentContinuationCompletion>,
|
||||
) {
|
||||
let navigation_session_id = state.navigate_session_id.clone();
|
||||
let navigation_loader_id = state.loader_id.clone();
|
||||
let document_continuation_gate_key =
|
||||
crate::conn::BackgroundNavigationGateKey::for_navigation(&token, &state);
|
||||
let mut foreground_document_continuation_observer = None;
|
||||
match navigation {
|
||||
network::MaterializedNavigationLoadOutcome::ResponseCommitReady(navigation) => {
|
||||
let navigation = *navigation;
|
||||
let configuration = conn.prepared_document_commit_configuration_for_session_owner(
|
||||
let stable_page_target = navigation.stable_page_target().cloned();
|
||||
let renderer_page = navigation.renderer_page_residence_identity();
|
||||
let candidate = match conn.prepare_renderer_agent_candidate_token_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
navigation.final_url(),
|
||||
);
|
||||
if let Err(error) = navigation.update_commit_configuration(configuration).await {
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
} else {
|
||||
let renderer_page = navigation.renderer_page_residence_identity();
|
||||
let candidate = conn.prepare_renderer_agent_candidate_token_for_session_owner(
|
||||
&token,
|
||||
navigation.renderer_devtools_agent_token(),
|
||||
) {
|
||||
Ok(candidate) => Some(candidate),
|
||||
Err(error) => {
|
||||
tracing::debug!(
|
||||
%error,
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = token.loader_id,
|
||||
"dropping superseded response commit-ready navigation"
|
||||
);
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(candidate) = candidate {
|
||||
let configuration = conn.prepared_document_commit_configuration_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
&token,
|
||||
navigation.renderer_devtools_agent_token(),
|
||||
navigation.final_url(),
|
||||
);
|
||||
match candidate.and_then(|candidate| {
|
||||
conn.commit_renderer_agent_candidate_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
candidate,
|
||||
renderer_page,
|
||||
)
|
||||
}) {
|
||||
if let Err(error) = navigation.update_commit_configuration(configuration).await {
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
} else {
|
||||
match match stable_page_target.as_ref() {
|
||||
Some(expected) => conn
|
||||
.commit_renderer_agent_candidate_for_stable_page_replacement_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
candidate,
|
||||
expected,
|
||||
),
|
||||
None => conn.commit_renderer_agent_candidate_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
candidate,
|
||||
renderer_page,
|
||||
),
|
||||
} {
|
||||
Ok(transaction) => {
|
||||
let permit = navigation.issue_commit_permit();
|
||||
match navigation.commit(permit).await {
|
||||
Ok(navigation) => {
|
||||
let navigation = network::materialize_loaded_navigation_progress(
|
||||
conn, &state, navigation,
|
||||
);
|
||||
commit_loaded_navigation_async(
|
||||
conn,
|
||||
out,
|
||||
Some(&token),
|
||||
state,
|
||||
if let Some(expected) = stable_page_target {
|
||||
let failed_url = navigation.final_url().clone();
|
||||
match conn
|
||||
.commit_stable_page_navigation_document_for_session_owner_async(
|
||||
state.navigate_session_id.as_deref(),
|
||||
&expected,
|
||||
navigation,
|
||||
Some(transaction),
|
||||
command_context,
|
||||
permit,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
if let Err(rollback_error) = conn
|
||||
.rollback_committed_renderer_agent_candidate_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
.await
|
||||
{
|
||||
Ok(navigation) => {
|
||||
let mut navigation =
|
||||
network::materialize_stable_page_navigation_progress(
|
||||
conn, &state, navigation,
|
||||
);
|
||||
foreground_document_continuation_observer =
|
||||
schedule_renderer_document_continuation(
|
||||
conn,
|
||||
&token,
|
||||
&state,
|
||||
&mut background_document_continuation,
|
||||
navigation.document_continuation_observer.take(),
|
||||
);
|
||||
Box::pin(commit_stable_page_navigation_async(
|
||||
conn,
|
||||
out,
|
||||
&token,
|
||||
state,
|
||||
navigation,
|
||||
transaction,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
if error.page_was_preserved() {
|
||||
if let Err(rollback_error) = conn
|
||||
.rollback_committed_renderer_agent_candidate_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
transaction,
|
||||
)
|
||||
{
|
||||
tracing::warn!(
|
||||
%rollback_error,
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
"failed to roll back renderer channel after pre-commit Page replacement failure"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let _ = conn
|
||||
.discard_loaded_page_after_failed_navigation_for_session_owner_async(
|
||||
state.navigate_session_id.as_deref(),
|
||||
&failed_url,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
push_navigation_commit_error(out, &state, error.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match navigation.commit(permit).await {
|
||||
Ok(navigation) => {
|
||||
let mut navigation =
|
||||
network::materialize_loaded_navigation_progress(
|
||||
conn, &state, navigation,
|
||||
);
|
||||
foreground_document_continuation_observer =
|
||||
schedule_renderer_document_continuation(
|
||||
conn,
|
||||
&token,
|
||||
&state,
|
||||
&mut background_document_continuation,
|
||||
navigation.document_continuation_observer.take(),
|
||||
);
|
||||
commit_loaded_navigation_async(
|
||||
conn,
|
||||
out,
|
||||
Some(&token),
|
||||
state,
|
||||
navigation,
|
||||
Some(transaction),
|
||||
command_context,
|
||||
)
|
||||
{
|
||||
tracing::warn!(
|
||||
%rollback_error,
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
"failed to roll back renderer channel after prepared document commit failure"
|
||||
);
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
if let Err(rollback_error) = conn
|
||||
.rollback_committed_renderer_agent_candidate_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
transaction,
|
||||
)
|
||||
{
|
||||
tracing::warn!(
|
||||
%rollback_error,
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
"failed to roll back renderer channel after prepared document commit failure"
|
||||
);
|
||||
}
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
}
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3455,15 +3677,24 @@ pub(crate) async fn complete_materialized_navigation_into_buffer_async(
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
network::MaterializedNavigationLoadOutcome::Loaded(navigation) => {
|
||||
let mut navigation = *navigation;
|
||||
foreground_document_continuation_observer = schedule_renderer_document_continuation(
|
||||
conn,
|
||||
&token,
|
||||
&state,
|
||||
&mut background_document_continuation,
|
||||
navigation.document_continuation_observer.take(),
|
||||
);
|
||||
commit_loaded_navigation_async(
|
||||
conn,
|
||||
out,
|
||||
Some(&token),
|
||||
state,
|
||||
*navigation,
|
||||
navigation,
|
||||
None,
|
||||
command_context,
|
||||
)
|
||||
@@ -3476,15 +3707,25 @@ pub(crate) async fn complete_materialized_navigation_into_buffer_async(
|
||||
commit_download_navigation_async(conn, out, state, navigation, command_context).await;
|
||||
}
|
||||
network::MaterializedNavigationLoadOutcome::Failed(navigation) => {
|
||||
let _ = conn.clear_pending_navigation_history_update_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
);
|
||||
let network::MaterializedFailedDocumentProgress {
|
||||
error_text,
|
||||
document_policy,
|
||||
history_policy,
|
||||
response_mode,
|
||||
progress_gate,
|
||||
} = navigation;
|
||||
match history_policy {
|
||||
network::FailedNavigationHistoryPolicy::DiscardPendingUpdate => {
|
||||
let _ = conn.clear_pending_navigation_history_update_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
);
|
||||
}
|
||||
network::FailedNavigationHistoryPolicy::RetainInitialEmptyDocumentReplacement => {
|
||||
let _ = conn.resolve_no_commit_response_navigation_history_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if document_policy.invalidates_committed_document() {
|
||||
let _ = conn
|
||||
.discard_loaded_page_after_failed_navigation_for_session_owner_async(
|
||||
@@ -3501,6 +3742,10 @@ pub(crate) async fn complete_materialized_navigation_into_buffer_async(
|
||||
.emit_navigation_error_into_buffer(out, &error_text);
|
||||
}
|
||||
}
|
||||
if let Some(completion) = background_document_continuation {
|
||||
let observer = conn.schedule_background_document_continuation_completion(completion, None);
|
||||
debug_assert!(observer.is_none());
|
||||
}
|
||||
let primary_protocol_session_id = conn
|
||||
.runtime_session_owner_primary_session_id(navigation_session_id.as_deref())
|
||||
.or_else(|| navigation_session_id.clone());
|
||||
@@ -3544,6 +3789,47 @@ pub(crate) async fn complete_materialized_navigation_into_buffer_async(
|
||||
navigation_session_id.as_deref(),
|
||||
&navigation_loader_id,
|
||||
);
|
||||
if let Some(observer) = foreground_document_continuation_observer {
|
||||
let (predecessor, page_state) = observer.wait().await.into_parts();
|
||||
let page_state_applied = page_state.is_some_and(|page_state| {
|
||||
conn.apply_renderer_document_continuation_page_state(
|
||||
&document_continuation_gate_key,
|
||||
page_state,
|
||||
)
|
||||
});
|
||||
if page_state_applied {
|
||||
let _ = conn
|
||||
.target_session_owner_navigation_history_snapshot(navigation_session_id.as_deref());
|
||||
if let Some(target_id) = document_continuation_gate_key.target_id() {
|
||||
command_context.protocol_events_mut().extend(
|
||||
conn.exact_target_info_changed_event_plan_for_target_delta(target_id)
|
||||
.into_background_events(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(predecessor) = predecessor {
|
||||
command_context.set_renderer_output_predecessor(predecessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_renderer_document_continuation(
|
||||
conn: &mut CdpConnection,
|
||||
token: &DocumentNavigationToken,
|
||||
state: &NavigationDispatchState,
|
||||
background_completion: &mut Option<BackgroundDocumentContinuationCompletion>,
|
||||
observer: Option<moli_core::page::RendererDocumentContinuationObserver>,
|
||||
) -> Option<moli_core::page::RendererDocumentContinuationObserver> {
|
||||
if let Some(completion) = background_completion.take() {
|
||||
return conn.schedule_background_document_continuation_completion(completion, observer);
|
||||
}
|
||||
if let Some(observer) = observer {
|
||||
return conn.schedule_background_document_continuation_completion(
|
||||
BackgroundDocumentContinuationCompletion::for_foreground_navigation(token, state),
|
||||
Some(observer),
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn push_navigation_commit_error(
|
||||
|
||||
@@ -3,6 +3,7 @@ use url::Url;
|
||||
use crate::conn::{
|
||||
CdpConnection, CommandDispatchContext, CommittedRendererAgentAttachment,
|
||||
DocumentNavigationToken, LoadedNavigationRendererAttachmentCommit, NavigationDispatchState,
|
||||
StablePageNavigationCommitTarget,
|
||||
};
|
||||
use crate::domains::activity::{
|
||||
MainDocumentDownloadNavigationActivity, MainDocumentNavigationActivity,
|
||||
@@ -10,6 +11,7 @@ use crate::domains::activity::{
|
||||
use crate::domains::command_output::CommandOutputBuffer;
|
||||
use crate::domains::network::{
|
||||
MaterializedDownloadDocumentProgress, MaterializedLoadedDocumentProgress,
|
||||
MaterializedStablePageDocumentProgress,
|
||||
};
|
||||
use moli_core::page::{
|
||||
Page, RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind,
|
||||
@@ -41,6 +43,7 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
main_document_commit,
|
||||
document_continuation_observer: _,
|
||||
progress_gate,
|
||||
navigation_engine,
|
||||
network_error_page,
|
||||
@@ -160,6 +163,135 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(super) async fn commit_stable_page_navigation_async(
|
||||
conn: &mut CdpConnection,
|
||||
out: &mut CommandOutputBuffer,
|
||||
token: &DocumentNavigationToken,
|
||||
state: NavigationDispatchState,
|
||||
navigation: MaterializedStablePageDocumentProgress,
|
||||
committed_renderer_attachment: CommittedRendererAgentAttachment,
|
||||
) {
|
||||
let MaterializedStablePageDocumentProgress {
|
||||
stable_page_target,
|
||||
pending_download,
|
||||
page_creation_artifacts,
|
||||
final_url,
|
||||
response_headers,
|
||||
response_from_cache,
|
||||
main_document_body,
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
main_document_commit,
|
||||
document_continuation_observer: _,
|
||||
progress_gate,
|
||||
navigation_engine,
|
||||
network_error_page,
|
||||
} = navigation;
|
||||
let target_url = network_error_page
|
||||
.as_ref()
|
||||
.map(|error_page| error_page.unreachable_url().clone())
|
||||
.unwrap_or_else(|| final_url.clone());
|
||||
let Some(main_document_commit) = main_document_commit else {
|
||||
let error = "stable Page navigation is missing its frozen main Document commit identity";
|
||||
if state.navigate_id.is_some() {
|
||||
out.push_error_after_messages(-32000, error);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = state.loader_id,
|
||||
"{error} after early Page.navigate result"
|
||||
);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let is_network_error_page = network_error_page.is_some();
|
||||
let navigation_session_id = state.navigate_session_id.clone();
|
||||
let (page_creation_artifacts, mut deferred_initial_renderer_document_lifecycle_events) =
|
||||
split_renderer_page_creation_lifecycle_at_load_boundary(page_creation_artifacts);
|
||||
let mut navigation_activity = MainDocumentNavigationActivity::new(
|
||||
state,
|
||||
final_url.clone(),
|
||||
progress_gate,
|
||||
Some(token.clone()),
|
||||
);
|
||||
if let Some(error_page) = network_error_page.as_ref() {
|
||||
navigation_activity =
|
||||
navigation_activity.with_network_error_page_result(error_page.error_text().to_owned());
|
||||
}
|
||||
// Keep the protocol-state transaction off the caller's navigation future:
|
||||
// background target creation already carries the fetch and target-handoff
|
||||
// state on its test thread stack.
|
||||
let Some(_commit) = Box::pin(commit_stable_page_navigation_protocol_state_async(
|
||||
conn,
|
||||
out,
|
||||
token,
|
||||
navigation_activity.state(),
|
||||
&stable_page_target,
|
||||
&final_url,
|
||||
&target_url,
|
||||
&main_document_commit,
|
||||
initial_runtime_realms,
|
||||
committed_renderer_attachment,
|
||||
))
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !is_network_error_page {
|
||||
let _ = conn.commit_main_document_resource_for_session_owner(
|
||||
navigation_session_id.as_deref(),
|
||||
navigation_activity.state().frame_id.clone(),
|
||||
navigation_activity.state().loader_id.clone(),
|
||||
final_url.clone(),
|
||||
response_headers,
|
||||
response_from_cache,
|
||||
main_document_body,
|
||||
);
|
||||
}
|
||||
let (renderer_document_binding, mut initial_renderer_document_lifecycle_events) = conn
|
||||
.bind_renderer_document_lifecycle_for_session_owner(
|
||||
navigation_activity.state().navigate_session_id.as_deref(),
|
||||
page_creation_artifacts,
|
||||
Some(token.clone()),
|
||||
navigation_activity.state().frame_id.clone(),
|
||||
navigation_activity.state().loader_id.clone(),
|
||||
);
|
||||
let load_visibility_barrier_armed = renderer_document_binding.is_some()
|
||||
&& conn.begin_renderer_document_load_visibility_barrier_for_session_owner(
|
||||
navigation_activity.state().navigate_session_id.as_deref(),
|
||||
&navigation_activity.state().loader_id,
|
||||
);
|
||||
if load_visibility_barrier_armed {
|
||||
let (_, visible_events) = conn.ingest_renderer_document_lifecycle_events_for_session_owner(
|
||||
navigation_activity.state().navigate_session_id.as_deref(),
|
||||
std::mem::take(&mut deferred_initial_renderer_document_lifecycle_events),
|
||||
);
|
||||
initial_renderer_document_lifecycle_events.extend(visible_events);
|
||||
}
|
||||
navigation_activity.defer_initial_renderer_document_lifecycle_events_until_load_boundary(
|
||||
deferred_initial_renderer_document_lifecycle_events,
|
||||
);
|
||||
if let Some(engine) = navigation_engine {
|
||||
conn.adopt_loaded_navigation_engine_for_session_owner(
|
||||
navigation_session_id.as_deref(),
|
||||
engine,
|
||||
);
|
||||
}
|
||||
Box::pin(async move {
|
||||
navigation_activity
|
||||
.emit_loaded_navigation_commit_async(
|
||||
conn,
|
||||
out,
|
||||
pending_download,
|
||||
renderer_document_binding,
|
||||
initial_renderer_document_lifecycle_events,
|
||||
renderer_output_predecessor,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
fn split_renderer_page_creation_lifecycle_at_load_boundary(
|
||||
mut artifacts: RendererPageCreationArtifacts,
|
||||
) -> (
|
||||
@@ -557,6 +689,197 @@ async fn restore_and_commit_loaded_navigation_page_async(
|
||||
Some(outcome)
|
||||
}
|
||||
|
||||
async fn commit_stable_page_navigation_protocol_state_async(
|
||||
conn: &mut CdpConnection,
|
||||
out: &mut CommandOutputBuffer,
|
||||
token: &DocumentNavigationToken,
|
||||
state: &NavigationDispatchState,
|
||||
expected: &StablePageNavigationCommitTarget,
|
||||
final_url: &Url,
|
||||
target_url: &Url,
|
||||
main_document_commit: &moli_core::page::RendererMainDocumentCommit,
|
||||
initial_runtime_realms: Vec<RendererRuntimeRealmInfo>,
|
||||
committed_renderer_attachment: CommittedRendererAgentAttachment,
|
||||
) -> Option<LoadedPageCommitOutcome> {
|
||||
if !conn.target_page_residence_identity_is_current_for_session(
|
||||
state.navigate_session_id.as_deref(),
|
||||
expected.target_page(),
|
||||
) || conn
|
||||
.renderer_page_residence_identity_for_session_owner(state.navigate_session_id.as_deref())
|
||||
!= Some(expected.renderer_page())
|
||||
|| committed_renderer_attachment.navigation() != token
|
||||
|| conn.current_renderer_agent_attachment_id_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
) != Some(committed_renderer_attachment.current().id())
|
||||
{
|
||||
tracing::warn!(
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = token.loader_id,
|
||||
"stable Page replacement no longer matches its committed protocol owner"
|
||||
);
|
||||
discard_irreversibly_committed_stable_page_if_current_async(
|
||||
conn, state, expected, final_url,
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
let Some(commit_state) = conn
|
||||
.prepare_loaded_navigation_commit_for_session_owner(state.navigate_session_id.as_deref())
|
||||
else {
|
||||
tracing::warn!(
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = token.loader_id,
|
||||
"stable Page replacement lost its protocol commit state after renderer commit"
|
||||
);
|
||||
discard_irreversibly_committed_stable_page_if_current_async(
|
||||
conn, state, expected, final_url,
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
};
|
||||
let page_commit = match conn.commit_loaded_navigation_document_replacement_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
committed_renderer_attachment,
|
||||
target_url,
|
||||
) {
|
||||
Some(Ok(commit)) => commit,
|
||||
Some(Err(error)) => {
|
||||
if state.navigate_id.is_some() {
|
||||
out.push_error_after_messages(
|
||||
-32000,
|
||||
format!("failed to commit stable Page protocol state: {error}"),
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
%error,
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
"stable Page navigation failed after early Page.navigate result"
|
||||
);
|
||||
}
|
||||
let _ = conn
|
||||
.discard_loaded_page_after_failed_navigation_for_session_owner_async(
|
||||
state.navigate_session_id.as_deref(),
|
||||
final_url,
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = token.loader_id,
|
||||
"stable Page replacement lost its target owner during protocol state commit"
|
||||
);
|
||||
discard_irreversibly_committed_stable_page_if_current_async(
|
||||
conn, state, expected, final_url,
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut claimed_inspector_await_events = Vec::new();
|
||||
for session_id in
|
||||
conn.page_event_session_ids_for_session_owner(state.navigate_session_id.as_deref())
|
||||
{
|
||||
conn.fail_claimed_pending_inspector_awaits_for_session_owner_background_events_into(
|
||||
&mut claimed_inspector_await_events,
|
||||
session_id.as_deref(),
|
||||
"Inspected target navigated or closed",
|
||||
);
|
||||
}
|
||||
out.extend_background_events_after_messages(claimed_inspector_await_events);
|
||||
if let Some(replaced_page_owner) = page_commit.replaced_page_owner.as_ref() {
|
||||
let worker_retirement_events =
|
||||
crate::domains::target::retire_dedicated_worker_targets_for_replaced_page_async(
|
||||
conn,
|
||||
replaced_page_owner,
|
||||
)
|
||||
.await;
|
||||
out.extend_background_events_after_messages(worker_retirement_events);
|
||||
}
|
||||
if conn
|
||||
.commit_loaded_navigation_target_identity_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
main_document_commit,
|
||||
target_url,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
tracing::warn!(
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = token.loader_id,
|
||||
"stable Page replacement lost its target identity commit route"
|
||||
);
|
||||
discard_irreversibly_committed_stable_page_if_current_async(
|
||||
conn, state, expected, final_url,
|
||||
)
|
||||
.await;
|
||||
return None;
|
||||
}
|
||||
if commit_state.runtime_frontend_enabled {
|
||||
let _ = conn.set_renderer_runtime_agent_owns_page_console_api_events_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
true,
|
||||
);
|
||||
}
|
||||
conn.commit_document_navigation_for_session_owner_if_matches(
|
||||
state.navigate_session_id.as_deref(),
|
||||
token,
|
||||
);
|
||||
let preload_channel_execution_context_ids = if conn
|
||||
.target_owner_has_bidi_channel_preload_script_for_session(
|
||||
state.navigate_session_id.as_deref(),
|
||||
) {
|
||||
dedupe_preload_channel_execution_context_ids(
|
||||
initial_runtime_realms
|
||||
.iter()
|
||||
.filter_map(runtime_realm_execution_context_id)
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut preload_channel_listener_events = Vec::new();
|
||||
for execution_context_id in preload_channel_execution_context_ids {
|
||||
Box::pin(
|
||||
crate::domains::runtime::start_bidi_preload_channel_listeners_for_execution_context_background_events_async(
|
||||
conn,
|
||||
state.navigate_session_id.as_deref(),
|
||||
execution_context_id,
|
||||
&mut preload_channel_listener_events,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
out.extend_background_events_after_messages(preload_channel_listener_events);
|
||||
Some(LoadedPageCommitOutcome::default())
|
||||
}
|
||||
|
||||
async fn discard_irreversibly_committed_stable_page_if_current_async(
|
||||
conn: &mut CdpConnection,
|
||||
state: &NavigationDispatchState,
|
||||
expected: &StablePageNavigationCommitTarget,
|
||||
final_url: &Url,
|
||||
) {
|
||||
if !conn.target_page_residence_identity_is_current_for_session(
|
||||
state.navigate_session_id.as_deref(),
|
||||
expected.target_page(),
|
||||
) {
|
||||
tracing::error!(
|
||||
session_id = state.navigate_session_id.as_deref(),
|
||||
loader_id = state.loader_id,
|
||||
"could not retire an irreversibly committed stable Page through its captured target route"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let _ = conn
|
||||
.discard_loaded_page_after_failed_navigation_for_session_owner_async(
|
||||
state.navigate_session_id.as_deref(),
|
||||
final_url,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn runtime_realm_execution_context_id(realm: &RendererRuntimeRealmInfo) -> Option<i64> {
|
||||
runtime_realm_has_native_unique_id(realm).then_some(realm.context_id)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ pub(super) async fn emit_prepared(
|
||||
popup_id,
|
||||
url,
|
||||
target_name,
|
||||
pending_auxiliary_page,
|
||||
session_storage_store,
|
||||
initial_empty_document_storage_key,
|
||||
) = activation.into_parts();
|
||||
@@ -112,6 +113,7 @@ pub(super) async fn emit_prepared(
|
||||
opener,
|
||||
can_access_opener,
|
||||
disposition,
|
||||
pending_auxiliary_page,
|
||||
session_storage_store,
|
||||
initial_empty_document_storage_key,
|
||||
);
|
||||
|
||||
@@ -277,6 +277,7 @@ fn document_start_script_from_add_preload_command(
|
||||
source: source.clone(),
|
||||
world_name: command.world_name.clone(),
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
}),
|
||||
DevToolsPreloadScriptSource::FunctionDeclaration {
|
||||
@@ -301,6 +302,7 @@ fn document_start_script_from_add_preload_command(
|
||||
source: source.source,
|
||||
world_name: command.world_name.clone(),
|
||||
has_bidi_channel_argument,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: source.channel_handoffs,
|
||||
})
|
||||
}
|
||||
@@ -1853,6 +1855,7 @@ mod protocol_neutral_tests {
|
||||
source: "globalThis.ready = true;".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
)];
|
||||
@@ -1877,6 +1880,7 @@ mod protocol_neutral_tests {
|
||||
source: "globalThis.ready = true;".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
)];
|
||||
@@ -1902,6 +1906,7 @@ mod protocol_neutral_tests {
|
||||
source: "globalThis.ready = true;".to_owned(),
|
||||
world_name: Some("utility".to_owned()),
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -1910,6 +1915,7 @@ mod protocol_neutral_tests {
|
||||
source: "globalThis.ready = true;".to_owned(),
|
||||
world_name: Some("utility".to_owned()),
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -1941,6 +1947,7 @@ mod protocol_neutral_tests {
|
||||
source: "globalThis.ready = true;".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -1949,6 +1956,7 @@ mod protocol_neutral_tests {
|
||||
source: "globalThis.ready = true;".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ async fn complete_tokened_materialized_navigation_background_events_async(
|
||||
navigation_state,
|
||||
navigation,
|
||||
&mut command_context,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut plan = output.into_plan();
|
||||
|
||||
@@ -58,6 +58,38 @@ fn assert_runtime_navigation_context_reset(
|
||||
);
|
||||
}
|
||||
|
||||
async fn wait_until_runtime_navigation_context_resets(
|
||||
ctx: &mut TestContext,
|
||||
session_ids: &[&str],
|
||||
frame_id: &str,
|
||||
) {
|
||||
let description = format!(
|
||||
"Runtime context replacement for {} in {frame_id}",
|
||||
session_ids.join(", ")
|
||||
);
|
||||
wait_until_messages(
|
||||
ctx,
|
||||
session_ids.first().copied(),
|
||||
&description,
|
||||
|messages| {
|
||||
session_ids.iter().all(|session_id| {
|
||||
let cleared = messages.iter().any(|message| {
|
||||
message["sessionId"] == json!(session_id)
|
||||
&& message["method"] == json!("Runtime.executionContextsCleared")
|
||||
});
|
||||
let default_created = messages.iter().any(|message| {
|
||||
message["sessionId"] == json!(session_id)
|
||||
&& message["method"] == json!("Runtime.executionContextCreated")
|
||||
&& message["params"]["context"]["auxData"]["isDefault"] == json!(true)
|
||||
&& message["params"]["context"]["auxData"]["frameId"] == json!(frame_id)
|
||||
});
|
||||
cleared && default_created
|
||||
})
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn data_url_commit_applies_preloads_worlds_and_bindings_before_author_script() {
|
||||
let mut ctx = TestContext::new();
|
||||
@@ -126,6 +158,16 @@ async fn data_url_commit_applies_preloads_worlds_and_bindings_before_author_scri
|
||||
take_response_by_id(&mut ctx, 90_104)["result"]["frameId"],
|
||||
json!("TID-1")
|
||||
);
|
||||
wait_until_message(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"data URL named-world execution context",
|
||||
|message| {
|
||||
message["method"] == json!("Runtime.executionContextCreated")
|
||||
&& message["params"]["context"]["name"] == json!("data-world")
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 90_105,
|
||||
@@ -179,7 +221,8 @@ async fn data_url_commit_applies_preloads_worlds_and_bindings_before_author_scri
|
||||
assert_eq!(
|
||||
binding_payloads,
|
||||
vec![json!("named-preload"), json!("author-script")],
|
||||
"data URL named-world preload must run before its first author script"
|
||||
"data URL named-world preload must run before its first author script: sent={:#?}",
|
||||
ctx.sent
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,6 +269,204 @@ async fn page_navigate_file_url_fails_before_navigation_events_or_document_repla
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn cross_document_page_navigate_replaces_realm_in_stable_page_residence() {
|
||||
let mut ctx = TestContext::new();
|
||||
load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank");
|
||||
ensure_initial_document_for_session(&mut ctx, Some("SID-1")).await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 90_110,
|
||||
"method": "Runtime.enable",
|
||||
"sessionId": "SID-1"
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(take_response_by_id(&mut ctx, 90_110)["result"], json!({}));
|
||||
let old_context_id = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Runtime.executionContextCreated")
|
||||
&& message["sessionId"] == json!("SID-1")
|
||||
&& message["params"]["context"]["auxData"]["isDefault"] == json!(true)
|
||||
})
|
||||
.and_then(|message| message["params"]["context"]["id"].as_i64())
|
||||
.expect("initial default execution context id");
|
||||
ctx.process_async(json!({
|
||||
"id": 90_111,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"contextId": old_context_id,
|
||||
"expression": "globalThis.__stablePageOldRealm = 'old'",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 90_111)["result"]["result"]["value"],
|
||||
json!("old")
|
||||
);
|
||||
|
||||
let before_target_page = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1"))
|
||||
.expect("initial target Page residence");
|
||||
let before_renderer_page = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1"))
|
||||
.expect("initial renderer Page residence");
|
||||
let before_page_attachment = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id)
|
||||
.expect("initial target Page attachment");
|
||||
let before_renderer_attachment = ctx
|
||||
.conn
|
||||
.current_renderer_agent_attachment_id_for_session_owner(Some("SID-1"))
|
||||
.expect("initial renderer agent attachment");
|
||||
let (before_devtools_agent, before_runtime) = {
|
||||
let page = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.and_then(|browser_context| {
|
||||
browser_context.active_target.runtime_slot.loaded_page_mut()
|
||||
})
|
||||
.expect("initial loaded Page");
|
||||
let devtools_agent = page.renderer_devtools_agent_token();
|
||||
let runtime = page
|
||||
.runtime_heap_usage_async()
|
||||
.await
|
||||
.expect("initial Page runtime diagnostics")
|
||||
.moli
|
||||
.runtime;
|
||||
(devtools_agent, runtime)
|
||||
};
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 90_112,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"url": "data:text/html,<!doctype html><body><main id='stable-page-replacement'>replacement document</main><script>globalThis.__stablePageNewRealm='new'</script></body>"
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let navigate = take_response_by_id(&mut ctx, 90_112);
|
||||
let replacement_loader_id = navigate["result"]["loaderId"]
|
||||
.as_str()
|
||||
.expect("replacement navigation loader id")
|
||||
.to_owned();
|
||||
wait_until_runtime_navigation_context_resets(&mut ctx, &["SID-1"], "TID-1").await;
|
||||
wait_until_renderer_document_load(&mut ctx, Some("SID-1"), "TID-1", &replacement_loader_id)
|
||||
.await;
|
||||
assert_runtime_navigation_context_reset(&ctx.sent, "SID-1", "TID-1");
|
||||
let new_context_id = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Runtime.executionContextCreated")
|
||||
&& message["sessionId"] == json!("SID-1")
|
||||
&& message["params"]["context"]["auxData"]["isDefault"] == json!(true)
|
||||
})
|
||||
.and_then(|message| message["params"]["context"]["id"].as_i64())
|
||||
.expect("replacement default execution context id");
|
||||
assert_ne!(new_context_id, old_context_id);
|
||||
|
||||
let after_target_page = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1"))
|
||||
.expect("replacement target Page residence");
|
||||
let after_renderer_page = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1"))
|
||||
.expect("replacement renderer Page residence");
|
||||
let after_page_attachment = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(BrowserContext::page_attachment_id)
|
||||
.expect("replacement target Page attachment");
|
||||
let after_renderer_attachment = ctx
|
||||
.conn
|
||||
.current_renderer_agent_attachment_id_for_session_owner(Some("SID-1"))
|
||||
.expect("replacement renderer agent attachment");
|
||||
let (after_devtools_agent, after_runtime) = {
|
||||
let page = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.and_then(|browser_context| {
|
||||
browser_context.active_target.runtime_slot.loaded_page_mut()
|
||||
})
|
||||
.expect("replacement loaded Page");
|
||||
let devtools_agent = page.renderer_devtools_agent_token();
|
||||
let runtime = page
|
||||
.runtime_heap_usage_async()
|
||||
.await
|
||||
.expect("replacement Page runtime diagnostics")
|
||||
.moli
|
||||
.runtime;
|
||||
(devtools_agent, runtime)
|
||||
};
|
||||
|
||||
assert_eq!(after_target_page, before_target_page);
|
||||
assert_eq!(after_renderer_page, before_renderer_page);
|
||||
assert_eq!(after_page_attachment, before_page_attachment);
|
||||
assert_ne!(after_renderer_attachment, before_renderer_attachment);
|
||||
assert_ne!(after_devtools_agent, before_devtools_agent);
|
||||
assert_eq!(
|
||||
after_runtime.script_agent_id,
|
||||
before_runtime.script_agent_id
|
||||
);
|
||||
assert_ne!(
|
||||
after_runtime.inspector_context_group_id,
|
||||
before_runtime.inspector_context_group_id
|
||||
);
|
||||
assert_eq!(
|
||||
after_runtime.main_window_proxy_identity_hash,
|
||||
before_runtime.main_window_proxy_identity_hash
|
||||
);
|
||||
assert!(after_runtime.main_window_proxy_identity_hash.is_some());
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 90_113,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"expression": "JSON.stringify([document.querySelector('#stable-page-replacement').textContent, typeof globalThis.__stablePageOldRealm, globalThis.__stablePageNewRealm])",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 90_113)["result"]["result"]["value"],
|
||||
json!(r#"["replacement document","undefined","new"]"#)
|
||||
);
|
||||
ctx.process_async(json!({
|
||||
"id": 90_114,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"contextId": old_context_id,
|
||||
"expression": "globalThis.__stablePageOldRealm",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let stale_context = take_response_by_id(&mut ctx, 90_114);
|
||||
assert_eq!(stale_context["error"]["code"], json!(-32000));
|
||||
assert_eq!(
|
||||
stale_context["error"]["message"],
|
||||
json!("Cannot find context with specified id")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn renderer_navigation_background_events_keep_typed_sidecars() {
|
||||
let mut ctx = TestContext::new();
|
||||
@@ -3390,6 +3631,8 @@ async fn navigate_after_real_runtime_enable_resets_before_creating_default_conte
|
||||
}))
|
||||
.await;
|
||||
|
||||
wait_until_runtime_navigation_context_resets(&mut ctx, &["SID-1"], "TID-1").await;
|
||||
|
||||
let sent = ctx.take_all();
|
||||
assert_runtime_navigation_context_reset(&sent, "SID-1", "TID-1");
|
||||
}
|
||||
@@ -3433,6 +3676,8 @@ async fn navigate_after_real_runtime_enable_fans_out_context_reset_to_auxiliary_
|
||||
}))
|
||||
.await;
|
||||
|
||||
wait_until_runtime_navigation_context_resets(&mut ctx, &["SID-1", "SID-aux"], "TID-1").await;
|
||||
|
||||
let sent = ctx.take_all();
|
||||
for session_id in ["SID-1", "SID-aux"] {
|
||||
assert_runtime_navigation_context_reset(&sent, session_id, "TID-1");
|
||||
@@ -3470,6 +3715,8 @@ async fn navigate_from_auxiliary_session_keeps_primary_and_auxiliary_runtime_eve
|
||||
}))
|
||||
.await;
|
||||
|
||||
wait_until_runtime_navigation_context_resets(&mut ctx, &["SID-1", "SID-aux"], "TID-1").await;
|
||||
|
||||
let sent = ctx.take_all();
|
||||
for session_id in ["SID-1", "SID-aux"] {
|
||||
assert_runtime_navigation_context_reset(&sent, session_id, "TID-1");
|
||||
@@ -3519,6 +3766,8 @@ async fn navigate_after_auxiliary_runtime_disable_keeps_primary_runtime_enabled(
|
||||
}))
|
||||
.await;
|
||||
|
||||
wait_until_runtime_navigation_context_resets(&mut ctx, &["SID-1"], "TID-1").await;
|
||||
|
||||
let sent = ctx.take_all();
|
||||
assert_runtime_navigation_context_reset(&sent, "SID-1", "TID-1");
|
||||
assert!(
|
||||
@@ -5075,6 +5324,13 @@ async fn navigate_failure_commits_error_document_with_visible_unreachable_url()
|
||||
.accepts_document_body_completion_event(&committed_document_token),
|
||||
"ordinary navigation load failures must invalidate the previously committed document"
|
||||
);
|
||||
wait_until_message(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"network error Document stopped loading",
|
||||
|message| message["method"] == json!("Page.frameStoppedLoading"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let messages = ctx.take_all();
|
||||
let frame_navigated = messages
|
||||
@@ -5117,21 +5373,37 @@ async fn navigate_failure_commits_error_document_with_visible_unreachable_url()
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 233,
|
||||
"method": "Page.getFrameTree",
|
||||
"sessionId": "SID-1"
|
||||
}))
|
||||
.await;
|
||||
let frame_tree = take_response_by_id(&mut ctx, 233);
|
||||
assert_eq!(
|
||||
frame_tree["result"]["frameTree"]["frame"]["url"],
|
||||
NETWORK_ERROR_PAGE_URL
|
||||
);
|
||||
assert_eq!(
|
||||
frame_tree["result"]["frameTree"]["frame"]["unreachableUrl"],
|
||||
unreachable_url
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 234,
|
||||
"method": "Page.getNavigationHistory",
|
||||
"sessionId": "SID-1"
|
||||
}))
|
||||
.await;
|
||||
let history = take_response_by_id(&mut ctx, 233);
|
||||
let history = take_response_by_id(&mut ctx, 234);
|
||||
let current_index = history["result"]["currentIndex"]
|
||||
.as_u64()
|
||||
.expect("current history index") as usize;
|
||||
assert_eq!(
|
||||
history["result"]["entries"][current_index]["url"],
|
||||
unreachable_url
|
||||
history["result"]["entries"][current_index]["url"], unreachable_url,
|
||||
"history should expose the failed requested URL, not the internal error URL"
|
||||
);
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn navigate_failure_creates_runtime_context_and_completes_lifecycle() {
|
||||
async fn navigate_failure_replaces_runtime_context_and_completes_lifecycle() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
drop(listener);
|
||||
@@ -5139,19 +5411,29 @@ async fn navigate_failure_creates_runtime_context_and_completes_lifecycle() {
|
||||
let mut ctx = TestContext::new();
|
||||
load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank");
|
||||
ctx.enable_page_events_for_test(Some("SID-1"));
|
||||
let bc = ctx.conn.browser_context.as_mut().unwrap();
|
||||
bc.devtools_session_state
|
||||
.runtime_session_state
|
||||
.runtime_frontend_enabled = true;
|
||||
bc.devtools_session_state
|
||||
ensure_initial_document_for_session(&mut ctx, Some("SID-1")).await;
|
||||
ctx.process_async(json!({
|
||||
"id": 230,
|
||||
"method": "Runtime.enable",
|
||||
"sessionId": "SID-1"
|
||||
}))
|
||||
.await;
|
||||
let _ = take_response_by_id(&mut ctx, 230);
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.devtools_session_state
|
||||
.page_session_state
|
||||
.page_lifecycle_events = true;
|
||||
ctx.sent.clear();
|
||||
|
||||
let unreachable_url = format!("http://{addr}/missing");
|
||||
ctx.process_async(json!({
|
||||
"id": 232,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-1",
|
||||
"params": { "url": format!("http://{addr}/missing") }
|
||||
"params": { "url": unreachable_url }
|
||||
}))
|
||||
.await;
|
||||
wait_until_message(
|
||||
@@ -5162,8 +5444,8 @@ async fn navigate_failure_creates_runtime_context_and_completes_lifecycle() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = ctx
|
||||
.sent
|
||||
let messages = ctx.take_all();
|
||||
let response = messages
|
||||
.iter()
|
||||
.find(|message| message["id"] == json!(232))
|
||||
.expect("Page.navigate response");
|
||||
@@ -5173,23 +5455,52 @@ async fn navigate_failure_creates_runtime_context_and_completes_lifecycle() {
|
||||
assert_eq!(response["result"]["isDownload"], false);
|
||||
assert!(response["result"]["errorText"].is_string());
|
||||
|
||||
let messages = ctx.take_all();
|
||||
assert!(
|
||||
messages.iter().any(|message| {
|
||||
assert_runtime_navigation_context_reset(&messages, "SID-1", "TID-1");
|
||||
let frame_index = messages
|
||||
.iter()
|
||||
.position(|message| message["method"] == json!("Page.frameNavigated"))
|
||||
.unwrap_or_else(|| panic!("missing error frame commit: {messages:?}"));
|
||||
let default_context_index = messages
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Runtime.executionContextCreated")
|
||||
&& message["params"]["context"]["auxData"]["isDefault"] == json!(true)
|
||||
}),
|
||||
"error Document should create a default runtime context: {messages:?}"
|
||||
);
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing replacement default context: {messages:?}"));
|
||||
let dom_content_loaded_index = messages
|
||||
.iter()
|
||||
.position(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
.unwrap_or_else(|| panic!("missing error Document DCL: {messages:?}"));
|
||||
let load_index = messages
|
||||
.iter()
|
||||
.position(|message| message["method"] == json!("Page.loadEventFired"))
|
||||
.unwrap_or_else(|| panic!("missing error Document load: {messages:?}"));
|
||||
assert!(
|
||||
messages.iter().any(|message| {
|
||||
message["method"] == json!("Page.lifecycleEvent")
|
||||
&& message["params"]["name"] == json!("DOMContentLoaded")
|
||||
}) && messages.iter().any(|message| {
|
||||
message["method"] == json!("Page.lifecycleEvent")
|
||||
&& message["params"]["name"] == json!("load")
|
||||
}),
|
||||
"error Document should complete lifecycle: {messages:?}"
|
||||
frame_index < default_context_index
|
||||
&& default_context_index < dom_content_loaded_index
|
||||
&& dom_content_loaded_index < load_index,
|
||||
"error Document commit/context/lifecycle ordering should match a real replacement Document: {messages:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
messages[frame_index]["params"]["frame"]["url"],
|
||||
NETWORK_ERROR_PAGE_URL
|
||||
);
|
||||
assert_eq!(
|
||||
messages[frame_index]["params"]["frame"]["unreachableUrl"],
|
||||
unreachable_url
|
||||
);
|
||||
assert!(messages.iter().any(|message| {
|
||||
message["method"] == json!("Page.lifecycleEvent")
|
||||
&& message["params"]["name"] == json!("DOMContentLoaded")
|
||||
}));
|
||||
assert!(messages.iter().any(|message| {
|
||||
message["method"] == json!("Page.lifecycleEvent")
|
||||
&& message["params"]["name"] == json!("load")
|
||||
}));
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.frameStoppedLoading"))
|
||||
);
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -6069,6 +6380,29 @@ async fn reload_targets_background_owner_without_promotion() {
|
||||
initial_html.contains(">1<"),
|
||||
"expected first background load to contain counter 1"
|
||||
);
|
||||
let target_page_before = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-background"))
|
||||
.expect("background target Page residence before reload");
|
||||
let renderer_page_before = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-background"))
|
||||
.expect("background renderer Page residence before reload");
|
||||
let (page_attachment_before, devtools_agent_before) = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|browser_context| browser_context.background_target("TID-background"))
|
||||
.map(|target| {
|
||||
(
|
||||
target.page_attachment_id(),
|
||||
target
|
||||
.loaded_page()
|
||||
.expect("background loaded Page before reload")
|
||||
.renderer_devtools_agent_token(),
|
||||
)
|
||||
})
|
||||
.expect("background target before reload");
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
@@ -6090,6 +6424,19 @@ async fn reload_targets_background_owner_without_promotion() {
|
||||
let background = browser_context
|
||||
.background_target_mut("TID-background")
|
||||
.expect("background target should remain parked");
|
||||
assert_eq!(
|
||||
background.page_attachment_id(),
|
||||
page_attachment_before,
|
||||
"background reload must retain its target Page attachment"
|
||||
);
|
||||
assert_ne!(
|
||||
background
|
||||
.loaded_page()
|
||||
.expect("background loaded Page after reload")
|
||||
.renderer_devtools_agent_token(),
|
||||
devtools_agent_before,
|
||||
"background reload must install a new renderer Document agent"
|
||||
);
|
||||
let reloaded_html = background
|
||||
.loaded_page_mut()
|
||||
.expect("loaded background page after reload")
|
||||
@@ -6100,6 +6447,16 @@ async fn reload_targets_background_owner_without_promotion() {
|
||||
reloaded_html.contains(">2<"),
|
||||
"expected background reload html to contain counter 2, got {reloaded_html}"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-background")),
|
||||
Some(target_page_before)
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-background")),
|
||||
Some(renderer_page_before)
|
||||
);
|
||||
|
||||
server.abort();
|
||||
}
|
||||
@@ -6348,6 +6705,8 @@ async fn repeated_http_navigation_after_runtime_enable_replaces_the_context_grou
|
||||
.expect("second HTTP navigation should have a loader")
|
||||
.to_owned();
|
||||
|
||||
wait_until_runtime_navigation_context_resets(&mut ctx, &["SID-1"], "TID-1").await;
|
||||
|
||||
assert_ne!(first_loader_id, second_loader_id);
|
||||
assert_eq!(
|
||||
request_count.load(std::sync::atomic::Ordering::SeqCst),
|
||||
@@ -6546,66 +6905,76 @@ async fn parser_script_location_navigation_suppresses_aborted_document_dcl() {
|
||||
|
||||
let mut ctx = TestContext::new();
|
||||
load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank");
|
||||
ctx.process_async(json!({
|
||||
"id": 251,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-1",
|
||||
"params": { "url": format!("http://{addr}/challenge") }
|
||||
}))
|
||||
.await;
|
||||
let _ = take_response_by_id(&mut ctx, 251);
|
||||
wait_until_messages(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"successor document lifecycle after parser-script navigation",
|
||||
|messages| {
|
||||
messages
|
||||
.iter()
|
||||
.filter(|message| message["method"] == json!("Page.frameNavigated"))
|
||||
.count()
|
||||
>= 2
|
||||
&& messages
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
ctx.process_and_wait_for_response_async(json!({
|
||||
"id": 251,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-1",
|
||||
"params": { "url": format!("http://{addr}/challenge") }
|
||||
}))
|
||||
.await;
|
||||
let _ = take_response_by_id(&mut ctx, 251);
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some("SID-1"),
|
||||
"parser-script successor navigation",
|
||||
)
|
||||
.await;
|
||||
wait_until_messages(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"successor document lifecycle after parser-script navigation",
|
||||
|messages| {
|
||||
messages
|
||||
.iter()
|
||||
.filter(|message| message["method"] == json!("Page.frameNavigated"))
|
||||
.count()
|
||||
>= 2
|
||||
&& messages
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = ctx.take_all();
|
||||
let domcontentloaded = events
|
||||
.iter()
|
||||
.filter(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
domcontentloaded.len(),
|
||||
1,
|
||||
"the synchronously aborted challenge document must not emit DCL: {events:?}"
|
||||
);
|
||||
let final_frame_commit_index = events
|
||||
.iter()
|
||||
.rposition(|message| message["method"] == json!("Page.frameNavigated"))
|
||||
.expect("final document frame commit");
|
||||
let domcontentloaded_index = events
|
||||
.iter()
|
||||
.position(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
.expect("final document DCL");
|
||||
assert!(
|
||||
final_frame_commit_index < domcontentloaded_index,
|
||||
"the only DCL must belong to the final document: {events:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("browser context")
|
||||
.target_url(),
|
||||
format!("http://{addr}/final")
|
||||
);
|
||||
assert!(
|
||||
loaded_page_html_for_test(&mut ctx)
|
||||
.await
|
||||
.contains("final content")
|
||||
);
|
||||
let events = ctx.take_all();
|
||||
let domcontentloaded = events
|
||||
.iter()
|
||||
.filter(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
domcontentloaded.len(),
|
||||
1,
|
||||
"the synchronously aborted challenge document must not emit DCL: {events:?}"
|
||||
);
|
||||
let final_frame_commit_index = events
|
||||
.iter()
|
||||
.rposition(|message| message["method"] == json!("Page.frameNavigated"))
|
||||
.expect("final document frame commit");
|
||||
let domcontentloaded_index = events
|
||||
.iter()
|
||||
.position(|message| message["method"] == json!("Page.domContentEventFired"))
|
||||
.expect("final document DCL");
|
||||
assert!(
|
||||
final_frame_commit_index < domcontentloaded_index,
|
||||
"the only DCL must belong to the final document: {events:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("browser context")
|
||||
.target_url(),
|
||||
format!("http://{addr}/final")
|
||||
);
|
||||
assert!(
|
||||
loaded_page_html_for_test(&mut ctx)
|
||||
.await
|
||||
.contains("final content")
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
server.abort();
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ async fn add_script_to_evaluate_on_new_document_reuses_identifier_for_duplicate_
|
||||
.to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -270,6 +271,7 @@ async fn add_script_to_evaluate_on_new_document_restored_scripts_bump_from_max_i
|
||||
source: "globalThis.__lm_preload = 'seed';".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -327,6 +329,7 @@ async fn add_script_to_evaluate_on_new_document_bump_ignores_non_numeric_identif
|
||||
source: "globalThis.__lm_preload = 'legacy';".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
@@ -344,6 +347,7 @@ async fn add_script_to_evaluate_on_new_document_bump_ignores_non_numeric_identif
|
||||
source: "globalThis.__lm_preload = 'seed';".to_owned(),
|
||||
world_name: None,
|
||||
has_bidi_channel_argument: false,
|
||||
browser_internal: false,
|
||||
bidi_channel_handoffs: Vec::new(),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -4,13 +4,21 @@ use crate::conn::BrowserContext;
|
||||
use crate::testing::TestContext;
|
||||
|
||||
pub(super) async fn with_loaded_document_async(ctx: &mut TestContext, html: &str) {
|
||||
with_loaded_document_for_target_async(ctx, html, "TID-1").await;
|
||||
}
|
||||
|
||||
pub(super) async fn with_loaded_document_for_target_async(
|
||||
ctx: &mut TestContext,
|
||||
html: &str,
|
||||
target_id: &str,
|
||||
) {
|
||||
ctx.conn
|
||||
.insert_browser_context(BrowserContext::new("BID-1".into()));
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context should be installed before loading test page")
|
||||
.set_active_target_id("TID-1".to_owned());
|
||||
.set_active_target_id(target_id.to_owned());
|
||||
let data_url = format!("data:text/html,{html}");
|
||||
ctx.install_navigation_fixture_for_session_owner(&data_url, None)
|
||||
.await;
|
||||
|
||||
@@ -341,16 +341,12 @@ async fn failed_heap_profiler_sampling_command_does_not_create_typed_projection(
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn heap_profiler_sampling_and_tracking_are_restored_on_replacement_page_isolate() {
|
||||
let mut ctx = TestContext::new();
|
||||
with_loaded_document_async(
|
||||
with_loaded_document_for_target_async(
|
||||
&mut ctx,
|
||||
"<!doctype html><script>globalThis.__heapBefore = []</script>",
|
||||
"TID-heap-profiler-restore",
|
||||
)
|
||||
.await;
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context should exist")
|
||||
.set_active_target_id("TID-heap-profiler-restore");
|
||||
|
||||
for command in [
|
||||
json!({"id": 206_840, "method": "HeapProfiler.enable"}),
|
||||
@@ -665,13 +661,13 @@ async fn heap_profiler_moli_diagnostics_reports_connection_state_without_loaded_
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["runtimeGetHeapUsageV8HeapScope"],
|
||||
json!("page-vm-document-isolate"),
|
||||
json!("script-agent-document-isolate"),
|
||||
"diagnostics should label Runtime.getHeapUsage heap scope: {response:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["runtimeGetHeapUsageV8HeapIsTargetLocal"],
|
||||
json!(true),
|
||||
"diagnostics should make PageVM-local heap stats explicit: {response:?}"
|
||||
json!(false),
|
||||
"heap stats may include explicitly related Pages in the same script agent: {response:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["runtimeGetHeapUsageMoliCountersScope"],
|
||||
@@ -680,12 +676,12 @@ async fn heap_profiler_moli_diagnostics_reports_connection_state_without_loaded_
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["runtimeCollectGarbageScope"],
|
||||
json!("page-vm-document-isolate"),
|
||||
json!("script-agent-document-isolate"),
|
||||
"diagnostics should label collectGarbage scope: {response:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["v8ForegroundTaskWakeScope"],
|
||||
json!("page-vm-document-isolate"),
|
||||
json!("script-agent-document-isolate"),
|
||||
"diagnostics should label V8 foreground task wake scope: {response:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::BackgroundNavigationCompletion;
|
||||
|
||||
/// Runtime.enable is a no-op that succeeds.
|
||||
#[tokio::test]
|
||||
@@ -516,12 +517,12 @@ async fn loaded_page_runtime_agent_state_commands_dispatch_after_runtime_enable(
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn runtime_agent_configuration_is_restored_on_replacement_page_isolate() {
|
||||
let mut ctx = TestContext::new();
|
||||
with_loaded_document_async(&mut ctx, "<!doctype html><body>before</body>").await;
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context should exist")
|
||||
.set_active_target_id("TID-runtime-agent-restore");
|
||||
with_loaded_document_for_target_async(
|
||||
&mut ctx,
|
||||
"<!doctype html><body>before</body>",
|
||||
"TID-runtime-agent-restore",
|
||||
)
|
||||
.await;
|
||||
enable_runtime_and_take_execution_context_id_async(&mut ctx, 11_016).await;
|
||||
|
||||
for (id, method, params) in [
|
||||
@@ -606,12 +607,12 @@ async fn runtime_agent_configuration_is_restored_on_replacement_page_isolate() {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn opaque_reattach_state_wins_over_conflicting_runtime_listener_configuration() {
|
||||
let mut ctx = TestContext::new();
|
||||
with_loaded_document_async(&mut ctx, "<!doctype html><body>before</body>").await;
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context should exist")
|
||||
.set_active_target_id("TID-runtime-reattach-precedence");
|
||||
with_loaded_document_for_target_async(
|
||||
&mut ctx,
|
||||
"<!doctype html><body>before</body>",
|
||||
"TID-runtime-reattach-precedence",
|
||||
)
|
||||
.await;
|
||||
|
||||
ctx.process_async(json!({"id": 11_023, "method": "Runtime.enable"}))
|
||||
.await;
|
||||
@@ -2233,7 +2234,7 @@ async fn page_navigate_empty_http_error_commits_browser_error_document() {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn page_navigate_network_failure_commits_error_document() {
|
||||
async fn page_navigate_network_failure_commits_error_document_in_stable_page() {
|
||||
async fn first() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE.as_str(), "text/html; charset=utf-8")],
|
||||
@@ -2497,23 +2498,23 @@ async fn page_navigate_network_failure_commits_error_document() {
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_error(91, -32000, "No resource with given identifier found");
|
||||
assert_ne!(
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some("SID-1")),
|
||||
Some(before_target_page),
|
||||
"current master installs navigation Documents as a new target Page attachment"
|
||||
"the network error Document must stay in the target's stable Page residence"
|
||||
);
|
||||
assert_ne!(
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some("SID-1")),
|
||||
Some(before_renderer_page),
|
||||
"the error Document must not reuse the retired renderer Page"
|
||||
"the network error Document must replace only the renderer Page's current realm"
|
||||
);
|
||||
assert_ne!(
|
||||
ctx.conn
|
||||
.current_renderer_agent_attachment_id_for_session_owner(Some("SID-1")),
|
||||
Some(before_renderer_attachment),
|
||||
"the error Document must own a replacement realm/Inspector attachment"
|
||||
"the error Document must replace the previous realm/renderer agent"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::test_support::{
|
||||
create_isolated_world_async, enable_runtime_and_take_execution_context_id_async,
|
||||
take_response_by_id, wait_for_response_by_id_async, with_loaded_document_async,
|
||||
with_loaded_document_for_active_target_async, with_loaded_http_document_async,
|
||||
with_loaded_document_for_active_target_async, with_loaded_document_for_target_async,
|
||||
with_loaded_http_document_async,
|
||||
};
|
||||
use crate::conn::{
|
||||
BrowserContext, CdpCommandTaskStep, CdpSchedulerEvent, NETWORK_ERROR_PAGE_URL,
|
||||
PendingCdpCommandDispatch,
|
||||
};
|
||||
use crate::domains::page::BackgroundNavigationCompletion;
|
||||
use crate::testing::{
|
||||
TestContext, spawn_connection_drop_server, wait_until_message, wait_until_messages,
|
||||
};
|
||||
|
||||
@@ -9,12 +9,7 @@ async fn enable_with_background_event_sender_defers_initial_document_page_build(
|
||||
bc.set_target_url("about:blank".into());
|
||||
ctx.conn.browser_context = Some(bc);
|
||||
|
||||
let (background_tx, _) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (completion_tx, _) =
|
||||
tokio::sync::mpsc::unbounded_channel::<BackgroundNavigationCompletion>();
|
||||
ctx.conn.set_background_event_sender(background_tx);
|
||||
ctx.conn
|
||||
.set_background_navigation_completion_sender(completion_tx);
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 21,
|
||||
|
||||
@@ -600,7 +600,7 @@ onconnect = event => {
|
||||
|
||||
let response = take_response_by_id(&mut ctx, 90_022);
|
||||
let isolate_scope = &response["result"]["isolateScope"];
|
||||
assert_eq!(isolate_scope["documentIsolateModel"], json!("page-vm"));
|
||||
assert_eq!(isolate_scope["documentIsolateModel"], json!("script-agent"));
|
||||
assert_eq!(isolate_scope["loadedDocumentPageCount"], json!(2));
|
||||
assert_eq!(
|
||||
isolate_scope["loadedDocumentRendererOwnerCount"],
|
||||
@@ -775,8 +775,8 @@ onconnect = event => {
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["estimatedDocumentIsolateCount"],
|
||||
json!(2),
|
||||
"popup PageVM must be counted as a second document isolate: {response:?}"
|
||||
json!(1),
|
||||
"a related opener and popup keep separate Window realms in one stable script-agent isolate: {response:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["documentContextCount"],
|
||||
@@ -801,8 +801,8 @@ onconnect = event => {
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["estimatedLiveV8IsolateCount"],
|
||||
json!(4),
|
||||
"combo should be two page document isolates plus two worker isolates: {response:?}"
|
||||
json!(3),
|
||||
"combo should be one related-page script-agent isolate plus two worker isolates: {response:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
response["result"]["activeBrowserContext"]["backgroundLoadedPageCount"],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5045,6 +5045,11 @@ async fn same_context_loaded_background_window_open_self_navigates_owner_without
|
||||
})
|
||||
})
|
||||
.await;
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some(&owner.session_id),
|
||||
"background _self Document continuation",
|
||||
)
|
||||
.await;
|
||||
let emitted = ctx.take_all();
|
||||
assert!(
|
||||
!emitted
|
||||
@@ -5134,6 +5139,11 @@ async fn same_context_named_popup_reuse_navigates_and_promotes_loaded_owner() {
|
||||
},
|
||||
)
|
||||
.await;
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some(&owner.session_id),
|
||||
"named background popup Document continuation",
|
||||
)
|
||||
.await;
|
||||
let emitted = ctx.take_all();
|
||||
assert!(
|
||||
!emitted
|
||||
|
||||
+720
@@ -1,5 +1,7 @@
|
||||
use super::super::tests_cdp_smoke_fixture::SmokeFixtureServer;
|
||||
use super::super::*;
|
||||
use crate::conn::NETWORK_ERROR_PAGE_URL;
|
||||
use crate::testing::spawn_connection_drop_server;
|
||||
use crate::{CdpCommandTaskStep, CommandDispatchContext, ParsedCdpCommand};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -963,6 +965,724 @@ async fn rust_cdp_chromium_target_window_open_waiting_popup_routes_initial_docum
|
||||
);
|
||||
}
|
||||
|
||||
// Chromium source/runtime evidence:
|
||||
// content/browser/renderer_host/navigation_request.cc::CommitErrorPage
|
||||
// content/browser/renderer_host/navigation_request_browsertest.cc
|
||||
// A failed popup destination replaces the initial empty Document while the
|
||||
// auxiliary browsing context, stable Page, WindowProxy, and opener relation survive.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn popup_transport_failure_commits_error_document_in_stable_auxiliary_page() {
|
||||
let (failing_addr, failing_server) = spawn_connection_drop_server().await;
|
||||
let mut ctx = TestContext::new_with_target_discovery(false);
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
load_bc_with_titled_page_async(
|
||||
&mut ctx,
|
||||
"BID-popup-network-error",
|
||||
"TID-popup-network-error-opener",
|
||||
"<main>opener</main>",
|
||||
)
|
||||
.await;
|
||||
set_auto_attach_waiting_for_debugger(&mut ctx, 260_223).await;
|
||||
ctx.take_all();
|
||||
|
||||
let unreachable_url = format!("http://{failing_addr}/popup-error");
|
||||
let messages = open_popup_from_runtime(
|
||||
&mut ctx,
|
||||
260_224,
|
||||
&format!(
|
||||
"(() => {{ const popup = window.open('{unreachable_url}', '_blank'); globalThis.__networkErrorPopup = popup; globalThis.__networkErrorPopupAlias = popup; popup.__openerImmediateMutation = 'old realm'; popup.document.body.dataset.openerMutation = 'old document'; return popup !== null; }})()"
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response(&messages, 260_224)["result"]["result"]["value"],
|
||||
true
|
||||
);
|
||||
let popup = event(&messages, "Target.targetCreated");
|
||||
let popup_target_id = popup["params"]["targetInfo"]["targetId"]
|
||||
.as_str()
|
||||
.expect("popup target id")
|
||||
.to_owned();
|
||||
assert_eq!(popup["params"]["targetInfo"]["url"], unreachable_url);
|
||||
assert_eq!(
|
||||
popup["params"]["targetInfo"]["openerId"],
|
||||
"TID-popup-network-error-opener"
|
||||
);
|
||||
assert_eq!(popup["params"]["targetInfo"]["canAccessOpener"], true);
|
||||
let attached = event(&messages, "Target.attachedToTarget");
|
||||
let popup_session_id = attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("popup session id")
|
||||
.to_owned();
|
||||
|
||||
for (id, method, params) in [
|
||||
(260_225, "Page.enable", json!({})),
|
||||
(260_226, "Network.enable", json!({})),
|
||||
(260_227, "Runtime.enable", json!({})),
|
||||
(
|
||||
260_228,
|
||||
"Page.setLifecycleEventsEnabled",
|
||||
json!({ "enabled": true }),
|
||||
),
|
||||
] {
|
||||
ctx.process_async(json!({
|
||||
"id": id,
|
||||
"method": method,
|
||||
"sessionId": popup_session_id,
|
||||
"params": params
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(id, json!({}), Some(&popup_session_id));
|
||||
}
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_229,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"expression": "({ href: location.href, marker: __openerImmediateMutation, bodyMarker: document.body.dataset.openerMutation, historyLength: history.length })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 260_229)["result"]["result"]["value"],
|
||||
json!({
|
||||
"href": "about:blank",
|
||||
"marker": "old realm",
|
||||
"bodyMarker": "old document",
|
||||
"historyLength": 1
|
||||
})
|
||||
);
|
||||
|
||||
let before_target_page = ctx
|
||||
.conn
|
||||
.target_page_residence_identity_for_session(Some(&popup_session_id))
|
||||
.expect("popup target Page residence");
|
||||
let before_renderer_page = ctx
|
||||
.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some(&popup_session_id))
|
||||
.expect("popup renderer Page residence");
|
||||
let before_renderer_attachment = ctx
|
||||
.conn
|
||||
.current_renderer_agent_attachment_id_for_session_owner(Some(&popup_session_id))
|
||||
.expect("popup renderer attachment");
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_230,
|
||||
"method": "Runtime.runIfWaitingForDebugger",
|
||||
"sessionId": popup_session_id
|
||||
}))
|
||||
.await;
|
||||
take_response_by_id(&mut ctx, 260_230);
|
||||
crate::testing::wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"popup network error Document load",
|
||||
|message| {
|
||||
message["method"] == json!("Page.loadEventFired")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
ctx.wait_until_scheduler_state("popup network error commit", |conn| {
|
||||
!conn.has_pending_document_navigation_for_session_owner(Some(&popup_session_id))
|
||||
&& conn
|
||||
.browser_context_by_id("BID-popup-network-error")
|
||||
.and_then(|browser_context| {
|
||||
browser_context.background_target(&popup_target_id)
|
||||
})
|
||||
.and_then(|target| target.loaded_page())
|
||||
.is_some_and(|page| page.final_url().as_str() == NETWORK_ERROR_PAGE_URL)
|
||||
})
|
||||
.await;
|
||||
|
||||
let request_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.requestWillBeSent")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["request"]["url"] == json!(unreachable_url)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup request: {:?}", ctx.sent));
|
||||
let request_id = ctx.sent[request_index]["params"]["requestId"]
|
||||
.as_str()
|
||||
.expect("popup request id")
|
||||
.to_owned();
|
||||
let failed_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.loadingFailed")
|
||||
&& message["params"]["requestId"] == json!(request_id)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup loadingFailed: {:?}", ctx.sent));
|
||||
let frame_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Page.frameNavigated")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["frame"]["url"]
|
||||
== json!(NETWORK_ERROR_PAGE_URL)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup error frame: {:?}", ctx.sent));
|
||||
let finished_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.loadingFinished")
|
||||
&& message["params"]["requestId"] == json!(request_id)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup loadingFinished: {:?}", ctx.sent));
|
||||
let dom_content_loaded_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Page.domContentEventFired")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
})
|
||||
.expect("popup error Document DCL");
|
||||
assert!(
|
||||
request_index < failed_index
|
||||
&& failed_index < frame_index
|
||||
&& frame_index < finished_index
|
||||
&& finished_index < dom_content_loaded_index,
|
||||
"popup error-page event order should match Chromium: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.sent[frame_index]["params"]["frame"]["unreachableUrl"],
|
||||
unreachable_url
|
||||
);
|
||||
assert_eq!(ctx.sent[failed_index]["params"]["canceled"], false);
|
||||
assert!(!ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Network.responseReceived")
|
||||
&& message["params"]["requestId"] == json!(request_id)
|
||||
}));
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_231,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"expression": "({ href: location.href, origin: location.origin, oldMarkerType: typeof __openerImmediateMutation, oldBodyMarkerType: typeof document.body.dataset.openerMutation, title: document.title, readyState: document.readyState, historyLength: history.length, hasOpener: opener !== null })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 260_231)["result"]["result"]["value"],
|
||||
json!({
|
||||
"href": NETWORK_ERROR_PAGE_URL,
|
||||
"origin": "null",
|
||||
"oldMarkerType": "undefined",
|
||||
"oldBodyMarkerType": "undefined",
|
||||
"title": "127.0.0.1",
|
||||
"readyState": "complete",
|
||||
"historyLength": 1,
|
||||
"hasOpener": true
|
||||
})
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_232,
|
||||
"method": "Page.getNavigationHistory",
|
||||
"sessionId": popup_session_id
|
||||
}))
|
||||
.await;
|
||||
let history = take_response_by_id(&mut ctx, 260_232);
|
||||
assert_eq!(history["result"]["currentIndex"], json!(0));
|
||||
assert_eq!(
|
||||
history["result"]["entries"]
|
||||
.as_array()
|
||||
.expect("popup error history entries")
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(history["result"]["entries"][0]["url"], unreachable_url);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_233,
|
||||
"method": "Target.getTargetInfo",
|
||||
"params": { "targetId": popup_target_id }
|
||||
}))
|
||||
.await;
|
||||
let target_info = take_response_by_id(&mut ctx, 260_233);
|
||||
assert_eq!(target_info["result"]["targetInfo"]["url"], unreachable_url);
|
||||
assert_eq!(
|
||||
target_info["result"]["targetInfo"]["openerId"],
|
||||
"TID-popup-network-error-opener"
|
||||
);
|
||||
assert_eq!(
|
||||
target_info["result"]["targetInfo"]["canAccessOpener"],
|
||||
true
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.target_page_residence_identity_for_session(Some(&popup_session_id)),
|
||||
Some(before_target_page),
|
||||
"popup error navigation should keep its target Page residence"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.renderer_page_residence_identity_for_session_owner(Some(&popup_session_id)),
|
||||
Some(before_renderer_page),
|
||||
"popup error navigation should keep its renderer Page/WindowProxy residence"
|
||||
);
|
||||
assert_ne!(
|
||||
ctx.conn
|
||||
.current_renderer_agent_attachment_id_for_session_owner(Some(
|
||||
&popup_session_id,
|
||||
)),
|
||||
Some(before_renderer_attachment),
|
||||
"popup error navigation should replace only the renderer Document/realm"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_234,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "({ retainedWindowProxy: __networkErrorPopup !== null && __networkErrorPopup === __networkErrorPopupAlias })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let opener_probe = take_response_by_id(&mut ctx, 260_234);
|
||||
assert_eq!(
|
||||
opener_probe["result"]["result"]["value"],
|
||||
json!({ "retainedWindowProxy": true }),
|
||||
"opener-side stable popup WindowProxy probe failed: {opener_probe:?}"
|
||||
);
|
||||
})
|
||||
.await;
|
||||
failing_server.abort();
|
||||
}
|
||||
|
||||
// Chromium/WPT sources:
|
||||
// third_party/blink/web_tests/external/wpt/html/browsers/browsing-the-web/
|
||||
// navigating-across-documents/initial-empty-document/window-open-204-fragment.html
|
||||
// navigating-across-documents/initial-empty-document/
|
||||
// window-open-204-pushState-replaceState.html
|
||||
// third_party/blink/web_tests/http/tests/inspector-protocol/page/navigate-204.js
|
||||
// third_party/blink/web_tests/http/tests/inspector-protocol/network/
|
||||
// navigation-204-loading-failed.js
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn popup_no_commit_responses_preserve_initial_document_before_redirect_replacement() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
let mut ctx = TestContext::new_with_target_discovery(false);
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
load_bc_with_titled_page_async(
|
||||
&mut ctx,
|
||||
"BID-popup-terminal",
|
||||
"TID-popup-terminal-opener",
|
||||
"<main>opener</main>",
|
||||
)
|
||||
.await;
|
||||
set_auto_attach_waiting_for_debugger(&mut ctx, 260_230).await;
|
||||
ctx.take_all();
|
||||
|
||||
let no_content_url = fixture.url("/no-content");
|
||||
let messages = open_popup_from_runtime(
|
||||
&mut ctx,
|
||||
260_231,
|
||||
&format!(
|
||||
"(() => {{ const popup = window.open('{no_content_url}', '_blank'); popup.__openerImmediateMutation = 'kept'; popup.document.body.dataset.openerMutation = 'kept'; return popup !== null; }})()"
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response(&messages, 260_231)["result"]["result"]["value"],
|
||||
true
|
||||
);
|
||||
let popup = event(&messages, "Target.targetCreated");
|
||||
let popup_target_id = popup["params"]["targetInfo"]["targetId"]
|
||||
.as_str()
|
||||
.expect("popup target id")
|
||||
.to_owned();
|
||||
let attached = event(&messages, "Target.attachedToTarget");
|
||||
let popup_session_id = attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("popup session id")
|
||||
.to_owned();
|
||||
|
||||
for (id, method, params) in [
|
||||
(260_232, "Page.enable", json!({})),
|
||||
(260_233, "Network.enable", json!({})),
|
||||
(
|
||||
260_234,
|
||||
"Page.setLifecycleEventsEnabled",
|
||||
json!({ "enabled": true }),
|
||||
),
|
||||
] {
|
||||
ctx.process_async(json!({
|
||||
"id": id,
|
||||
"method": method,
|
||||
"sessionId": popup_session_id,
|
||||
"params": params
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(id, json!({}), Some(&popup_session_id));
|
||||
}
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_235,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"expression": "({ href: location.href, globalMarker: __openerImmediateMutation, bodyMarker: document.body.dataset.openerMutation, historyLength: history.length })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 260_235)["result"]["result"]["value"],
|
||||
json!({
|
||||
"href": "about:blank",
|
||||
"globalMarker": "kept",
|
||||
"bodyMarker": "kept",
|
||||
"historyLength": 1
|
||||
})
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_236,
|
||||
"method": "Runtime.runIfWaitingForDebugger",
|
||||
"sessionId": popup_session_id
|
||||
}))
|
||||
.await;
|
||||
take_response_by_id(&mut ctx, 260_236);
|
||||
crate::testing::wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"popup 204 navigation abort",
|
||||
|message| {
|
||||
message["method"] == json!("Network.loadingFailed")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["errorText"] == json!("net::ERR_ABORTED")
|
||||
},
|
||||
)
|
||||
.await;
|
||||
ctx.wait_until_scheduler_state("popup 204 terminal completion", |conn| {
|
||||
!conn.has_pending_document_navigation_for_session_owner(Some(&popup_session_id))
|
||||
})
|
||||
.await;
|
||||
|
||||
let response_204_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.responseReceived")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["response"]["url"] == json!(no_content_url)
|
||||
&& message["params"]["response"]["status"] == json!(204)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup 204 response: {:?}", ctx.sent));
|
||||
let request_204 = ctx.sent[response_204_index]["params"]["requestId"]
|
||||
.as_str()
|
||||
.expect("204 request id")
|
||||
.to_owned();
|
||||
let failed_204_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| {
|
||||
message["method"] == json!("Network.loadingFailed")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["requestId"] == json!(request_204)
|
||||
&& message["params"]["errorText"] == json!("net::ERR_ABORTED")
|
||||
&& message["params"]["canceled"] == json!(true)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup 204 loadingFailed: {:?}", ctx.sent));
|
||||
assert!(response_204_index < failed_204_index);
|
||||
assert!(!ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Network.loadingFinished")
|
||||
&& message["params"]["requestId"] == json!(request_204)
|
||||
}));
|
||||
assert!(!ctx.sent.iter().any(|message| {
|
||||
(message["method"] == json!("Page.frameNavigated")
|
||||
&& message["params"]["frame"]["url"] == json!(no_content_url))
|
||||
|| (message["method"] == json!("Page.lifecycleEvent")
|
||||
&& matches!(
|
||||
message["params"]["name"].as_str(),
|
||||
Some("DOMContentLoaded" | "load")
|
||||
))
|
||||
}));
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_237,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"expression": "location.hash = 'after-204'; const fragmentHistoryLength = history.length; history.pushState({ source: 'initial-empty' }, '', '#after-push-state'); ({ href: location.href, globalMarker: __openerImmediateMutation, bodyMarker: document.body.dataset.openerMutation, fragmentHistoryLength, historyLength: history.length, historyState: history.state.source })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 260_237)["result"]["result"]["value"],
|
||||
json!({
|
||||
"href": "about:blank#after-push-state",
|
||||
"globalMarker": "kept",
|
||||
"bodyMarker": "kept",
|
||||
"fragmentHistoryLength": 1,
|
||||
"historyLength": 1,
|
||||
"historyState": "initial-empty"
|
||||
})
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_243,
|
||||
"method": "Page.getNavigationHistory",
|
||||
"sessionId": popup_session_id
|
||||
}))
|
||||
.await;
|
||||
let fragment_history = take_response_by_id(&mut ctx, 260_243);
|
||||
assert_eq!(fragment_history["result"]["currentIndex"], json!(0));
|
||||
assert_eq!(
|
||||
fragment_history["result"]["entries"]
|
||||
.as_array()
|
||||
.expect("post-204 fragment history entries")
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
fragment_history["result"]["entries"][0]["url"],
|
||||
"about:blank#after-push-state"
|
||||
);
|
||||
|
||||
ctx.sent.clear();
|
||||
let reset_content_url = fixture.url("/reset-content");
|
||||
ctx.process_async(json!({
|
||||
"id": 260_238,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": { "url": reset_content_url }
|
||||
}))
|
||||
.await;
|
||||
crate::testing::wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"popup 205 Page.navigate result",
|
||||
|message| message["id"] == json!(260_238),
|
||||
)
|
||||
.await;
|
||||
let reset_result = take_response_by_id(&mut ctx, 260_238);
|
||||
assert_eq!(reset_result["result"]["frameId"], popup_target_id);
|
||||
assert!(reset_result["result"]["loaderId"].as_str().is_some());
|
||||
assert_eq!(
|
||||
reset_result["result"]["errorText"],
|
||||
json!("net::ERR_ABORTED")
|
||||
);
|
||||
assert_eq!(reset_result["result"]["isDownload"], json!(false));
|
||||
let response_205 = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Network.responseReceived")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["response"]["url"] == json!(reset_content_url)
|
||||
&& message["params"]["response"]["status"] == json!(205)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing popup 205 response: {:?}", ctx.sent));
|
||||
let request_205 = response_205["params"]["requestId"]
|
||||
.as_str()
|
||||
.expect("205 request id");
|
||||
assert!(ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Network.loadingFailed")
|
||||
&& message["params"]["requestId"] == json!(request_205)
|
||||
&& message["params"]["errorText"] == json!("net::ERR_ABORTED")
|
||||
&& message["params"]["canceled"] == json!(true)
|
||||
}));
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_239,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"expression": "({ href: location.href, marker: __openerImmediateMutation, historyLength: history.length })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 260_239)["result"]["result"]["value"],
|
||||
json!({
|
||||
"href": "about:blank#after-push-state",
|
||||
"marker": "kept",
|
||||
"historyLength": 1
|
||||
})
|
||||
);
|
||||
|
||||
ctx.sent.clear();
|
||||
let redirect_start_url = fixture.url("/redirect-start");
|
||||
let redirect_final_url = fixture.url("/redirect-final");
|
||||
ctx.process_async(json!({
|
||||
"id": 260_240,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": { "url": redirect_start_url }
|
||||
}))
|
||||
.await;
|
||||
crate::testing::wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"popup redirect Page.navigate result",
|
||||
|message| message["id"] == json!(260_240),
|
||||
)
|
||||
.await;
|
||||
let redirect_result = take_response_by_id(&mut ctx, 260_240);
|
||||
let redirect_loader_id = redirect_result["result"]["loaderId"]
|
||||
.as_str()
|
||||
.expect("redirect loader id")
|
||||
.to_owned();
|
||||
crate::testing::wait_until_scheduler_message(
|
||||
&mut ctx,
|
||||
"popup redirect load lifecycle",
|
||||
|message| {
|
||||
message["method"] == json!("Page.lifecycleEvent")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["frameId"] == json!(popup_target_id)
|
||||
&& message["params"]["loaderId"] == json!(redirect_loader_id)
|
||||
&& message["params"]["name"] == json!("load")
|
||||
},
|
||||
)
|
||||
.await;
|
||||
ctx.wait_until_scheduler_state("popup redirect authoritative load", |conn| {
|
||||
conn.renderer_document_lifecycle_authoritative_state_for_session_owner(Some(
|
||||
&popup_session_id,
|
||||
))
|
||||
.is_some_and(|(binding, snapshot)| {
|
||||
binding.loader_id == redirect_loader_id && snapshot.load.is_some()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
let document_requests = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message["method"] == json!("Network.requestWillBeSent")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["type"] == json!("Document")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
document_requests
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message["params"]["request"]["url"] == json!(redirect_start_url)
|
||||
})
|
||||
.count(),
|
||||
1,
|
||||
"redirect start must have one authoritative hop: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
assert_eq!(
|
||||
document_requests
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message["params"]["request"]["url"] == json!(redirect_final_url)
|
||||
})
|
||||
.count(),
|
||||
1,
|
||||
"redirect final must have one authoritative hop: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
let start_request = document_requests
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["params"]["request"]["url"] == json!(redirect_start_url)
|
||||
})
|
||||
.expect("redirect start request");
|
||||
let final_request = document_requests
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["params"]["request"]["url"] == json!(redirect_final_url)
|
||||
})
|
||||
.expect("redirect final request");
|
||||
assert_eq!(
|
||||
start_request["params"]["requestId"],
|
||||
final_request["params"]["requestId"]
|
||||
);
|
||||
assert_eq!(
|
||||
final_request["params"]["redirectResponse"]["url"],
|
||||
redirect_start_url
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.sent
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message["method"] == json!("Page.frameNavigated")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["frame"]["loaderId"]
|
||||
== json!(redirect_loader_id)
|
||||
&& message["params"]["frame"]["url"] == json!(redirect_final_url)
|
||||
})
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
for lifecycle_name in ["DOMContentLoaded", "load"] {
|
||||
assert_eq!(
|
||||
ctx.sent
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message["method"] == json!("Page.lifecycleEvent")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["loaderId"] == json!(redirect_loader_id)
|
||||
&& message["params"]["name"] == json!(lifecycle_name)
|
||||
})
|
||||
.count(),
|
||||
1,
|
||||
"{lifecycle_name} must be published once: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
}
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_241,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"expression": "({ href: location.href, text: document.querySelector('main').textContent, historyLength: history.length, readyState: document.readyState, oldMarkerType: typeof __openerImmediateMutation })",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 260_241)["result"]["result"]["value"],
|
||||
json!({
|
||||
"href": redirect_final_url,
|
||||
"text": "redirect final",
|
||||
"historyLength": 1,
|
||||
"readyState": "complete",
|
||||
"oldMarkerType": "undefined"
|
||||
})
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_242,
|
||||
"method": "Page.getNavigationHistory",
|
||||
"sessionId": popup_session_id
|
||||
}))
|
||||
.await;
|
||||
let history = take_response_by_id(&mut ctx, 260_242);
|
||||
assert_eq!(history["result"]["currentIndex"], json!(0));
|
||||
assert_eq!(
|
||||
history["result"]["entries"]
|
||||
.as_array()
|
||||
.expect("popup history entries")
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(history["result"]["entries"][0]["url"], redirect_final_url);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// Chromium source:
|
||||
// third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-windowOpen-empty-url.js
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
||||
@@ -400,6 +400,18 @@ async fn fixture_handler(
|
||||
}
|
||||
"/redirect-start" => redirect("/redirect-final", &[("cache-control", "no-store")]),
|
||||
"/redirect-final" => html("<!doctype html><main>redirect final</main>"),
|
||||
"/no-content" => response(
|
||||
StatusCode::NO_CONTENT,
|
||||
"text/plain; charset=utf-8",
|
||||
Vec::new(),
|
||||
&[("x-smoke-navigation-terminal", "no-content")],
|
||||
),
|
||||
"/reset-content" => response(
|
||||
StatusCode::RESET_CONTENT,
|
||||
"text/plain; charset=utf-8",
|
||||
Vec::new(),
|
||||
&[("x-smoke-navigation-terminal", "reset-content")],
|
||||
),
|
||||
"/history-a" => html("<!doctype html><main>history a</main>"),
|
||||
"/history-b" => html("<!doctype html><main>history b</main>"),
|
||||
"/document-continue" => {
|
||||
@@ -749,6 +761,19 @@ async fn rust_smoke_fixture_serves_all_document_and_control_routes() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rust_smoke_fixture_serves_navigation_no_commit_routes() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
for (path, expected_status) in [("/no-content", 204), ("/reset-content", 205)] {
|
||||
let response = fixture_get(&fixture, path).await;
|
||||
assert_eq!(response.status, expected_status, "route {path}");
|
||||
assert!(
|
||||
response.body.is_empty(),
|
||||
"route {path} must not carry a body"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rust_smoke_fixture_serves_all_subresource_routes() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
|
||||
@@ -6079,6 +6079,7 @@ async fn playwright_over_cdp_script_execution_disabled_blocks_page_scripts_but_n
|
||||
let mut ctx = TestContext::new();
|
||||
let attached = create_attached_page_session_async(&mut ctx, 240, 241, 242, 2393, 243).await;
|
||||
let session_id = attached.session_id;
|
||||
let target_id = attached.target_id;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 244,
|
||||
@@ -6125,7 +6126,7 @@ async fn playwright_over_cdp_script_execution_disabled_blocks_page_scripts_but_n
|
||||
crate::testing::wait_until_renderer_document_load(
|
||||
&mut ctx,
|
||||
Some(session_id.as_str()),
|
||||
&attached.target_id,
|
||||
&target_id,
|
||||
&loader_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -171,7 +171,7 @@ async fn get_target_info_reports_background_target_in_same_browser_context() {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn popup_target_diagnostics_report_distinct_page_vm_document_isolates() {
|
||||
async fn popup_initial_about_blank_adopts_renderer_page_and_related_script_agent() {
|
||||
let mut ctx = TestContext::new();
|
||||
ctx.conn.auto_attach = true;
|
||||
let browser_context = ctx
|
||||
@@ -195,6 +195,7 @@ async fn popup_target_diagnostics_report_distinct_page_vm_document_isolates() {
|
||||
)
|
||||
.await
|
||||
.expect("opener page should load");
|
||||
let opener_renderer_page_id = opener_page.renderer_page_id().as_u64();
|
||||
let opener_url = opener_page.final_url().as_str().to_owned();
|
||||
{
|
||||
let browser_context = ctx
|
||||
@@ -251,25 +252,60 @@ async fn popup_target_diagnostics_report_distinct_page_vm_document_isolates() {
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_else(|| panic!("popup target should be auto-attached: {:?}", ctx.sent));
|
||||
let popup_session_id = attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("popup session id should be present")
|
||||
.to_owned();
|
||||
assert!(
|
||||
attached["params"]["sessionId"].as_str().is_some(),
|
||||
"popup session id should be present"
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 104_216,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"url": "data:text/html,<!doctype html><title>popup</title><body>popup</body>"
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
consume_main_document_navigation_start(&mut ctx);
|
||||
let navigation = take_response_by_id(&mut ctx, 104_216);
|
||||
assert_eq!(navigation["result"]["frameId"], json!(popup_target_id));
|
||||
ctx.sent.clear();
|
||||
let opener_script_agent_id = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.and_then(|browser_context| browser_context.active_target.runtime_slot.loaded_page_mut())
|
||||
.expect("opener should remain loaded")
|
||||
.runtime_heap_usage_async()
|
||||
.await
|
||||
.expect("opener heap diagnostics should be available")
|
||||
.moli
|
||||
.runtime
|
||||
.script_agent_id;
|
||||
let (popup_renderer_page_id, popup_browsing_context_id, popup_script_agent_id) = {
|
||||
let popup = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.and_then(|browser_context| browser_context.background_target_mut(&popup_target_id))
|
||||
.expect("popup target should remain staged");
|
||||
let popup_browsing_context_id = popup
|
||||
.auxiliary_browsing_context_id()
|
||||
.expect("popup target should retain its renderer browsing-context identity");
|
||||
let popup_page = popup
|
||||
.loaded_page_mut()
|
||||
.expect("popup initial empty Document should be loaded");
|
||||
let popup_renderer_page_id = popup_page.renderer_page_id().as_u64();
|
||||
let popup_script_agent_id = popup_page
|
||||
.runtime_heap_usage_async()
|
||||
.await
|
||||
.expect("popup heap diagnostics should be available")
|
||||
.moli
|
||||
.runtime
|
||||
.script_agent_id;
|
||||
(
|
||||
popup_renderer_page_id,
|
||||
popup_browsing_context_id,
|
||||
popup_script_agent_id,
|
||||
)
|
||||
};
|
||||
assert_ne!(popup_renderer_page_id, opener_renderer_page_id);
|
||||
assert_eq!(
|
||||
popup_renderer_page_id, popup_browsing_context_id,
|
||||
"protocol must build the popup's initial Document with the renderer-reserved Page identity"
|
||||
);
|
||||
assert_eq!(
|
||||
popup_script_agent_id, opener_script_agent_id,
|
||||
"the renderer-reserved initial popup Page should join its opener's script agent"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 104_217,
|
||||
@@ -284,15 +320,20 @@ async fn popup_target_diagnostics_report_distinct_page_vm_document_isolates() {
|
||||
json!(1),
|
||||
"opener and loaded popup target should share one renderer owner: {diagnostics:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["loadedDocumentScriptAgentCount"],
|
||||
json!(1),
|
||||
"the renderer-reserved popup Page should join its opener's explicit script agent: {diagnostics:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["estimatedDocumentIsolateCount"],
|
||||
json!(2),
|
||||
"loaded popup PageVM must own a distinct document isolate: {diagnostics:?}"
|
||||
json!(1),
|
||||
"two related PageVM realms should be hosted by one document script-agent isolate: {diagnostics:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["estimatedLiveV8IsolateCount"],
|
||||
json!(2),
|
||||
"opener plus popup without workers should report two live page document isolates: {diagnostics:?}"
|
||||
json!(1),
|
||||
"opener plus related popup without workers should report one live script-agent isolate: {diagnostics:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
isolate_scope["documentContextCount"],
|
||||
@@ -304,6 +345,79 @@ async fn popup_target_diagnostics_report_distinct_page_vm_document_isolates() {
|
||||
json!(1),
|
||||
"the popup should remain a loaded background target"
|
||||
);
|
||||
|
||||
ctx.sent.clear();
|
||||
ctx.process_async(json!({
|
||||
"id": 104_218,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": "SID-popup-opener",
|
||||
"params": {
|
||||
"expression": "window.open('about:blank#diagnostics-noopener', '_blank', 'noopener') === null",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let noopener_response = take_response_by_id(&mut ctx, 104_218);
|
||||
assert_eq!(
|
||||
noopener_response["result"]["result"]["value"],
|
||||
json!(true),
|
||||
"noopener window.open should return null after accepting the popup"
|
||||
);
|
||||
let noopener_target = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Target.targetCreated")
|
||||
&& message["params"]["targetInfo"]["canAccessOpener"] == json!(false)
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_else(|| panic!("noopener popup target should be created: {:?}", ctx.sent));
|
||||
let noopener_target_id = noopener_target["params"]["targetInfo"]["targetId"]
|
||||
.as_str()
|
||||
.expect("noopener popup target id should be present")
|
||||
.to_owned();
|
||||
let noopener_script_agent_id = {
|
||||
let popup = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.and_then(|browser_context| browser_context.background_target_mut(&noopener_target_id))
|
||||
.expect("noopener popup target should remain staged");
|
||||
let popup_browsing_context_id = popup
|
||||
.auxiliary_browsing_context_id()
|
||||
.expect("noopener popup should retain its auxiliary browsing-context identity");
|
||||
let popup_page = popup
|
||||
.loaded_page_mut()
|
||||
.expect("noopener popup initial empty Document should be loaded");
|
||||
assert_eq!(
|
||||
popup_page.renderer_page_id().as_u64(),
|
||||
popup_browsing_context_id,
|
||||
"noopener popup must also adopt its renderer-reserved Page identity"
|
||||
);
|
||||
popup_page
|
||||
.runtime_heap_usage_async()
|
||||
.await
|
||||
.expect("noopener popup heap diagnostics should be available")
|
||||
.moli
|
||||
.runtime
|
||||
.script_agent_id
|
||||
};
|
||||
assert_ne!(
|
||||
noopener_script_agent_id, opener_script_agent_id,
|
||||
"opener suppression must keep the auxiliary Page in a fresh script agent"
|
||||
);
|
||||
|
||||
ctx.sent.clear();
|
||||
ctx.process_async(json!({
|
||||
"id": 104_219,
|
||||
"method": "HeapProfiler.moliDiagnostics"
|
||||
}))
|
||||
.await;
|
||||
let diagnostics = take_response_by_id(&mut ctx, 104_219);
|
||||
let isolate_scope = &diagnostics["result"]["isolateScope"];
|
||||
assert_eq!(isolate_scope["loadedDocumentPageCount"], json!(3));
|
||||
assert_eq!(isolate_scope["loadedDocumentScriptAgentCount"], json!(2));
|
||||
assert_eq!(isolate_scope["estimatedDocumentIsolateCount"], json!(2));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
||||
@@ -1004,7 +1004,10 @@ async fn timer_window_open_emits_page_event_from_runtime_work() {
|
||||
async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key() {
|
||||
async fn opener() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE.as_str(), "text/html")],
|
||||
[
|
||||
(CONTENT_TYPE.as_str(), "text/html"),
|
||||
("content-security-policy", "script-src 'self'"),
|
||||
],
|
||||
"<!doctype html><main>session storage opener</main>",
|
||||
)
|
||||
}
|
||||
@@ -1017,17 +1020,13 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
Arc<tokio::sync::Semaphore>,
|
||||
)>,
|
||||
) -> impl IntoResponse {
|
||||
// The opener's lightweight WindowProxy facade starts its mirrored
|
||||
// load before the concrete popup action can reach protocol. Let that
|
||||
// renderer-local request finish; gate the second request, which is the
|
||||
// real auxiliary target navigation whose lifetime this test covers.
|
||||
// The real auxiliary Page owns the only navigation. Gate that request
|
||||
// so the test can inspect the synchronously created initial realm
|
||||
// after target admission but before the replacement Document commits.
|
||||
let request_index = request_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
if request_index == 0 {
|
||||
return opener().await;
|
||||
}
|
||||
assert_eq!(
|
||||
request_index, 1,
|
||||
"popup URL must have exactly two load owners"
|
||||
request_index, 0,
|
||||
"popup URL must have exactly one authoritative navigation owner"
|
||||
);
|
||||
request_started.add_permits(1);
|
||||
let permit = response_release
|
||||
@@ -1108,7 +1107,7 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
"id": 1201,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "(() => { localStorage.clear(); localStorage.setItem('shared', 'opener'); sessionStorage.clear(); sessionStorage.setItem('phase', 'before'); const popup = window.open('about:blank', '_blank'); sessionStorage.setItem('phase', 'after'); return `${popup.sessionStorage.getItem('phase')}|${sessionStorage.getItem('phase')}|${popup.localStorage.getItem('shared')}`; })()",
|
||||
"expression": "(() => { const openerOrigin = location.origin; const openerHref = location.href; const openerBase = document.baseURI; localStorage.clear(); localStorage.setItem('shared', 'opener'); sessionStorage.clear(); sessionStorage.setItem('phase', 'before'); const popup = window.open('about:blank', '_blank'); let popupEvalBlocked = false; try { popup.eval('1'); } catch (error) { popupEvalBlocked = error.name === 'EvalError'; } sessionStorage.setItem('phase', 'after'); return `${popup.sessionStorage.getItem('phase')}|${sessionStorage.getItem('phase')}|${popup.localStorage.getItem('shared')}|${popup.location.origin === openerOrigin}|${popup.origin === openerOrigin}|${popup.document.referrer === openerHref}|${popup.document.baseURI === openerBase}|${popup.name === ''}|${popupEvalBlocked}`; })()",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
@@ -1118,7 +1117,7 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
json!({
|
||||
"result": {
|
||||
"type": "string",
|
||||
"value": "before|after|opener"
|
||||
"value": "before|after|opener|true|true|true|true|true|true"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
@@ -1150,12 +1149,40 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
.to_owned();
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 12021,
|
||||
"sessionId": popup_session_id,
|
||||
"method": "Runtime.enable"
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(12021, json!({}), Some(&popup_session_id));
|
||||
let default_context = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Runtime.executionContextCreated")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["context"]["auxData"]["isDefault"] == json!(true)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("popup default execution context: {:?}", ctx.sent));
|
||||
assert_eq!(
|
||||
default_context["params"]["context"]["origin"],
|
||||
json!(format!("http://{addr}")),
|
||||
"CDP must expose the creator-inherited origin for the real initial about:blank realm: {default_context:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
default_context["params"]["context"]["auxData"]["frameId"],
|
||||
json!(popup_target_id)
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1203,
|
||||
"sessionId": popup_session_id,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "`${String(sessionStorage.getItem('phase'))}|${String(localStorage.getItem('shared'))}`",
|
||||
"expression": "(() => { let evalBlocked = false; try { eval('1'); } catch (error) { evalBlocked = error.name === 'EvalError'; } return `${String(sessionStorage.getItem('phase'))}|${String(localStorage.getItem('shared'))}|${evalBlocked}`; })()",
|
||||
"allowUnsafeEvalBlockedByCSP": false,
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
@@ -1165,7 +1192,7 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
json!({
|
||||
"result": {
|
||||
"type": "string",
|
||||
"value": "before|opener"
|
||||
"value": "before|opener|true"
|
||||
}
|
||||
}),
|
||||
Some(&popup_session_id),
|
||||
@@ -1215,6 +1242,19 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
Some(&popup_session_id),
|
||||
);
|
||||
|
||||
ctx.sent.clear();
|
||||
ctx.process_async(json!({
|
||||
"id": 12033,
|
||||
"method": "Target.setAutoAttach",
|
||||
"params": {
|
||||
"autoAttach": true,
|
||||
"waitForDebuggerOnStart": true,
|
||||
"flatten": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(12033, json!({}), None);
|
||||
|
||||
ctx.sent.clear();
|
||||
let cross_origin_url = format!("http://{cross_origin_addr}/popup");
|
||||
ctx.process_async(json!({
|
||||
@@ -1223,7 +1263,7 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
"sessionId": opener_session_id,
|
||||
"params": {
|
||||
"expression": format!(
|
||||
"(() => {{ sessionStorage.setItem('cross-origin-secret', 'opener-only'); return window.open({cross_origin_url:?}, '_blank') !== null; }})()"
|
||||
"(() => {{ sessionStorage.setItem('cross-origin-secret', 'opener-only'); const popup = window.open({cross_origin_url:?}, '_blank'); if (popup === null) return false; popup.__initialNonEmptyMarker = 'before-navigation'; popup.document.body.setAttribute('data-before-navigation', 'preserved'); window.__pendingNonEmptyPopup = popup; window.__pendingNonEmptyDocument = popup.document; return popup.location.href === 'about:blank'; }})()"
|
||||
),
|
||||
"returnByValue": true
|
||||
}
|
||||
@@ -1251,6 +1291,67 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
.as_str()
|
||||
.expect("cross-origin popup target id")
|
||||
.to_owned();
|
||||
let cross_origin_attached = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message["method"] == json!("Target.attachedToTarget")
|
||||
&& message["params"]["targetInfo"]["targetId"]
|
||||
== json!(cross_origin_target_id)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("cross-origin popup auto-attach event: {:?}", ctx.sent));
|
||||
assert_eq!(
|
||||
cross_origin_attached["params"]["waitingForDebugger"],
|
||||
json!(true)
|
||||
);
|
||||
let cross_origin_session_id = cross_origin_attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("cross-origin popup session id")
|
||||
.to_owned();
|
||||
ctx.sent.clear();
|
||||
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.browser_context_by_id("BID-popup-storage")
|
||||
.is_some_and(|browser_context| browser_context
|
||||
.has_pending_document_navigation_for_target(Some(&cross_origin_target_id))),
|
||||
"waitForDebuggerOnStart must hold the target-owned navigation before it starts"
|
||||
);
|
||||
assert_eq!(
|
||||
cross_origin_request_count.load(std::sync::atomic::Ordering::SeqCst),
|
||||
0,
|
||||
"target admission must precede the only popup network request"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 12051,
|
||||
"sessionId": cross_origin_session_id,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "`${window.opener.__pendingNonEmptyPopup === window}|${window.opener.__pendingNonEmptyDocument === document}|${window.__initialNonEmptyMarker}|${document.body.getAttribute('data-before-navigation')}|${sessionStorage.getItem('cross-origin-secret')}`",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(
|
||||
12051,
|
||||
json!({
|
||||
"result": {
|
||||
"type": "string",
|
||||
"value": "true|true|before-navigation|preserved|opener-only"
|
||||
}
|
||||
}),
|
||||
Some(&cross_origin_session_id),
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 12052,
|
||||
"sessionId": cross_origin_session_id,
|
||||
"method": "Runtime.runIfWaitingForDebugger"
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(12052, json!({}), Some(&cross_origin_session_id));
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.wait_until_scheduler_state("cross-origin popup navigation starts", |conn| {
|
||||
@@ -1270,28 +1371,6 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
.expect("cross-origin popup request gate must remain open");
|
||||
request_started.forget();
|
||||
|
||||
// Attach while the target's requested URL is still loading. Session
|
||||
// attachment belongs to the browsing context and must not race the
|
||||
// replacement Document's commit.
|
||||
ctx.process_and_wait_for_response_async(json!({
|
||||
"id": 1205,
|
||||
"method": "Target.attachToTarget",
|
||||
"params": { "targetId": cross_origin_target_id }
|
||||
}))
|
||||
.await;
|
||||
let attach_response = take_response_by_id(&mut ctx, 1205);
|
||||
let cross_origin_session_id = attach_response["result"]["sessionId"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("cross-origin popup session id: {attach_response:?}"))
|
||||
.to_owned();
|
||||
assert!(
|
||||
ctx.conn
|
||||
.browser_context_by_id("BID-popup-storage")
|
||||
.is_some_and(|browser_context| browser_context
|
||||
.has_pending_document_navigation_for_target(Some(&cross_origin_target_id))),
|
||||
"attaching a DevTools session must not replace the target-owned navigation"
|
||||
);
|
||||
ctx.sent.clear();
|
||||
cross_origin_response_release.add_permits(1);
|
||||
let response_returned = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
@@ -1314,6 +1393,12 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key(
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
cross_origin_request_count.load(std::sync::atomic::Ordering::SeqCst),
|
||||
1,
|
||||
"non-empty popup navigation must issue exactly one network request"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1206,
|
||||
"sessionId": cross_origin_session_id,
|
||||
@@ -1611,6 +1696,118 @@ async fn popup_initial_empty_document_frame_tree_inherits_opener_origin() {
|
||||
assert!(initial.is_on_initial_empty_document());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn opener_window_handle_projects_the_renderer_owned_auxiliary_realm() {
|
||||
let mut ctx = TestContext::new();
|
||||
load_bc_with_titled_page_async(
|
||||
&mut ctx,
|
||||
"BID-popup-window-proxy",
|
||||
"TID-opener-window-proxy",
|
||||
"<main>popup WindowProxy opener</main>",
|
||||
)
|
||||
.await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1250,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "(() => { const popup = window.open('about:blank', '_blank'); globalThis.__lmPopupWindow = popup; globalThis.__lmPopupDocument = popup.document; popup.__lmSynchronousRealmMarker = 'before-target-activation'; popup.document.body.textContent = 'synchronous-document'; return `${popup !== null}|${__lmPopupDocument === popup.document}|${popup.__lmSynchronousRealmMarker}|${popup.document.body.textContent}|${popup.name === ''}|${popup.location.origin === location.origin}|${popup.document.referrer === location.href}|${popup.document.baseURI === document.baseURI}`; })()",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let open_response = take_response_by_id(&mut ctx, 1250);
|
||||
assert_eq!(
|
||||
open_response["result"]["result"]["value"],
|
||||
json!("true|true|before-target-activation|synchronous-document|true|true|true|true")
|
||||
);
|
||||
let popup_target_id = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.find(|message| message["method"] == json!("Target.targetCreated"))
|
||||
.and_then(|message| message["params"]["targetInfo"]["targetId"].as_str())
|
||||
.expect("window.open should create its auxiliary target")
|
||||
.to_owned();
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1251,
|
||||
"method": "Target.attachToTarget",
|
||||
"params": { "targetId": popup_target_id }
|
||||
}))
|
||||
.await;
|
||||
let popup_session_id = take_response_by_id(&mut ctx, 1251)["result"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("popup session id")
|
||||
.to_owned();
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1252,
|
||||
"sessionId": popup_session_id,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "const synchronousDocumentSurvived = window.opener.__lmPopupDocument === document; const synchronousRealmSurvived = __lmSynchronousRealmMarker === 'before-target-activation'; globalThis.__lmAuxiliaryRealmMarker = 'renderer-owned'; document.body.textContent = 'auxiliary-document'; `${synchronousDocumentSurvived}|${synchronousRealmSurvived}|${__lmAuxiliaryRealmMarker}|${document.body.textContent}|${window === globalThis}|${name === ''}|${document.referrer === window.opener.location.href}|${document.baseURI === window.opener.document.baseURI}`",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let popup_evaluate = take_response_by_id(&mut ctx, 1252);
|
||||
assert_eq!(
|
||||
popup_evaluate["result"]["result"]["value"],
|
||||
json!("true|true|renderer-owned|auxiliary-document|true|true|true|true"),
|
||||
"target adoption must retain the exact Document and realm created synchronously by window.open: {popup_evaluate:?}"
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1253,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "`${__lmPopupDocument === __lmPopupWindow.document}|${__lmPopupWindow.__lmAuxiliaryRealmMarker}|${__lmPopupDocument.body.textContent}|${__lmPopupWindow === __lmPopupWindow.window}`",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let opener_projection = take_response_by_id(&mut ctx, 1253);
|
||||
assert_eq!(
|
||||
opener_projection["result"]["result"]["value"],
|
||||
json!("true|renderer-owned|auxiliary-document|true"),
|
||||
"the opener-retained objects must be the auxiliary Page's actual stable WindowProxy and Document: {opener_projection:?}"
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1254,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "__lmPopupWindow.__lmProjectedFromOpener = 'same-proxy'; true",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 1254)["result"]["result"]["value"],
|
||||
json!(true)
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 1255,
|
||||
"sessionId": popup_session_id,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "String(globalThis.__lmProjectedFromOpener)",
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(&mut ctx, 1255)["result"]["result"]["value"],
|
||||
json!("same-proxy")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn window_open_self_navigates_current_target_without_popup_target() {
|
||||
let mut ctx = TestContext::new();
|
||||
@@ -3266,6 +3463,12 @@ async fn create_target_without_debugger_wait_starts_requested_url_navigation() {
|
||||
"Target.createTarget response should retain the created target id: {messages:?}"
|
||||
);
|
||||
|
||||
ctx.wait_for_document_continuation_for_test(
|
||||
Some(session_id),
|
||||
"created target requested-URL Document continuation",
|
||||
)
|
||||
.await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 9010,
|
||||
"sessionId": session_id,
|
||||
|
||||
@@ -18,10 +18,10 @@ pub mod version;
|
||||
pub use devtools_runtime::*;
|
||||
|
||||
pub use conn::{
|
||||
BackgroundCommandResponsePayload, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection,
|
||||
CdpInitialStoragePartition, CdpRendererCommandAccess, CdpRendererCommandReplacement,
|
||||
CdpRendererCommandReplayDispatch, CdpRendererOwnerTurnOutcome, CdpSchedulerEvent,
|
||||
CdpTargetHostLifecycleDelta, CdpTargetHostLifecycleObserver, CdpTurnOutcome,
|
||||
BackgroundCommandResponsePayload, BackgroundNavigationGateKey, BackgroundProtocolEvent,
|
||||
CdpCommandTaskStep, CdpConnection, CdpInitialStoragePartition, CdpRendererCommandAccess,
|
||||
CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch, CdpRendererOwnerTurnOutcome,
|
||||
CdpSchedulerEvent, CdpTargetHostLifecycleDelta, CdpTargetHostLifecycleObserver, CdpTurnOutcome,
|
||||
CommandDispatchContext, CommandResponseFlushContext, CommandResponseFlushPermit,
|
||||
CompletedCdpCommandDispatch, CompletedDeferredMainDocumentLoadCompletion,
|
||||
CompletedRuntimeProtocolMessageDispatch, DEFAULT_CDP_PAGE_TARGET_ID, DEFAULT_CDP_TAB_TARGET_ID,
|
||||
@@ -39,9 +39,10 @@ pub use domains::activity::{
|
||||
RuntimeCommandOutputBarrierTerminal, RuntimeCommandOutputBarriers,
|
||||
};
|
||||
pub use domains::page::{
|
||||
BackgroundNavigationCompletion, CompletedPageScreencastCapture,
|
||||
PageScreencastCaptureCompletion, PageScreencastCaptureStart, PageScreencastRegistration,
|
||||
PageScreencastSubscriptionStatus, PendingPageScreencastCapture, build_default_raster_pdf,
|
||||
BackgroundDocumentContinuationCompletion, BackgroundNavigationCompletion,
|
||||
CompletedPageScreencastCapture, PageScreencastCaptureCompletion, PageScreencastCaptureStart,
|
||||
PageScreencastRegistration, PageScreencastSubscriptionStatus, PendingPageScreencastCapture,
|
||||
build_default_raster_pdf,
|
||||
};
|
||||
pub use domains::runtime::{
|
||||
CompletedDevToolsRuntimeCommandDispatch, DevToolsRuntimeCommandTaskStep,
|
||||
|
||||
@@ -9,11 +9,29 @@
|
||||
use moli_core::{RendererDocumentLifecycleIdentity, RendererOutputResidenceIdentity};
|
||||
|
||||
use crate::{
|
||||
DeferredMainDocumentLoadCompletionOutputInterest, DeferredMainDocumentLoadObservationId,
|
||||
ProtocolSchedulerWork,
|
||||
BackgroundNavigationGateKey, DeferredMainDocumentLoadCompletionOutputInterest,
|
||||
DeferredMainDocumentLoadObservationId, ProtocolSchedulerWork,
|
||||
conn::{CdpConnection, DocumentNavigationToken, RendererPageResidenceIdentity},
|
||||
};
|
||||
|
||||
/// Constructs the exact key carried by a synthetic Document-continuation
|
||||
/// completion in scheduler-only tests.
|
||||
pub fn background_navigation_gate_key(
|
||||
target_id: Option<String>,
|
||||
session_id: Option<String>,
|
||||
frame_id: String,
|
||||
loader_id: String,
|
||||
navigation_request_id: Option<u64>,
|
||||
) -> BackgroundNavigationGateKey {
|
||||
BackgroundNavigationGateKey::from_test_parts(
|
||||
target_id,
|
||||
session_id,
|
||||
frame_id,
|
||||
loader_id,
|
||||
navigation_request_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Opaque exact-token fixture for scheduler tests that need a real
|
||||
/// target-owned background navigation request.
|
||||
pub struct BackgroundNavigationRequestFixture {
|
||||
|
||||
+310
-31
@@ -4,7 +4,7 @@
|
||||
//! focused assertions over emitted results, events, and errors.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::conn::{
|
||||
@@ -53,7 +53,15 @@ pub struct TestContext {
|
||||
tokio::sync::mpsc::UnboundedSender<crate::domains::page::BackgroundNavigationCompletion>,
|
||||
background_navigation_completion_rx:
|
||||
tokio::sync::mpsc::UnboundedReceiver<crate::domains::page::BackgroundNavigationCompletion>,
|
||||
document_continuation_completion_tx: tokio::sync::mpsc::UnboundedSender<
|
||||
crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
>,
|
||||
document_continuation_completion_rx: tokio::sync::mpsc::UnboundedReceiver<
|
||||
crate::domains::page::BackgroundDocumentContinuationCompletion,
|
||||
>,
|
||||
document_continuation_scheduler_explicitly_enabled: bool,
|
||||
background_navigation_scheduler_enabled: bool,
|
||||
pending_document_continuation_gates: HashSet<super::conn::BackgroundNavigationGateKey>,
|
||||
}
|
||||
|
||||
struct PendingTestRuntimeDeferredReply {
|
||||
@@ -209,8 +217,13 @@ impl TestContext {
|
||||
let (background_event_tx, background_event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (background_navigation_completion_tx, background_navigation_completion_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
let (document_continuation_completion_tx, document_continuation_completion_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
conn.set_renderer_publication_sender(renderer_publication_tx);
|
||||
conn.set_runtime_inspector_response_ready_sender(runtime_inspector_response_ready_tx);
|
||||
conn.set_document_continuation_completion_sender(
|
||||
document_continuation_completion_tx.clone(),
|
||||
);
|
||||
Self {
|
||||
conn,
|
||||
sent: Vec::new(),
|
||||
@@ -223,7 +236,11 @@ impl TestContext {
|
||||
background_event_rx,
|
||||
background_navigation_completion_tx,
|
||||
background_navigation_completion_rx,
|
||||
document_continuation_completion_tx,
|
||||
document_continuation_completion_rx,
|
||||
document_continuation_scheduler_explicitly_enabled: false,
|
||||
background_navigation_scheduler_enabled: false,
|
||||
pending_document_continuation_gates: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +256,7 @@ impl TestContext {
|
||||
if self.background_navigation_scheduler_enabled {
|
||||
return;
|
||||
}
|
||||
self.enable_document_continuation_scheduler_for_test();
|
||||
self.conn
|
||||
.set_background_event_sender(self.background_event_tx.clone());
|
||||
self.conn.set_background_navigation_completion_sender(
|
||||
@@ -247,6 +265,20 @@ impl TestContext {
|
||||
self.background_navigation_scheduler_enabled = true;
|
||||
}
|
||||
|
||||
/// Enables only the typed post-commit continuation lane.
|
||||
///
|
||||
/// Most compatibility tests keep foreground navigation synchronous through
|
||||
/// its first stable renderer-owner boundary. Tests that intentionally park
|
||||
/// author script execution behind Debugger control need the command reply
|
||||
/// to remain independently routable so the controlling command can resume
|
||||
/// that same continuation.
|
||||
pub(crate) fn enable_document_continuation_scheduler_for_test(&mut self) {
|
||||
self.conn.set_document_continuation_completion_sender(
|
||||
self.document_continuation_completion_tx.clone(),
|
||||
);
|
||||
self.document_continuation_scheduler_explicitly_enabled = true;
|
||||
}
|
||||
|
||||
/// Loads and installs one production-shaped navigation fixture for the
|
||||
/// exact Page target addressed by `session_id`.
|
||||
///
|
||||
@@ -409,6 +441,39 @@ impl TestContext {
|
||||
let response_start = self.sent.len();
|
||||
Box::pin(self.process_parsed_command_like_scheduler(&command, true)).await;
|
||||
Box::pin(self.wait_for_test_command_response(command_id, response_start)).await;
|
||||
if !self.document_continuation_scheduler_explicitly_enabled
|
||||
&& test_command_starts_document_navigation(&command)
|
||||
{
|
||||
Box::pin(self.wait_for_document_continuation_for_test(
|
||||
command.session_id(),
|
||||
"foreground navigation command continuation",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn wait_for_document_continuation_for_test(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
description: &str,
|
||||
) {
|
||||
while self
|
||||
.conn
|
||||
.has_pending_document_navigation_for_session_owner(session_id)
|
||||
|| self
|
||||
.pending_document_continuation_gates
|
||||
.iter()
|
||||
.any(|key| key.matches_session_owner(session_id))
|
||||
{
|
||||
assert!(
|
||||
matches!(
|
||||
Box::pin(self.wait_for_one_test_scheduler_turn()).await,
|
||||
TestSchedulerTurnOutcome::Processed(_)
|
||||
),
|
||||
"test scheduler lost all input while waiting for {description}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -687,11 +752,106 @@ impl TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors the external-load wait used by the WebDriver adapters for one
|
||||
/// protocol-neutral navigation command.
|
||||
///
|
||||
/// A raw `CdpConnection` returns at the command boundary. The production
|
||||
/// scheduler then waits for the exact target's typed Document continuation
|
||||
/// and main-Document load residence before exposing a `wait: Load` result.
|
||||
/// Direct-command tests must drive that same boundary rather than relying
|
||||
/// on the renderer owner happening to finish first.
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn wait_for_direct_navigation_load_for_test(
|
||||
&mut self,
|
||||
context: &crate::devtools_runtime::DevToolsCommandContext,
|
||||
expected_loader_id: &str,
|
||||
description: &str,
|
||||
) {
|
||||
while self
|
||||
.conn
|
||||
.has_inflight_background_navigation_for_devtools_context(context)
|
||||
|| self
|
||||
.pending_document_continuation_gates
|
||||
.iter()
|
||||
.any(|key| navigation_gate_matches_devtools_context(key, context))
|
||||
{
|
||||
assert!(
|
||||
matches!(
|
||||
Box::pin(self.wait_for_one_test_scheduler_turn()).await,
|
||||
TestSchedulerTurnOutcome::Processed(_)
|
||||
),
|
||||
"test scheduler lost all external input while waiting for {description}"
|
||||
);
|
||||
}
|
||||
|
||||
let wait_key = self
|
||||
.conn
|
||||
.capture_devtools_document_lifecycle_wait_key(
|
||||
context,
|
||||
expected_loader_id,
|
||||
moli_core::page::RendererDocumentLifecycleMilestone::Load,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"failed to capture the exact load lifecycle waiter for {description}; loader={expected_loader_id}"
|
||||
)
|
||||
});
|
||||
let lifecycle_state = tokio::time::timeout(TEST_SCHEDULER_INPUT_TIMEOUT, async {
|
||||
loop {
|
||||
let state = self
|
||||
.conn
|
||||
.devtools_document_lifecycle_wait_state(context, &wait_key);
|
||||
if state != crate::conn::DevToolsDocumentLifecycleWaitState::Pending {
|
||||
return state;
|
||||
}
|
||||
assert!(
|
||||
matches!(
|
||||
Box::pin(self.wait_for_one_test_scheduler_turn()).await,
|
||||
TestSchedulerTurnOutcome::Processed(_)
|
||||
),
|
||||
"test scheduler lost all external input while waiting for {description}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("timed out waiting for {description}"));
|
||||
let _ = self
|
||||
.conn
|
||||
.release_devtools_document_lifecycle_wait_key(context, &wait_key);
|
||||
assert_eq!(
|
||||
lifecycle_state,
|
||||
crate::conn::DevToolsDocumentLifecycleWaitState::Reached,
|
||||
"exact load lifecycle did not reach its requested milestone for {description}"
|
||||
);
|
||||
|
||||
let waited = tokio::time::timeout(TEST_SCHEDULER_INPUT_TIMEOUT, async {
|
||||
while self.pending_protocol_scheduler_work.iter().any(|work| {
|
||||
work.observes_main_document_load_for_devtools_context(&self.conn, context)
|
||||
}) {
|
||||
assert!(
|
||||
matches!(
|
||||
Box::pin(self.wait_for_one_test_scheduler_turn()).await,
|
||||
TestSchedulerTurnOutcome::Processed(_)
|
||||
),
|
||||
"test scheduler lost all external input while waiting for {description}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if waited.is_err() {
|
||||
panic!(
|
||||
"timed out waiting for {description}; pending protocol work={:?}",
|
||||
self.pending_protocol_scheduler_work
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_parsed_command_like_scheduler(
|
||||
&mut self,
|
||||
command: &ParsedCdpCommand,
|
||||
drain_after_command: bool,
|
||||
) -> Vec<CdpSchedulerEvent> {
|
||||
Box::pin(self.wait_for_document_navigation_gate_before_command(command)).await;
|
||||
let output_session_id = command.command_output_session_id().map(str::to_owned);
|
||||
let mut protocol_events = Vec::new();
|
||||
let mut scheduler_events = Vec::new();
|
||||
@@ -767,6 +927,28 @@ impl TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_document_navigation_gate_before_command(
|
||||
&mut self,
|
||||
command: &ParsedCdpCommand,
|
||||
) {
|
||||
if !command.waits_for_document_continuation_to_finish() {
|
||||
return;
|
||||
}
|
||||
while self
|
||||
.pending_document_continuation_gates
|
||||
.iter()
|
||||
.any(|key| key.matches_session_owner(command.session_id()))
|
||||
{
|
||||
assert!(
|
||||
matches!(
|
||||
Box::pin(self.wait_for_one_test_scheduler_turn()).await,
|
||||
TestSchedulerTurnOutcome::Processed(_)
|
||||
),
|
||||
"CDP command waiting for the committed Document continuation lost all scheduler input"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn admit_runtime_output_barrier(
|
||||
&mut self,
|
||||
command: &ParsedCdpCommand,
|
||||
@@ -1174,6 +1356,12 @@ impl TestContext {
|
||||
Box::pin(self.route_protocol_events_like_scheduler(prefix, work)).await;
|
||||
}
|
||||
|
||||
if let Some(key) = completion.document_continuation_gate_key() {
|
||||
assert!(
|
||||
self.pending_document_continuation_gates.remove(&key),
|
||||
"document-continuation completion must settle an admitted background navigation gate"
|
||||
);
|
||||
}
|
||||
let outcome = self
|
||||
.conn
|
||||
.drain_background_navigation_completion_turn_async(completion)
|
||||
@@ -1187,8 +1375,8 @@ impl TestContext {
|
||||
renderer_output_predecessor,
|
||||
) = outcome.into_renderer_owner_turn_parts();
|
||||
assert!(
|
||||
renderer_output_predecessor.is_none(),
|
||||
"background navigation completion must use an insertion boundary"
|
||||
renderer_output_boundary.is_none() || renderer_output_predecessor.is_none(),
|
||||
"background navigation completion cannot carry both an insertion boundary and a continuation predecessor"
|
||||
);
|
||||
if !completion_prefix.is_empty() {
|
||||
Box::pin(self.route_protocol_events_like_scheduler(
|
||||
@@ -1197,9 +1385,8 @@ impl TestContext {
|
||||
))
|
||||
.await;
|
||||
}
|
||||
if let Some(boundary) = renderer_output_boundary {
|
||||
Box::pin(self.route_renderer_output_predecessor_before_command_response(boundary))
|
||||
.await;
|
||||
if let Some(fence) = renderer_output_boundary.or(renderer_output_predecessor) {
|
||||
Box::pin(self.route_renderer_output_predecessor_before_command_response(fence)).await;
|
||||
}
|
||||
completion_suffix.append(&mut post_response_events);
|
||||
while let Ok(event) = self.background_event_rx.try_recv() {
|
||||
@@ -1215,7 +1402,15 @@ impl TestContext {
|
||||
|
||||
async fn run_one_ready_test_scheduler_turn(&mut self) -> TestSchedulerTurnOutcome {
|
||||
let mut work = VecDeque::new();
|
||||
let input_kind = if self.background_navigation_scheduler_enabled
|
||||
let input_kind = if let Ok(completion) = self.document_continuation_completion_rx.try_recv()
|
||||
{
|
||||
work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(
|
||||
crate::domains::page::BackgroundNavigationCompletion::document_continuation(
|
||||
completion,
|
||||
),
|
||||
));
|
||||
TestSchedulerInputKind::BackgroundNavigationCompletion
|
||||
} else if self.background_navigation_scheduler_enabled
|
||||
&& let Ok(completion) = self.background_navigation_completion_rx.try_recv()
|
||||
{
|
||||
work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(
|
||||
@@ -1325,6 +1520,15 @@ impl TestContext {
|
||||
work.push_back(TestSchedulerWork::RuntimeDeferredReplyReady(response));
|
||||
TestSchedulerInputKind::RuntimeDeferredReply
|
||||
}
|
||||
maybe_continuation = self.document_continuation_completion_rx.recv() => {
|
||||
let Some(completion) = maybe_continuation else {
|
||||
return TestSchedulerTurnOutcome::Idle;
|
||||
};
|
||||
work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(
|
||||
crate::domains::page::BackgroundNavigationCompletion::document_continuation(completion),
|
||||
));
|
||||
TestSchedulerInputKind::BackgroundNavigationCompletion
|
||||
}
|
||||
maybe_completion = self.background_navigation_completion_rx.recv(), if background_navigation_scheduler_enabled => {
|
||||
let Some(completion) = maybe_completion else {
|
||||
return TestSchedulerTurnOutcome::Idle;
|
||||
@@ -1350,6 +1554,15 @@ impl TestContext {
|
||||
} else {
|
||||
tokio::select! {
|
||||
biased;
|
||||
maybe_continuation = self.document_continuation_completion_rx.recv() => {
|
||||
let Some(completion) = maybe_continuation else {
|
||||
return TestSchedulerTurnOutcome::Idle;
|
||||
};
|
||||
work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(
|
||||
crate::domains::page::BackgroundNavigationCompletion::document_continuation(completion),
|
||||
));
|
||||
TestSchedulerInputKind::BackgroundNavigationCompletion
|
||||
}
|
||||
maybe_completion = self.background_navigation_completion_rx.recv(), if background_navigation_scheduler_enabled => {
|
||||
let Some(completion) = maybe_completion else {
|
||||
return TestSchedulerTurnOutcome::Idle;
|
||||
@@ -1485,7 +1698,17 @@ impl TestContext {
|
||||
work: &mut VecDeque<TestSchedulerWork>,
|
||||
) {
|
||||
let mut queue = VecDeque::new();
|
||||
enqueue_scheduler_events_like_scheduler(&mut queue, scheduler_events);
|
||||
for event in scheduler_events {
|
||||
match event {
|
||||
CdpSchedulerEvent::DocumentContinuationStarted { key } => {
|
||||
self.pending_document_continuation_gates.insert(key);
|
||||
}
|
||||
CdpSchedulerEvent::ProtocolWorkPublished { work } => {
|
||||
queue.push_back(TestDeferredSchedulerWork(work));
|
||||
}
|
||||
CdpSchedulerEvent::PageScreencastStarted { .. } => {}
|
||||
}
|
||||
}
|
||||
while let Some(TestDeferredSchedulerWork(protocol_work)) = queue.pop_front() {
|
||||
self.pending_protocol_scheduler_work
|
||||
.push_back(protocol_work);
|
||||
@@ -1501,8 +1724,16 @@ impl TestContext {
|
||||
work: &mut VecDeque<TestSchedulerWork>,
|
||||
) {
|
||||
loop {
|
||||
let can_execute = |work: &ProtocolSchedulerWork| {
|
||||
self.background_navigation_scheduler_enabled
|
||||
|| !work.requires_background_navigation_scheduler()
|
||||
};
|
||||
let front_is_fixture_parked = self
|
||||
.pending_protocol_scheduler_work
|
||||
.front()
|
||||
.is_some_and(|work| work.is_ready() && !can_execute(work));
|
||||
let selected_index = match self.pending_protocol_scheduler_work.front() {
|
||||
Some(front) if front.is_ready() => 0,
|
||||
Some(front) if front.is_ready() && can_execute(front) => 0,
|
||||
Some(front)
|
||||
if front.kind() == ProtocolSchedulerWorkKind::MainDocumentLoadOwnerAction =>
|
||||
{
|
||||
@@ -1511,36 +1742,51 @@ impl TestContext {
|
||||
.iter()
|
||||
.position(|candidate| {
|
||||
candidate.is_ready()
|
||||
&& candidate.is_top_level_location_navigation_owner_action()
|
||||
&& can_execute(candidate)
|
||||
&& (candidate.is_top_level_location_navigation_owner_action()
|
||||
|| candidate.is_command_followup())
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Production checks the pending load observer out of the
|
||||
// FIFO while it waits for the renderer. An unconstrained
|
||||
// location owner action may then run and replace that
|
||||
// exact source Document, which completes the observer as
|
||||
// Superseded. Keeping the pending observer at the front in
|
||||
// this protocol-only harness would deadlock the action
|
||||
// needed to make it terminal.
|
||||
// FIFO while it waits for the renderer. A navigation owner
|
||||
// action may then replace that exact source Document and
|
||||
// make the observer terminal.
|
||||
index
|
||||
}
|
||||
Some(_) | None => return,
|
||||
Some(_) if front_is_fixture_parked => {
|
||||
let Some(index) = self
|
||||
.pending_protocol_scheduler_work
|
||||
.iter()
|
||||
.position(|candidate| candidate.is_ready() && can_execute(candidate))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
index
|
||||
}
|
||||
Some(_) => {
|
||||
let Some(index) =
|
||||
self.pending_protocol_scheduler_work
|
||||
.iter()
|
||||
.position(|candidate| {
|
||||
candidate.is_ready()
|
||||
&& can_execute(candidate)
|
||||
&& candidate.is_command_followup()
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
index
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
if !self.background_navigation_scheduler_enabled
|
||||
&& self
|
||||
.pending_protocol_scheduler_work
|
||||
.get(selected_index)
|
||||
.is_some_and(ProtocolSchedulerWork::requires_background_navigation_scheduler)
|
||||
{
|
||||
// The default protocol fixture has no owner task lane. Keep
|
||||
// independent popup navigation resident rather than invoking
|
||||
// the production function's synchronous fallback while the
|
||||
// exact renderer cursor is still being projected. Tests that
|
||||
// assert navigation progress opt into the production-shaped
|
||||
// background scheduler and drive its typed completions.
|
||||
return;
|
||||
}
|
||||
// The default protocol fixture has no owner task lane. Keep an
|
||||
// independent popup navigation resident instead of invoking its
|
||||
// synchronous fallback while the opener cursor is projected, but
|
||||
// do not let that fixture-only limitation block ready work for a
|
||||
// different target. Tests that assert popup navigation progress
|
||||
// opt into the production-shaped background scheduler.
|
||||
let protocol_work = self
|
||||
.pending_protocol_scheduler_work
|
||||
.remove(selected_index)
|
||||
@@ -1830,6 +2076,29 @@ impl TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn navigation_gate_matches_devtools_context(
|
||||
key: &super::conn::BackgroundNavigationGateKey,
|
||||
context: &crate::devtools_runtime::DevToolsCommandContext,
|
||||
) -> bool {
|
||||
if let Some(target_id) = context.target_id.as_ref() {
|
||||
return key.target_id() == Some(target_id.as_str());
|
||||
}
|
||||
key.matches_session_owner(
|
||||
context
|
||||
.session_id
|
||||
.as_ref()
|
||||
.map(|session_id| session_id.as_str()),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_command_starts_document_navigation(command: &ParsedCdpCommand) -> bool {
|
||||
matches!(
|
||||
command.method(),
|
||||
"Page.navigate" | "Page.reload" | "Page.navigateToHistoryEntry"
|
||||
)
|
||||
}
|
||||
|
||||
fn protocol_events_into_messages(events: Vec<BackgroundProtocolEvent>) -> Vec<Value> {
|
||||
events
|
||||
.into_iter()
|
||||
@@ -1925,6 +2194,7 @@ fn enqueue_scheduler_events_like_scheduler(
|
||||
queue.push_back(TestDeferredSchedulerWork(work));
|
||||
}
|
||||
CdpSchedulerEvent::PageScreencastStarted { .. } => {}
|
||||
CdpSchedulerEvent::DocumentContinuationStarted { key: _ } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2150,8 +2420,13 @@ mod tests {
|
||||
let (background_event_tx, background_event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (background_navigation_completion_tx, background_navigation_completion_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
let (document_continuation_completion_tx, document_continuation_completion_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
conn.set_renderer_publication_sender(publication_tx.clone());
|
||||
conn.set_runtime_inspector_response_ready_sender(runtime_response_tx);
|
||||
conn.set_document_continuation_completion_sender(
|
||||
document_continuation_completion_tx.clone(),
|
||||
);
|
||||
let mut ctx = TestContext {
|
||||
conn,
|
||||
sent: Vec::new(),
|
||||
@@ -2164,7 +2439,11 @@ mod tests {
|
||||
background_event_rx,
|
||||
background_navigation_completion_tx,
|
||||
background_navigation_completion_rx,
|
||||
document_continuation_completion_tx,
|
||||
document_continuation_completion_rx,
|
||||
document_continuation_scheduler_explicitly_enabled: false,
|
||||
background_navigation_scheduler_enabled: false,
|
||||
pending_document_continuation_gates: HashSet::new(),
|
||||
};
|
||||
let opened = |page_id| {
|
||||
RendererOutputTransportMessage::from(RendererOutputStreamControl::Opened {
|
||||
|
||||
@@ -28,10 +28,10 @@ pub(crate) struct BrowsingContextId {
|
||||
|
||||
/// Stable identity of the V8/script execution agent hosting one or more realms.
|
||||
///
|
||||
/// The current production policy assigns one agent to each concurrently live
|
||||
/// top-level Page, while nested realms and same-Page navigation generations
|
||||
/// reuse that agent. Related auxiliary Pages may share an id only after the
|
||||
/// selective shared-agent admission path is implemented.
|
||||
/// Fresh top-level Pages receive a new agent, while nested realms and
|
||||
/// same-Page navigation generations reuse their Page's agent. An explicitly
|
||||
/// related auxiliary Page may join its opener's agent; unrelated and
|
||||
/// opener-suppressed Pages stay isolated.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub(crate) struct ScriptAgentId(u64);
|
||||
|
||||
@@ -47,13 +47,9 @@ impl ScriptAgentId {
|
||||
}
|
||||
|
||||
/// Admission scope currently retaining a script agent.
|
||||
///
|
||||
/// `RelatedPageGroup` is used only by the Phase 2 feasibility path until
|
||||
/// auxiliary browsing-context admission becomes a production policy.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ScriptAgentScope {
|
||||
PageScriptEnvironment,
|
||||
#[cfg(test)]
|
||||
RelatedPageGroup,
|
||||
}
|
||||
|
||||
@@ -61,7 +57,6 @@ impl ScriptAgentScope {
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::PageScriptEnvironment => "page-script-environment",
|
||||
#[cfg(test)]
|
||||
Self::RelatedPageGroup => "related-page-group",
|
||||
}
|
||||
}
|
||||
@@ -83,7 +78,6 @@ impl BrowsingContextId {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const fn auxiliary_top_level(value: u64) -> Self {
|
||||
assert!(value != 0, "auxiliary browsing-context id must be non-zero");
|
||||
Self {
|
||||
@@ -96,7 +90,6 @@ impl BrowsingContextId {
|
||||
self.kind
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const fn value(self) -> u64 {
|
||||
self.value
|
||||
}
|
||||
|
||||
@@ -353,11 +353,13 @@ use self::range_live::{
|
||||
update_live_ranges_for_text_split,
|
||||
};
|
||||
pub(super) use self::runtime_state::finish_context_bootstrap;
|
||||
pub(crate) use self::runtime_state::inherit_auxiliary_window_viewport_surface;
|
||||
pub(crate) use self::runtime_state::install_child_window_eval_runtime_state;
|
||||
pub(crate) use self::runtime_state::install_webassembly_runtime_state;
|
||||
#[cfg(feature = "wpt-extensions")]
|
||||
pub(crate) use self::runtime_state::install_wpt_webdriver_runtime_state;
|
||||
pub(crate) use self::runtime_state::set_window_origin_runtime_state;
|
||||
pub(crate) use self::runtime_state::window_origin_runtime_state;
|
||||
pub(crate) use self::runtime_state::{
|
||||
ORIGINAL_WEBASSEMBLY_COMPILE_ERROR_CONSTRUCTOR_SLOT,
|
||||
ORIGINAL_WEBASSEMBLY_GLOBAL_VALUE_GETTER_SLOT, ORIGINAL_WEBASSEMBLY_INSTANCE_CONSTRUCTOR_SLOT,
|
||||
@@ -371,7 +373,7 @@ pub(in crate::context_bootstrap) use self::shared::*;
|
||||
pub(crate) use self::shared::{
|
||||
CHILD_BROWSING_CONTEXT_HANDLE_SLOT, DOCUMENT_SELECTION_CHANGE_LISTENER_SLOT,
|
||||
READABLE_STREAM_CHILD_REALM_HANDLED_REJECTION_SLOT, WINDOW_CUSTOM_ELEMENTS_SLOT,
|
||||
WINDOW_NAME_SLOT,
|
||||
WINDOW_NAME_SLOT, WINDOW_OPENER_SLOT,
|
||||
};
|
||||
pub(crate) use self::shared::{
|
||||
RuntimeMessageSourceSecurity, current_runtime_message_agent_cluster,
|
||||
|
||||
@@ -23,8 +23,8 @@ use super::navigation_result::{
|
||||
};
|
||||
use super::navigation_serialize::sync_child_navigation_entry_seed_from_owner;
|
||||
use super::navigation_window::{
|
||||
runtime_window_is_global, runtime_window_owner, window_location_for_holder,
|
||||
window_navigation_for_holder,
|
||||
navigation_document_is_initial_empty, runtime_window_is_global, runtime_window_owner,
|
||||
url_is_about_blank_document, window_location_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
@@ -140,7 +140,7 @@ fn mutate_history_object<'s>(
|
||||
);
|
||||
return;
|
||||
};
|
||||
if !moli_url::same_origin(&url, ¤t_url) {
|
||||
if !history_target_is_same_origin(scope, owner, ¤t_href, ¤t_url, &url) {
|
||||
throw_history_security_error(
|
||||
scope,
|
||||
"Failed to execute 'pushState' or 'replaceState' on 'History': A history state object with URL of a different origin cannot be created in a document with origin.",
|
||||
@@ -178,6 +178,18 @@ fn mutate_history_object<'s>(
|
||||
navigate_outcome = Some(outcome);
|
||||
}
|
||||
|
||||
// The initial empty Document remains initial across same-document URL
|
||||
// mutations. The URL and history update steps therefore convert pushState
|
||||
// to replacement until a different Document commits (or document.open()
|
||||
// explicitly exits the initial state).
|
||||
let kind = if matches!(kind, HistoryMutationKind::Push)
|
||||
&& navigation_document_is_initial_empty(scope, owner)
|
||||
{
|
||||
HistoryMutationKind::Replace
|
||||
} else {
|
||||
kind
|
||||
};
|
||||
|
||||
let entries = history_entries(scope, history).unwrap_or_else(|| v8::Array::new(scope, 0));
|
||||
let current_index = history_index(scope, history);
|
||||
let current_navigation_index = navigation_current_entry_index(scope, owner).unwrap_or(0);
|
||||
@@ -350,18 +362,49 @@ fn history_same_origin_reference_url<'s>(
|
||||
if history_url_inherits_origin(scope, owner, current_href)
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
return Ok(unsafe { &mut *host_ptr }.host_document().url().clone());
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
return Ok(host
|
||||
.dom_host()
|
||||
.document_base_url()
|
||||
.unwrap_or_else(|| host.host_document().url().clone()));
|
||||
}
|
||||
url::Url::parse(current_href)
|
||||
}
|
||||
|
||||
fn history_target_is_same_origin<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
current_href: &str,
|
||||
current_url: &url::Url,
|
||||
target_url: &url::Url,
|
||||
) -> bool {
|
||||
if !history_url_inherits_origin(scope, owner, current_href) {
|
||||
return moli_url::same_origin(target_url, current_url);
|
||||
}
|
||||
let Some(current_origin) = window_origin_runtime_state(scope, owner) else {
|
||||
return moli_url::same_origin(target_url, current_url);
|
||||
};
|
||||
if url_is_about_blank_document(target_url) {
|
||||
// An initial about:blank Document and its same-document URL variants
|
||||
// retain the creator origin, including a shared opaque identity that
|
||||
// cannot be reconstructed from either serialized URL.
|
||||
return true;
|
||||
}
|
||||
moli_url::origin_ascii_serialization(target_url) == current_origin
|
||||
}
|
||||
|
||||
fn history_url_inherits_origin<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
current_href: &str,
|
||||
) -> bool {
|
||||
!runtime_window_is_global(scope, owner)
|
||||
&& matches!(current_href, "about:blank" | "about:srcdoc")
|
||||
let current_url_inherits_origin = current_href == "about:srcdoc"
|
||||
|| url::Url::parse(current_href)
|
||||
.ok()
|
||||
.is_some_and(|url| url_is_about_blank_document(&url));
|
||||
current_url_inherits_origin
|
||||
&& (!runtime_window_is_global(scope, owner)
|
||||
|| navigation_document_is_initial_empty(scope, owner))
|
||||
}
|
||||
|
||||
fn throw_history_security_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
|
||||
|
||||
@@ -36,9 +36,10 @@ use super::navigation_seed::history_entry_seed_for_reload;
|
||||
use super::navigation_serialize::serialize_history_entries;
|
||||
use super::navigation_window::{
|
||||
child_browsing_context_handle_for_runtime_owner, navigation_document_can_update_current_entry,
|
||||
navigation_document_has_opaque_origin, navigation_unload_event_active,
|
||||
runtime_window_is_global, runtime_window_owner, runtime_window_uses_top_level_history_model,
|
||||
url_is_about_blank_document, window_history_for_holder, window_location_for_holder,
|
||||
navigation_document_has_opaque_origin, navigation_document_is_initial_empty,
|
||||
navigation_unload_event_active, runtime_window_is_global, runtime_window_owner,
|
||||
runtime_window_uses_top_level_history_model, url_is_about_blank_document,
|
||||
window_history_for_holder, window_location_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use crate::native_bridge::NavigationHistoryEntrySeed;
|
||||
@@ -305,6 +306,11 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
|
||||
super::navigation_window::window_navigation_for_holder(scope, owner)
|
||||
};
|
||||
let effective_kind = match kind {
|
||||
LocationNavigationKind::Assign
|
||||
if navigation_document_is_initial_empty(scope, owner) =>
|
||||
{
|
||||
LocationNavigationKind::Replace
|
||||
}
|
||||
LocationNavigationKind::Assign if source_element.is_some() && exact_same_href => {
|
||||
LocationNavigationKind::Replace
|
||||
}
|
||||
@@ -1193,6 +1199,9 @@ fn history_entry_seed_for_cross_document_location<'s>(
|
||||
let current_index = history_index(scope, history);
|
||||
let current_navigation_index = navigation_current_entry_index(scope, owner).unwrap_or(0);
|
||||
let mutation = match kind {
|
||||
LocationNavigationKind::Assign if navigation_document_is_initial_empty(scope, owner) => {
|
||||
NavigationHistoryMutation::Replace
|
||||
}
|
||||
LocationNavigationKind::Assign => NavigationHistoryMutation::Push,
|
||||
LocationNavigationKind::Replace => NavigationHistoryMutation::Replace,
|
||||
LocationNavigationKind::Reload => return None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::super::navigation_window::{
|
||||
navigation_document_has_opaque_origin, runtime_window_owner,
|
||||
};
|
||||
use super::super::window_origin_runtime_state;
|
||||
use super::helpers::{
|
||||
location_host_string, navigate_modified_location_url, parsed_location_url,
|
||||
require_location_href_slot, set_return_string, v8_value_to_string,
|
||||
@@ -355,7 +356,14 @@ fn location_attribute_getter<'s>(
|
||||
"null".to_owned()
|
||||
} else {
|
||||
parsed_location_url(scope, holder)
|
||||
.map(|url| moli_url::origin_ascii_serialization(&url))
|
||||
.map(|url| {
|
||||
if moli_url::is_about_blank(&url) {
|
||||
window_origin_runtime_state(scope, owner)
|
||||
.unwrap_or_else(|| moli_url::origin_ascii_serialization(&url))
|
||||
} else {
|
||||
moli_url::origin_ascii_serialization(&url)
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
set_return_string(scope, rv, &origin);
|
||||
|
||||
@@ -15,8 +15,8 @@ use super::navigation_serialize::{
|
||||
serialize_navigation_entry_object,
|
||||
};
|
||||
use super::navigation_window::{
|
||||
child_browsing_context_handle_for_runtime_owner, runtime_window_is_global,
|
||||
window_location_for_holder,
|
||||
child_browsing_context_handle_for_runtime_owner, navigation_document_is_initial_empty,
|
||||
runtime_window_is_global, window_location_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use moli_page_types::{NavigationHistoryMutation, cross_document_navigation_seed};
|
||||
@@ -53,11 +53,17 @@ pub(super) fn handle_navigation_navigate_cross_document<'s>(
|
||||
.iter()
|
||||
.find(|entry| entry.history_index == current_index)
|
||||
.is_some_and(|entry| entry.url == "about:blank");
|
||||
let replace_initial_empty_document = navigation_document_is_initial_empty(scope, owner);
|
||||
let mutation = match navigate_history_kind {
|
||||
NavigationNavigateHistoryKind::Push if replace_initial_empty_document => {
|
||||
NavigationHistoryMutation::Replace
|
||||
}
|
||||
NavigationNavigateHistoryKind::Push => NavigationHistoryMutation::Push,
|
||||
NavigationNavigateHistoryKind::Replace => NavigationHistoryMutation::Replace,
|
||||
NavigationNavigateHistoryKind::Default => {
|
||||
if current_href.as_deref() == Some("about:blank") && current_entry_is_about_blank {
|
||||
if replace_initial_empty_document
|
||||
|| (current_href.as_deref() == Some("about:blank") && current_entry_is_about_blank)
|
||||
{
|
||||
NavigationHistoryMutation::Replace
|
||||
} else {
|
||||
NavigationHistoryMutation::Push
|
||||
|
||||
@@ -29,8 +29,8 @@ use super::navigation_serialize::{
|
||||
sync_child_pending_navigation_entry_seed_from_owner,
|
||||
};
|
||||
use super::navigation_window::{
|
||||
runtime_top_window_owner, runtime_window_is_global, window_history_for_holder,
|
||||
window_location_for_holder, window_navigation_for_holder,
|
||||
navigation_document_is_initial_empty, runtime_top_window_owner, runtime_window_is_global,
|
||||
window_history_for_holder, window_location_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use crate::native_bridge::NavigationActivationSeed;
|
||||
|
||||
@@ -4,8 +4,13 @@ pub(in crate::context_bootstrap) fn update_navigation_current_entry_for_same_doc
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
href: &str,
|
||||
kind: LocationNavigationKind,
|
||||
mut kind: LocationNavigationKind,
|
||||
) {
|
||||
if matches!(kind, LocationNavigationKind::Assign)
|
||||
&& navigation_document_is_initial_empty(scope, owner)
|
||||
{
|
||||
kind = LocationNavigationKind::Replace;
|
||||
}
|
||||
let Some(history) = window_history_for_holder(scope, owner) else {
|
||||
return;
|
||||
};
|
||||
@@ -130,9 +135,14 @@ pub(in crate::context_bootstrap) fn apply_navigation_navigate_same_document<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
href: &str,
|
||||
kind: LocationNavigationKind,
|
||||
mut kind: LocationNavigationKind,
|
||||
navigation_state: Option<v8::Local<'s, v8::Value>>,
|
||||
) {
|
||||
if matches!(kind, LocationNavigationKind::Assign)
|
||||
&& navigation_document_is_initial_empty(scope, owner)
|
||||
{
|
||||
kind = LocationNavigationKind::Replace;
|
||||
}
|
||||
let Some(history) = window_history_for_holder(scope, owner) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -44,6 +44,15 @@ pub(super) fn runtime_window_is_global<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn navigation_document_is_initial_empty<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
runtime_window_is_global(scope, owner)
|
||||
&& context_host_ptr_from_global_bridge(scope)
|
||||
.is_some_and(|host_ptr| unsafe { &*host_ptr }.root_document_is_initial_empty())
|
||||
}
|
||||
|
||||
pub(super) fn runtime_window_uses_top_level_history_model<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -60,6 +60,13 @@ pub(crate) const ORIGINAL_WEBASSEMBLY_GLOBAL_VALUE_GETTER_SLOT: &str =
|
||||
const WINDOW_INDEXED_DB_SURFACE_SLOT: &str = "moli.Window.indexedDB";
|
||||
const WINDOW_ORIGIN_RUNTIME_SLOT: &str = "__moliWindowOriginRuntime";
|
||||
const WINDOW_INTRINSIC_EVAL_SLOT: &str = "__moliWindowIntrinsicEval";
|
||||
const AUXILIARY_WINDOW_VIEWPORT_SURFACE_PROPERTIES: &[&str] = &[
|
||||
"innerWidth",
|
||||
"innerHeight",
|
||||
"outerWidth",
|
||||
"outerHeight",
|
||||
"devicePixelRatio",
|
||||
];
|
||||
pub(in crate::context_bootstrap) const WINDOW_SECURE_CONTEXT_AVAILABLE_SLOT: &str =
|
||||
"__moliWindowSecureContextAvailable";
|
||||
|
||||
@@ -2060,6 +2067,30 @@ pub(crate) fn set_window_origin_runtime_state(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn inherit_auxiliary_window_viewport_surface<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
opener: v8::Local<'s, v8::Object>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
for property in AUXILIARY_WINDOW_VIEWPORT_SURFACE_PROPERTIES {
|
||||
let Some(value) = opener.get(scope, v8str(scope, property).into()) else {
|
||||
continue;
|
||||
};
|
||||
if value.is_number() {
|
||||
native_bridge::set_object_slot(scope, window, property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn window_origin_runtime_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
get_private_value(scope, window, WINDOW_ORIGIN_RUNTIME_SLOT)
|
||||
.and_then(|value| value.to_string(scope))
|
||||
.map(|value| value.to_rust_string_lossy(scope))
|
||||
}
|
||||
|
||||
pub(crate) fn install_webassembly_runtime_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
global: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(in crate::context_bootstrap) const WINDOW_SESSION_STORAGE_SLOT: &str =
|
||||
"__moliWindowSessionStorage";
|
||||
pub(crate) const WINDOW_CUSTOM_ELEMENTS_SLOT: &str = "__moliWindowCustomElements";
|
||||
pub(crate) const WINDOW_NAME_SLOT: &str = "__moliWindowName";
|
||||
pub(crate) const WINDOW_OPENER_SLOT: &str = "__moliWindowOpener";
|
||||
pub(crate) const CHILD_BROWSING_CONTEXT_HANDLE_SLOT: &str = "__moliChildBrowsingContextHandle";
|
||||
pub(in crate::context_bootstrap) const WINDOW_SELF_SLOT: &str = "__moliWindowSelf";
|
||||
pub(in crate::context_bootstrap) const WINDOW_PARENT_SLOT: &str = "__moliWindowParent";
|
||||
|
||||
@@ -104,9 +104,13 @@ pub(in crate::context_bootstrap) fn window_opener_getter<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if window_receiver(scope, &args).is_some() {
|
||||
rv.set_null();
|
||||
}
|
||||
let Some(receiver) = window_receiver(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
rv.set(
|
||||
window_hidden_value(scope, receiver, WINDOW_OPENER_SLOT)
|
||||
.unwrap_or_else(|| v8::null(scope).into()),
|
||||
);
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn window_inner_width_getter<'s>(
|
||||
|
||||
@@ -244,14 +244,21 @@ pub(crate) fn window_open_callback<'s>(
|
||||
opener_child_handle,
|
||||
&parsed.target_name,
|
||||
&url,
|
||||
Some(!suppress_opener),
|
||||
entered_base_url,
|
||||
creator_policy_container,
|
||||
)
|
||||
{
|
||||
let popup_id = opened_popup.popup_id;
|
||||
let session_storage_store = host.lightweight_popup_session_storage_store(popup_id);
|
||||
let initial_empty_document_storage_key =
|
||||
host.lightweight_popup_initial_empty_document_storage_key(popup_id);
|
||||
let session_storage_store = opened_popup
|
||||
.captured_session_storage_store
|
||||
.clone()
|
||||
.or_else(|| host.lightweight_popup_session_storage_store(popup_id));
|
||||
let initial_empty_document_storage_key = opened_popup
|
||||
.captured_initial_empty_document_storage_key
|
||||
.clone()
|
||||
.or_else(|| host.lightweight_popup_initial_empty_document_storage_key(popup_id));
|
||||
let pending_auxiliary_page = opened_popup.pending_auxiliary_page;
|
||||
let window_open_event = opened_popup
|
||||
.created_new_browsing_context
|
||||
.then_some(window_open_event);
|
||||
@@ -265,10 +272,8 @@ pub(crate) fn window_open_callback<'s>(
|
||||
parsed.target_name,
|
||||
popup_disposition,
|
||||
)
|
||||
.with_initial_auxiliary_state(
|
||||
session_storage_store,
|
||||
initial_empty_document_storage_key,
|
||||
),
|
||||
.with_initial_auxiliary_state(session_storage_store, initial_empty_document_storage_key)
|
||||
.with_pending_auxiliary_page(pending_auxiliary_page),
|
||||
window_open_event,
|
||||
);
|
||||
if suppress_opener {
|
||||
|
||||
@@ -503,6 +503,15 @@ impl DocumentRuntime {
|
||||
self.policy_container = policy;
|
||||
}
|
||||
|
||||
/// Installs the creator-derived policy state for a real initial-empty
|
||||
/// Document before its realm becomes observable to author code.
|
||||
pub(crate) fn set_initial_document_policy_container(
|
||||
&mut self,
|
||||
policy_container: DocumentPolicyContainer,
|
||||
) {
|
||||
self.policy_container = policy_container;
|
||||
}
|
||||
|
||||
pub(crate) fn set_bypass_content_security_policy(&mut self, bypass: bool) {
|
||||
self.bypass_content_security_policy = bypass;
|
||||
}
|
||||
|
||||
@@ -310,12 +310,16 @@ fn host_lightweight_popup_id_for_object_callback(
|
||||
args: v8::FunctionCallbackArguments<'_>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Ok(host_ptr) = context_host_ptr_from_callback_data(args.data()) else {
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
let Some(object) = args.get(0).to_object(scope) else {
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, object)
|
||||
else {
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let Some(popup_id) = host.lightweight_popup_id_for_window_proxy(scope, object) else {
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -231,6 +231,7 @@ pub use runtime::{
|
||||
RendererDevToolsAgentToken, RendererDevToolsMainCommandEnvelope, RendererDocumentBoxModel,
|
||||
RendererDocumentChildNodeSnapshotEvent, RendererDocumentChildNodeSnapshotEvents,
|
||||
RendererDocumentChildNodeSnapshots, RendererDocumentCommitPermit,
|
||||
RendererDocumentContinuationCompletion, RendererDocumentContinuationObserver,
|
||||
RendererDocumentFrontendNodeIdsResolution, RendererDocumentHitTestResult,
|
||||
RendererDocumentIsolateAccountingDiagnostics, RendererDocumentLifecycleEvent,
|
||||
RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity,
|
||||
@@ -276,8 +277,10 @@ pub use runtime::{
|
||||
RendererPageCookieFacadeSnapshotReply, RendererPageCreationArtifacts,
|
||||
RendererPageCreationDiagnostics, RendererPageCreationPhase, RendererPageCreationProgress,
|
||||
RendererPageDiagnosticsSnapshot, RendererPageDumpFormat, RendererPageDumpOptions,
|
||||
RendererPageDumpStripOptions, RendererPageHandle, RendererPageReply,
|
||||
RendererPageReservationToken, RendererPageState, RendererPageTestingHandle, RendererPageView,
|
||||
RendererPageDumpStripOptions, RendererPageHandle, RendererPageReplacementCommit,
|
||||
RendererPageReplacementCommitError, RendererPageReplacementCommitFailureDisposition,
|
||||
RendererPageReplacementReservationPending, RendererPageReply, RendererPageReservationToken,
|
||||
RendererPageState, RendererPageTestingHandle, RendererPageView, RendererPendingAuxiliaryPage,
|
||||
RendererPendingDownloadActivation, RendererPendingDownloadResponse,
|
||||
RendererPendingFileChooserActivation, RendererPendingJavaScriptDialog,
|
||||
RendererPendingPopupActivation, RendererPendingSameDocumentNavigation,
|
||||
|
||||
@@ -190,6 +190,26 @@ impl NativeBridgeBindings {
|
||||
v8::Local::new(scope, &self.window_global_template)
|
||||
}
|
||||
|
||||
/// Builds per-Context bridge bindings while the owning isolate is already
|
||||
/// entered by an outer native callback.
|
||||
///
|
||||
/// Related auxiliary Pages share the opener's script-agent isolate but
|
||||
/// require independent wrapper templates for their own `JsContextHost`.
|
||||
/// Reusing only the isolate-level Window templates is intentional; every
|
||||
/// mutable bridge/cache object below that boundary is rebuilt per realm.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub(crate) fn build_peer_in_scope(&self, scope: &mut v8::PinScope<'_, '_>) -> Self {
|
||||
let window_global_template = v8::Local::new(scope, &self.window_global_template);
|
||||
let cross_origin_window_global_template =
|
||||
v8::Local::new(scope, &self.cross_origin_window_global_template);
|
||||
Self::build(
|
||||
scope,
|
||||
self.isolate_ptr,
|
||||
window_global_template,
|
||||
cross_origin_window_global_template,
|
||||
)
|
||||
}
|
||||
|
||||
/// Install the native bridge global object with two internal fields:
|
||||
/// - field 0: `*mut JsContextHost` (for `runtime_ptr_from_object` compatibility)
|
||||
/// - field 1: `*const RefCell<JsContextHost>` from the per-context bridge-ref token
|
||||
@@ -353,6 +373,40 @@ impl NativeBridgeBindings {
|
||||
(global, v8::Global::new(scope, context))
|
||||
}
|
||||
|
||||
pub(super) fn instantiate_same_origin_window_proxy_shell<'s, 'i>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, 'i>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
) -> (v8::Local<'s, v8::Object>, v8::Global<v8::Context>) {
|
||||
// An accepted same-agent popup needs a real V8 global proxy before
|
||||
// protocol has materialized its auxiliary Page. A temporary context
|
||||
// owns that proxy synchronously; the related Page later detaches it
|
||||
// and supplies the exact object as ContextOptions::global_object.
|
||||
let global_template = v8::Local::new(scope, &self.window_global_template);
|
||||
let parent_security_token = scope.get_current_context().get_security_token(scope);
|
||||
let context = v8::Context::new(
|
||||
scope,
|
||||
v8::ContextOptions {
|
||||
global_template: Some(global_template),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
context.set_security_token(parent_security_token);
|
||||
crate::util::install_context_host_pointer_slot(context, host_ptr);
|
||||
{
|
||||
let facade_scope = &mut v8::ContextScope::new(scope, context);
|
||||
let global = context.global(facade_scope);
|
||||
crate::context_bootstrap::exposed_interfaces::capture_eager_intrinsic_interfaces(
|
||||
facade_scope,
|
||||
global,
|
||||
crate::context_bootstrap::exposed_interfaces::RealmKind::Window,
|
||||
)
|
||||
.expect("same-origin WindowProxy facade must initialize intrinsic interfaces");
|
||||
}
|
||||
let global = context.global(scope);
|
||||
(global, v8::Global::new(scope, context))
|
||||
}
|
||||
|
||||
pub(super) fn attach_window_proxy_shell_to_facade<'s, 'i>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, 'i, ()>,
|
||||
|
||||
@@ -350,7 +350,7 @@ impl ChildWindowProxyRecords {
|
||||
) -> bool {
|
||||
// A replacement Document reuses the browsing-context handle. Its context
|
||||
// binding must survive delayed retirement of the previous LocalWindow.
|
||||
let Some(record) = self.records.get_mut(&handle) else {
|
||||
let Some(record) = self.existing_record_mut(handle) else {
|
||||
return false;
|
||||
};
|
||||
if record.default_execution_context_id != Some(expected_execution_context_id) {
|
||||
|
||||
@@ -76,6 +76,78 @@ impl JsContextHost {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bind_page_script_environment(
|
||||
&mut self,
|
||||
environment: crate::script_vm::RendererPageScriptEnvironment,
|
||||
) {
|
||||
assert!(
|
||||
self.page_script_environment.is_none(),
|
||||
"context host Page script environment must be bound only once"
|
||||
);
|
||||
self.bind_auxiliary_page_reservation_allocator(
|
||||
environment.auxiliary_page_reservation_allocator(),
|
||||
);
|
||||
self.page_script_environment = Some(environment);
|
||||
}
|
||||
|
||||
pub(crate) fn bind_auxiliary_page_reservation_allocator(
|
||||
&mut self,
|
||||
allocator: RendererAuxiliaryPageReservationAllocator,
|
||||
) {
|
||||
assert!(
|
||||
self.auxiliary_page_reservation_allocator
|
||||
.replace(allocator)
|
||||
.is_none(),
|
||||
"context host auxiliary Page allocator must be bound only once"
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn reserve_pending_auxiliary_page(
|
||||
&self,
|
||||
exposes_opener: bool,
|
||||
) -> Option<RendererPendingAuxiliaryPage> {
|
||||
self.auxiliary_page_reservation_allocator
|
||||
.as_ref()
|
||||
.map(|allocator| allocator.reserve(exposes_opener))
|
||||
}
|
||||
|
||||
pub(crate) fn stage_pending_auxiliary_window_proxy(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
window_proxy: v8::Global<v8::Object>,
|
||||
facade_context: v8::Global<v8::Context>,
|
||||
inherits_creator_security_token: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let inherited_security_token = inherits_creator_security_token.then(|| {
|
||||
let facade_context = v8::Local::new(scope, &facade_context);
|
||||
v8::Global::new(scope, facade_context.get_security_token(scope))
|
||||
});
|
||||
self.auxiliary_page_reservation_allocator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("context host is missing its auxiliary Page allocator"))?
|
||||
.stage_related_window_proxy(
|
||||
pending,
|
||||
RendererStagedAuxiliaryWindowProxy::new(
|
||||
window_proxy,
|
||||
facade_context,
|
||||
inherited_security_token,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn stage_related_initial_empty_page_in_scope(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
init: crate::runtime::RendererRelatedInitialEmptyPageRealmInit,
|
||||
) -> anyhow::Result<()> {
|
||||
self.page_script_environment
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("context host is missing its Page script environment"))?
|
||||
.stage_related_initial_empty_page_in_scope(scope, &self.bridge.bindings, pending, init)
|
||||
}
|
||||
|
||||
pub(crate) fn install_page_default_context(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_, ()>,
|
||||
@@ -209,7 +281,10 @@ impl JsContextHost {
|
||||
#[cfg(test)]
|
||||
force_fresh_layout_reads_for_test: false,
|
||||
root_document_lifecycle: None,
|
||||
root_document_is_initial_empty: false,
|
||||
output_journal: None,
|
||||
auxiliary_page_reservation_allocator: None,
|
||||
page_script_environment: None,
|
||||
page_context_resources_closed: false,
|
||||
page_default_context: None,
|
||||
v8_finalizers: crate::v8_finalizer::V8FinalizerRegistry::default(),
|
||||
@@ -484,6 +559,18 @@ impl JsContextHost {
|
||||
self.root_document_lifecycle = Some(lifecycle);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_root_document_initial_empty(&mut self) {
|
||||
debug_assert!(
|
||||
moli_url::is_about_blank(self.document_url()),
|
||||
"only an about:blank root Document can be marked initial empty"
|
||||
);
|
||||
self.root_document_is_initial_empty = true;
|
||||
}
|
||||
|
||||
pub(crate) fn root_document_is_initial_empty(&self) -> bool {
|
||||
self.root_document_is_initial_empty
|
||||
}
|
||||
|
||||
/// Returns the exact root Document that owns Page-scoped protocol
|
||||
/// handoffs produced by this host.
|
||||
///
|
||||
@@ -499,6 +586,11 @@ impl JsContextHost {
|
||||
}
|
||||
|
||||
pub(crate) fn open_root_document(&mut self, scope: &mut v8::PinScope<'_, '_>) {
|
||||
// Blink's Document::open() explicitly makes an initial empty Document
|
||||
// cease to be initial. Keep this state on the Document owner rather
|
||||
// than inferring it from the current URL, which same-document
|
||||
// navigation is allowed to change.
|
||||
self.root_document_is_initial_empty = false;
|
||||
let descendant_count_before = self.child_browsing_contexts.len();
|
||||
for child_handle in self.top_level_child_browsing_context_handles_in_document_order() {
|
||||
self.drop_child_browsing_context_subtree_with_window_realm(scope, child_handle);
|
||||
|
||||
@@ -29,8 +29,10 @@ use crate::{
|
||||
reflector::ReflectorId,
|
||||
renderer_resource_scheduler::RendererResourceScheduler,
|
||||
runtime::{
|
||||
RendererBrowserContextRuntime, RendererDocumentLifecycleJournalHandle,
|
||||
RendererPageContextCancelReceiver, SharedRendererBackendNodeRegistry,
|
||||
RendererAuxiliaryPageReservationAllocator, RendererBrowserContextRuntime,
|
||||
RendererDocumentLifecycleJournalHandle, RendererPageContextCancelReceiver,
|
||||
RendererPendingAuxiliaryPage, RendererStagedAuxiliaryWindowProxy,
|
||||
SharedRendererBackendNodeRegistry,
|
||||
},
|
||||
script_provenance::CompiledStringProvenance,
|
||||
shared_worker_runtime::SharedWorkerClientEndpointOwner,
|
||||
@@ -208,6 +210,7 @@ pub(crate) use popups::{
|
||||
enter_top_level_lightweight_popup_scope, javascript_url_csp_source,
|
||||
lightweight_popup_id_from_window, restore_active_lightweight_popup_scope,
|
||||
restore_deferred_active_lightweight_popup_scope_if_present,
|
||||
set_renderer_owned_auxiliary_popup_id,
|
||||
};
|
||||
pub(crate) use range_records::{RangeBoundarySide, RangeRecordHandle};
|
||||
pub(crate) use runtime_observable::{
|
||||
@@ -810,7 +813,10 @@ pub(crate) struct JsContextHost {
|
||||
#[cfg(test)]
|
||||
force_fresh_layout_reads_for_test: bool,
|
||||
root_document_lifecycle: Option<RendererDocumentLifecycleJournalHandle>,
|
||||
root_document_is_initial_empty: bool,
|
||||
output_journal: Option<crate::runtime::RendererTurnOutputJournal>,
|
||||
auxiliary_page_reservation_allocator: Option<RendererAuxiliaryPageReservationAllocator>,
|
||||
page_script_environment: Option<crate::script_vm::RendererPageScriptEnvironment>,
|
||||
page_context_resources_closed: bool,
|
||||
page_default_context: Option<v8::Weak<v8::Context>>,
|
||||
pub(crate) v8_finalizers: crate::v8_finalizer::V8FinalizerRegistry,
|
||||
|
||||
@@ -4,8 +4,9 @@ use super::{
|
||||
use crate::{
|
||||
content_security_policy::content_security_policy_forces_opaque_origin,
|
||||
context_bootstrap::{
|
||||
SharedWebStorageStore, WINDOW_NAME_SLOT, apply_local_window_location_navigation,
|
||||
deep_clone_shared_web_storage_store, dispatch_simple_event_target_event,
|
||||
SharedWebStorageStore, WINDOW_NAME_SLOT, WINDOW_OPENER_SLOT,
|
||||
apply_local_window_location_navigation, deep_clone_shared_web_storage_store,
|
||||
dispatch_simple_event_target_event, inherit_auxiliary_window_viewport_surface,
|
||||
install_navigation_bootstrap_entry_for_holder, install_simple_event_target_methods,
|
||||
install_storage_aliases_for_window,
|
||||
install_window_location_history_navigation_runtime_state, new_shared_web_storage_store,
|
||||
@@ -32,7 +33,10 @@ use crate::{
|
||||
set_object_value,
|
||||
},
|
||||
},
|
||||
runtime::RendererPendingPopupActivation,
|
||||
runtime::{
|
||||
PageVmEnvConfig, RendererPendingAuxiliaryPage, RendererPendingPopupActivation,
|
||||
RendererRelatedInitialEmptyPageRealmInit, RendererStagedAuxiliaryWindowProxy,
|
||||
},
|
||||
types::{
|
||||
LoadedChildDocument, LoadedChildScriptSource, PopupClassicScriptLoadCompletion,
|
||||
PopupDocumentLoadCompletion, PopupDocumentLoadOutcome,
|
||||
@@ -58,6 +62,7 @@ use url::Url;
|
||||
|
||||
const LIGHTWEIGHT_POPUP_EVENT_LISTENERS_SLOT: &str = "__lmLightweightPopupEventListeners";
|
||||
const LIGHTWEIGHT_POPUP_ID_SLOT: &str = "__lmLightweightPopupId";
|
||||
const RENDERER_OWNED_AUXILIARY_POPUP_ID_SLOT: &str = "__lmRendererOwnedAuxiliaryPopupId";
|
||||
const ACTIVE_LIGHTWEIGHT_POPUP_ID_SLOT: &str = "__lmActiveLightweightPopupId";
|
||||
const LIGHTWEIGHT_POPUP_JAVASCRIPT_POPUP_ID_SLOT: &str = "__lmLightweightPopupJavascriptPopupId";
|
||||
const LIGHTWEIGHT_POPUP_JAVASCRIPT_DOCUMENT_ID_SLOT: &str =
|
||||
@@ -67,14 +72,6 @@ const LIGHTWEIGHT_POPUP_JAVASCRIPT_NAVIGATION_ID_SLOT: &str =
|
||||
const LIGHTWEIGHT_POPUP_JAVASCRIPT_SOURCE_SLOT: &str = "__lmLightweightPopupJavascriptSource";
|
||||
const LIGHTWEIGHT_POPUP_DOCUMENT_WRITE_SESSION_SLOT: &str =
|
||||
"__lmLightweightPopupDocumentWriteSession";
|
||||
const LIGHTWEIGHT_POPUP_VIEWPORT_SURFACE_PROPERTIES: &[&str] = &[
|
||||
"innerWidth",
|
||||
"innerHeight",
|
||||
"outerWidth",
|
||||
"outerHeight",
|
||||
"devicePixelRatio",
|
||||
];
|
||||
|
||||
/// Whether applying a popup response entered author code or an event-dispatch
|
||||
/// algorithm before returning to the enclosing Page resource task.
|
||||
///
|
||||
@@ -413,6 +410,9 @@ pub(crate) struct OpenedLightweightPopup<'scope> {
|
||||
pub(crate) window: v8::Local<'scope, v8::Object>,
|
||||
pub(crate) popup_id: u64,
|
||||
pub(crate) created_new_browsing_context: bool,
|
||||
pub(crate) pending_auxiliary_page: Option<RendererPendingAuxiliaryPage>,
|
||||
pub(crate) captured_session_storage_store: Option<SharedWebStorageStore>,
|
||||
pub(crate) captured_initial_empty_document_storage_key: Option<MoliStorageKey>,
|
||||
}
|
||||
|
||||
fn inherit_lightweight_popup_opener_sandbox(
|
||||
@@ -609,6 +609,7 @@ impl JsContextHost {
|
||||
opener_child_handle: Option<DomHandle>,
|
||||
target_name: &str,
|
||||
href: &str,
|
||||
auxiliary_page_exposes_opener: Option<bool>,
|
||||
creator_base_url: Url,
|
||||
creator_policy_container: DocumentPolicyContainer,
|
||||
) -> Option<OpenedLightweightPopup<'s>> {
|
||||
@@ -630,24 +631,184 @@ impl JsContextHost {
|
||||
window,
|
||||
popup_id,
|
||||
created_new_browsing_context: false,
|
||||
pending_auxiliary_page: None,
|
||||
captured_session_storage_store: None,
|
||||
captured_initial_empty_document_storage_key: None,
|
||||
});
|
||||
}
|
||||
let (window, popup_id) = self.create_lightweight_popup_window(
|
||||
let pending_auxiliary_page = auxiliary_page_exposes_opener
|
||||
.and_then(|exposes_opener| self.reserve_pending_auxiliary_page(exposes_opener));
|
||||
let stages_same_agent_window_proxy =
|
||||
pending_auxiliary_page.is_some() && auxiliary_page_exposes_opener == Some(true);
|
||||
let opener_sandbox_policy = opener_child_handle
|
||||
.and_then(|handle| self.child_browsing_context_popup_opener_sandbox_policy(handle));
|
||||
let inherits_creator_security_token =
|
||||
!opener_sandbox_policy.is_some_and(|policy| policy.forces_opaque_origin);
|
||||
if let (Some(pending_auxiliary_page), Some(opener)) = (pending_auxiliary_page, opener)
|
||||
&& auxiliary_page_exposes_opener == Some(true)
|
||||
&& trackable_lightweight_popup_window_name(target_name).is_none()
|
||||
&& let Ok(requested_url) = Url::parse(href)
|
||||
&& requested_url.scheme() != "javascript"
|
||||
{
|
||||
return self.create_renderer_owned_initial_empty_window(
|
||||
scope,
|
||||
host_ptr,
|
||||
opener,
|
||||
opener_child_handle,
|
||||
requested_url,
|
||||
pending_auxiliary_page,
|
||||
opener_sandbox_policy,
|
||||
creator_base_url.clone(),
|
||||
creator_policy_container.clone(),
|
||||
inherits_creator_security_token,
|
||||
);
|
||||
}
|
||||
let (window, popup_id, facade_context) = self.create_lightweight_popup_window(
|
||||
scope,
|
||||
host_ptr,
|
||||
opener,
|
||||
opener_child_handle,
|
||||
target_name,
|
||||
href,
|
||||
opener_child_handle
|
||||
.and_then(|handle| self.child_browsing_context_popup_opener_sandbox_policy(handle)),
|
||||
stages_same_agent_window_proxy,
|
||||
opener_sandbox_policy,
|
||||
creator_base_url,
|
||||
creator_policy_container,
|
||||
)?;
|
||||
if let (Some(pending_auxiliary_page), Some(facade_context)) =
|
||||
(pending_auxiliary_page, facade_context)
|
||||
{
|
||||
self.stage_pending_auxiliary_window_proxy(
|
||||
scope,
|
||||
pending_auxiliary_page,
|
||||
v8::Global::new(scope, window),
|
||||
facade_context,
|
||||
inherits_creator_security_token,
|
||||
)
|
||||
.expect("related auxiliary Page must stage its exact opener WindowProxy");
|
||||
}
|
||||
Some(OpenedLightweightPopup {
|
||||
window,
|
||||
popup_id,
|
||||
created_new_browsing_context: true,
|
||||
pending_auxiliary_page,
|
||||
captured_session_storage_store: None,
|
||||
captured_initial_empty_document_storage_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_renderer_owned_initial_empty_window<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
opener: v8::Local<'s, v8::Object>,
|
||||
opener_child_handle: Option<DomHandle>,
|
||||
requested_url: Url,
|
||||
pending_auxiliary_page: RendererPendingAuxiliaryPage,
|
||||
opener_sandbox_policy: Option<DocumentSandboxPolicy>,
|
||||
creator_base_url: Url,
|
||||
mut creator_policy_container: DocumentPolicyContainer,
|
||||
inherits_creator_security_token: bool,
|
||||
) -> Option<OpenedLightweightPopup<'s>> {
|
||||
let initial_url = if moli_url::is_about_blank(&requested_url) {
|
||||
requested_url
|
||||
} else {
|
||||
// A non-empty destination is not the initial Document. It remains
|
||||
// on the immutable popup activation until target admission, then
|
||||
// the auxiliary Page owner starts its one replacement navigation.
|
||||
about_blank_url()
|
||||
};
|
||||
let initial_base_url = lightweight_popup_initial_base_url(&initial_url, creator_base_url);
|
||||
inherit_lightweight_popup_opener_sandbox(
|
||||
&mut creator_policy_container.sandbox,
|
||||
opener_sandbox_policy,
|
||||
);
|
||||
let creator_resource_authority = self
|
||||
.document_resource_loader_for_dispatch_scope(self.entered_owner_dispatch_scope(scope))?
|
||||
.clone();
|
||||
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
|
||||
scope,
|
||||
Some(opener),
|
||||
opener_child_handle,
|
||||
&initial_url,
|
||||
opener_sandbox_policy.is_some_and(|policy| policy.forces_opaque_origin),
|
||||
);
|
||||
let session_storage_store = self.cloned_lightweight_popup_session_storage_store(
|
||||
scope,
|
||||
opener,
|
||||
opener_child_handle,
|
||||
&storage_scope,
|
||||
);
|
||||
let (window, facade_context) = self
|
||||
.bridge
|
||||
.bindings
|
||||
.instantiate_same_origin_window_proxy_shell(scope, host_ptr);
|
||||
let inherited_security_token = inherits_creator_security_token.then(|| {
|
||||
let facade_context = v8::Local::new(scope, &facade_context);
|
||||
v8::Global::new(scope, facade_context.get_security_token(scope))
|
||||
});
|
||||
let staged_window_proxy = RendererStagedAuxiliaryWindowProxy::new(
|
||||
v8::Global::new(scope, window),
|
||||
facade_context,
|
||||
inherited_security_token,
|
||||
);
|
||||
let mut dom_host = crate::dom::native::DomHost::from_dom(
|
||||
crate::parser::HtmlParser::with_scripting_enabled(
|
||||
creator_policy_container.sandbox.allows_scripts,
|
||||
)
|
||||
.parse(
|
||||
initial_url.clone(),
|
||||
"<!doctype html><html><head></head><body></body></html>".to_owned(),
|
||||
),
|
||||
);
|
||||
let document_handle = dom_host.document_handle();
|
||||
let _ = dom_host.set_document_fallback_base_url_for_handle(
|
||||
document_handle,
|
||||
Some(initial_base_url.clone()),
|
||||
);
|
||||
let env = PageVmEnvConfig::related_initial_empty(
|
||||
crate::RendererWebStorageHandles::new(
|
||||
self.web_storage_store(),
|
||||
session_storage_store.clone(),
|
||||
),
|
||||
storage_scope.storage_key().clone(),
|
||||
&creator_policy_container,
|
||||
self.indexed_db_manager(),
|
||||
self.storage_bucket_store(),
|
||||
);
|
||||
let popup_id = self.next_lightweight_popup_id;
|
||||
self.next_lightweight_popup_id = self.next_lightweight_popup_id.wrapping_add(1).max(1);
|
||||
let init = RendererRelatedInitialEmptyPageRealmInit {
|
||||
dom_host,
|
||||
loader: creator_resource_authority
|
||||
.request_client()
|
||||
.fork_with_isolated_page_network_policy(),
|
||||
env,
|
||||
inherited_origin: storage_scope.origin().to_owned(),
|
||||
policy_container: creator_policy_container,
|
||||
auxiliary_popup_id: popup_id,
|
||||
staged_window_proxy,
|
||||
opener: Some(v8::Global::new(scope, opener)),
|
||||
window_name: String::new(),
|
||||
};
|
||||
if let Err(error) =
|
||||
self.stage_related_initial_empty_page_in_scope(scope, pending_auxiliary_page, init)
|
||||
{
|
||||
tracing::debug!(
|
||||
error = %error,
|
||||
"failed to stage renderer-owned initial auxiliary realm; rejecting synchronous proxy path"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(OpenedLightweightPopup {
|
||||
window,
|
||||
popup_id,
|
||||
created_new_browsing_context: true,
|
||||
pending_auxiliary_page: Some(pending_auxiliary_page),
|
||||
captured_session_storage_store: Some(session_storage_store),
|
||||
captured_initial_empty_document_storage_key: Some(storage_scope.storage_key().clone()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -659,10 +820,15 @@ impl JsContextHost {
|
||||
opener_child_handle: Option<DomHandle>,
|
||||
target_name: &str,
|
||||
href: &str,
|
||||
detachable_window_proxy: bool,
|
||||
opener_sandbox_policy: Option<DocumentSandboxPolicy>,
|
||||
creator_base_url: Url,
|
||||
creator_policy_container: DocumentPolicyContainer,
|
||||
) -> Option<(v8::Local<'s, v8::Object>, u64)> {
|
||||
) -> Option<(
|
||||
v8::Local<'s, v8::Object>,
|
||||
u64,
|
||||
Option<v8::Global<v8::Context>>,
|
||||
)> {
|
||||
let parsed_url = Url::parse(href).ok()?;
|
||||
let initial_url = if parsed_url.scheme() == "javascript" {
|
||||
about_blank_url()
|
||||
@@ -693,10 +859,20 @@ impl JsContextHost {
|
||||
.next_lightweight_popup_id
|
||||
.checked_add(1)
|
||||
.expect("lightweight popup id space exhausted");
|
||||
let window = self
|
||||
.bridge
|
||||
.bindings
|
||||
.instantiate_window_shell(scope, host_ptr);
|
||||
let (window, facade_context) = if detachable_window_proxy {
|
||||
let (window, context) = self
|
||||
.bridge
|
||||
.bindings
|
||||
.instantiate_same_origin_window_proxy_shell(scope, host_ptr);
|
||||
(window, Some(context))
|
||||
} else {
|
||||
(
|
||||
self.bridge
|
||||
.bindings
|
||||
.instantiate_window_shell(scope, host_ptr),
|
||||
None,
|
||||
)
|
||||
};
|
||||
let popup_id_private_value = v8::BigInt::new_from_u64(scope, popup_id);
|
||||
set_private_value(
|
||||
scope,
|
||||
@@ -734,12 +910,48 @@ impl JsContextHost {
|
||||
set_object_slot(scope, window, "name", value.into());
|
||||
}
|
||||
if let Some(opener) = opener {
|
||||
set_private_value(scope, window, WINDOW_OPENER_SLOT, opener.into());
|
||||
set_object_slot(scope, window, "opener", opener.into());
|
||||
install_lightweight_popup_viewport_surface_from_opener(scope, opener, window);
|
||||
inherit_auxiliary_window_viewport_surface(scope, opener, window);
|
||||
} else {
|
||||
let opener = v8::null(scope);
|
||||
set_private_value(scope, window, WINDOW_OPENER_SLOT, opener.into());
|
||||
set_object_slot(scope, window, "opener", opener.into());
|
||||
}
|
||||
if let Some(facade_context) = facade_context.as_ref() {
|
||||
// The legacy lightweight loader continues to execute until the
|
||||
// initial Document owner is unified. Give its temporary realm an
|
||||
// observable token and install realm-local identity slots from
|
||||
// inside that realm; setting a V8 private on its global proxy
|
||||
// while entered in the opener realm can otherwise target the
|
||||
// wrong inner global.
|
||||
let facade_context = v8::Local::new(scope, facade_context);
|
||||
let context_token = self.allocate_runtime_observable_context_token();
|
||||
crate::native_bridge::install_runtime_observable_context_token_for_context(
|
||||
facade_context,
|
||||
context_token,
|
||||
);
|
||||
let opener_value: v8::Local<'s, v8::Value> = opener
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| v8::null(scope).into());
|
||||
let opener_value = v8::Global::new(scope, opener_value);
|
||||
let facade_scope = &mut v8::ContextScope::new(scope, facade_context);
|
||||
let facade_window = facade_context.global(facade_scope);
|
||||
let popup_id_value = v8::BigInt::new_from_u64(facade_scope, popup_id);
|
||||
set_private_value(
|
||||
facade_scope,
|
||||
facade_window,
|
||||
LIGHTWEIGHT_POPUP_ID_SLOT,
|
||||
popup_id_value.into(),
|
||||
);
|
||||
let opener_value = v8::Local::new(facade_scope, &opener_value);
|
||||
set_private_value(
|
||||
facade_scope,
|
||||
facade_window,
|
||||
WINDOW_OPENER_SLOT,
|
||||
opener_value,
|
||||
);
|
||||
}
|
||||
if let Ok(navigator) =
|
||||
crate::context_bootstrap::build_lightweight_popup_window_navigator_object(
|
||||
scope, popup_id,
|
||||
@@ -905,7 +1117,7 @@ impl JsContextHost {
|
||||
if queue_synthetic_load {
|
||||
self.queue_lightweight_popup_load_event(navigation_task);
|
||||
}
|
||||
Some((window, popup_id))
|
||||
Some((window, popup_id, facade_context))
|
||||
}
|
||||
|
||||
fn reopen_lightweight_popup_window<'s>(
|
||||
@@ -1030,6 +1242,28 @@ impl JsContextHost {
|
||||
.map(|record| v8::Local::new(scope, &record.window_proxy))
|
||||
}
|
||||
|
||||
pub(crate) fn lightweight_popup_id_for_window_proxy<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) -> Option<u64> {
|
||||
lightweight_popup_id_from_window(scope, window)
|
||||
.filter(|popup_id| {
|
||||
self.lightweight_popup_browsing_contexts
|
||||
.contains_key(popup_id)
|
||||
})
|
||||
.or_else(|| {
|
||||
self.lightweight_popup_browsing_contexts
|
||||
.iter()
|
||||
.find_map(|(popup_id, record)| {
|
||||
v8::Local::new(scope, &record.window_proxy)
|
||||
.strict_equals(window.into())
|
||||
.then_some(*popup_id)
|
||||
})
|
||||
})
|
||||
.or_else(|| renderer_owned_auxiliary_popup_id_from_window(scope, window))
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn lightweight_popup_opener_endpoint(
|
||||
&self,
|
||||
popup_id: u64,
|
||||
@@ -3912,6 +4146,28 @@ pub(crate) fn lightweight_popup_id_from_window<'s>(
|
||||
lightweight_popup_id_from_value(scope, value)
|
||||
}
|
||||
|
||||
pub(crate) fn set_renderer_owned_auxiliary_popup_id<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
popup_id: u64,
|
||||
) {
|
||||
let value = v8::BigInt::new_from_u64(scope, popup_id);
|
||||
set_private_value(
|
||||
scope,
|
||||
window,
|
||||
RENDERER_OWNED_AUXILIARY_POPUP_ID_SLOT,
|
||||
value.into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn renderer_owned_auxiliary_popup_id_from_window<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) -> Option<u64> {
|
||||
let value = get_private_value(scope, window, RENDERER_OWNED_AUXILIARY_POPUP_ID_SLOT)?;
|
||||
lightweight_popup_id_from_value(scope, value)
|
||||
}
|
||||
|
||||
pub(crate) fn active_lightweight_popup_id(scope: &mut v8::PinScope<'_, '_>) -> Option<u64> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let value = get_private_value(scope, global, ACTIVE_LIGHTWEIGHT_POPUP_ID_SLOT)?;
|
||||
@@ -4029,21 +4285,6 @@ fn install_lightweight_popup_indexed_db_factory<'s>(
|
||||
true
|
||||
}
|
||||
|
||||
fn install_lightweight_popup_viewport_surface_from_opener<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
opener: v8::Local<'s, v8::Object>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
for property in LIGHTWEIGHT_POPUP_VIEWPORT_SURFACE_PROPERTIES {
|
||||
let Some(value) = opener.get(scope, v8str(scope, property).into()) else {
|
||||
continue;
|
||||
};
|
||||
if value.is_number() {
|
||||
set_object_slot(scope, window, property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lightweight_popup_close_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
|
||||
@@ -965,10 +965,17 @@ fn document_referrer_getter_function<'s>(
|
||||
return;
|
||||
};
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let referrer = runtime
|
||||
.child_browsing_context_referrer_for_document_handle(handle)
|
||||
.or_else(|| runtime.lightweight_popup_referrer_for_document_handle(handle))
|
||||
.unwrap_or("");
|
||||
let referrer = if handle == runtime.document_handle() {
|
||||
runtime
|
||||
.document_policy_container()
|
||||
.document_referrer
|
||||
.as_str()
|
||||
} else {
|
||||
runtime
|
||||
.child_browsing_context_referrer_for_document_handle(handle)
|
||||
.or_else(|| runtime.lightweight_popup_referrer_for_document_handle(handle))
|
||||
.unwrap_or("")
|
||||
};
|
||||
set_document_string_return_value(scope, &mut rv, referrer);
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ fn navigate_hyperlink_popup_target(
|
||||
None,
|
||||
target_name,
|
||||
resolved_url,
|
||||
Some(!relations.suppress_opener),
|
||||
creator.base_url,
|
||||
creator.policy_container,
|
||||
) else {
|
||||
@@ -253,6 +254,7 @@ fn navigate_hyperlink_popup_target(
|
||||
let session_storage_store = runtime.lightweight_popup_session_storage_store(popup_id);
|
||||
let initial_empty_document_storage_key =
|
||||
runtime.lightweight_popup_initial_empty_document_storage_key(popup_id);
|
||||
let pending_auxiliary_page = opened_popup.pending_auxiliary_page;
|
||||
let user_gesture = runtime.protocol_user_gesture_activation();
|
||||
let window_open_event = opened_popup.created_new_browsing_context.then(|| {
|
||||
RendererPendingWindowOpenEvent::browser_window(resolved_url, target_name, user_gesture)
|
||||
@@ -267,7 +269,8 @@ fn navigate_hyperlink_popup_target(
|
||||
target_name.to_owned(),
|
||||
disposition,
|
||||
)
|
||||
.with_initial_auxiliary_state(session_storage_store, initial_empty_document_storage_key),
|
||||
.with_initial_auxiliary_state(session_storage_store, initial_empty_document_storage_key)
|
||||
.with_pending_auxiliary_page(pending_auxiliary_page),
|
||||
window_open_event,
|
||||
);
|
||||
true
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) use current_input::{
|
||||
pub(crate) mod document;
|
||||
pub(crate) mod element;
|
||||
mod helpers;
|
||||
pub(crate) use helpers::set_object_slot;
|
||||
mod history_queue;
|
||||
pub(super) mod identity;
|
||||
mod node;
|
||||
@@ -61,7 +62,8 @@ pub(crate) use context_host::{
|
||||
install_runtime_observable_context_token_for_context, is_cross_origin_top_window_proxy,
|
||||
lightweight_popup_id_from_window, restore_active_lightweight_popup_scope,
|
||||
restore_deferred_active_lightweight_popup_scope_if_present,
|
||||
throw_cross_origin_location_security_error, throw_cross_origin_type_error,
|
||||
set_renderer_owned_auxiliary_popup_id, throw_cross_origin_location_security_error,
|
||||
throw_cross_origin_type_error,
|
||||
};
|
||||
|
||||
pub(crate) const ACTIVE_CHILD_WINDOW_HANDLE_SLOT: &str = "__moliActiveChildWindowHandle";
|
||||
|
||||
@@ -246,6 +246,10 @@ impl DocumentResourceLoader {
|
||||
&self.request_client
|
||||
}
|
||||
|
||||
pub(crate) fn document_origin(&self) -> String {
|
||||
self.authority.lifecycle.lock().context.origin().to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn owner(&self) -> crate::native_bridge::WindowDocumentOwner {
|
||||
self.authority.lifecycle.lock().context.owner()
|
||||
|
||||
@@ -316,6 +316,16 @@ impl PageRuntimeTaskSource {
|
||||
Rc::as_ptr(&self.page_task_producer_routes) as usize
|
||||
}
|
||||
|
||||
/// Allocates one exact cross-Document commit identity from this stable
|
||||
/// Page source. Prepared browser-owned navigation does not originate in a
|
||||
/// ScriptVm, but it must share the same identity namespace as script
|
||||
/// navigation so two replacement publications can never alias.
|
||||
pub(crate) fn next_top_level_navigation_handoff(
|
||||
&self,
|
||||
) -> super::RendererTopLevelNavigationHandoff {
|
||||
self.wake.next_top_level_navigation_handoff()
|
||||
}
|
||||
|
||||
pub(crate) fn v8_foreground_task_sender(&self) -> Option<RendererPageV8ForegroundTaskSender> {
|
||||
self.page_task_producer_routes
|
||||
.borrow()
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::Instant,
|
||||
@@ -14,6 +15,7 @@ use crate::DocumentStartScript;
|
||||
#[cfg(test)]
|
||||
use crate::dom::native::NativeDom;
|
||||
use crate::{
|
||||
browsing_context_model::BrowsingContextId,
|
||||
dom::NodeId,
|
||||
network::{ResourceRequestClient, context::DocumentResourceLoader},
|
||||
};
|
||||
@@ -286,7 +288,8 @@ pub use self::owner::{
|
||||
};
|
||||
pub use self::owner_local::RendererPageTestingHandle;
|
||||
pub use self::owner_local::{
|
||||
RendererPageCommandPending, RendererPageHandle, RendererRuntimeInspectorSessionDetachGuard,
|
||||
RendererPageCommandPending, RendererPageHandle, RendererPageReplacementReservationPending,
|
||||
RendererRuntimeInspectorSessionDetachGuard,
|
||||
};
|
||||
pub(crate) use self::owner_local_store::RendererPageToken;
|
||||
pub use self::page::{JsRuntime, JsRuntimeOwner, PendingHtmlPage, PreparedRendererDocument};
|
||||
@@ -528,21 +531,27 @@ impl PageId {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque owner-local reservation for one renderer Page identity.
|
||||
///
|
||||
/// The reservation is allocated before a queued full-body build or prepared
|
||||
/// document can enter parser or author-script execution. This lets external
|
||||
/// observers bind work to the future Page without guessing from the currently
|
||||
/// installed Page.
|
||||
/// Selects the script environment admitted when a Page reservation is
|
||||
/// consumed by document bootstrap.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) enum RendererScriptAgentAdmission {
|
||||
Fresh,
|
||||
#[cfg(test)]
|
||||
RelatedPageForExperiment {
|
||||
source_page_id: PageId,
|
||||
RelatedAuxiliaryPage {
|
||||
opener_page_id: PageId,
|
||||
},
|
||||
ExistingPageReplacement {
|
||||
expected_vm_creation_id: u64,
|
||||
reservation_nonce: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Opaque owner-local reservation for one renderer Page admission.
|
||||
///
|
||||
/// Initial creation reserves a future Page identity. A live replacement
|
||||
/// reserves one exact committed `PageVm` generation of an existing identity.
|
||||
/// Both are allocated before a queued full-body build or prepared document can
|
||||
/// enter parser or author-script execution, so external observers never infer
|
||||
/// ownership from whichever Page happens to be installed later.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct RendererPageReservationToken {
|
||||
local_host_id: RendererOwnerLocalHostId,
|
||||
@@ -559,17 +568,32 @@ impl RendererPageReservationToken {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_related_page_for_experiment(
|
||||
pub(crate) fn new_related_auxiliary_page(
|
||||
local_host_id: RendererOwnerLocalHostId,
|
||||
page_id: PageId,
|
||||
source_page_id: PageId,
|
||||
opener_page_id: PageId,
|
||||
) -> Self {
|
||||
Self {
|
||||
local_host_id,
|
||||
page_id,
|
||||
script_agent_admission: RendererScriptAgentAdmission::RelatedPageForExperiment {
|
||||
source_page_id,
|
||||
script_agent_admission: RendererScriptAgentAdmission::RelatedAuxiliaryPage {
|
||||
opener_page_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_existing_page_replacement(
|
||||
local_host_id: RendererOwnerLocalHostId,
|
||||
page_id: PageId,
|
||||
expected_vm_creation_id: u64,
|
||||
reservation_nonce: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
local_host_id,
|
||||
page_id,
|
||||
script_agent_admission: RendererScriptAgentAdmission::ExistingPageReplacement {
|
||||
expected_vm_creation_id,
|
||||
reservation_nonce,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -587,6 +611,228 @@ impl RendererPageReservationToken {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renderer-owned identity reserved synchronously for a newly accepted
|
||||
/// auxiliary top-level browsing context.
|
||||
///
|
||||
/// The typed browsing-context identity remains stable across Document
|
||||
/// replacements, while the Page reservation is consumed exactly once by the
|
||||
/// protocol target's initial empty-Document build. Reserving here prevents
|
||||
/// protocol code from manufacturing a second Page identity for a popup the
|
||||
/// renderer has already accepted.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct RendererPendingAuxiliaryPage {
|
||||
browsing_context_id: BrowsingContextId,
|
||||
page_reservation: RendererPageReservationToken,
|
||||
}
|
||||
|
||||
impl RendererPendingAuxiliaryPage {
|
||||
pub fn browsing_context_id(self) -> u64 {
|
||||
self.browsing_context_id.value()
|
||||
}
|
||||
|
||||
pub fn page_reservation(self) -> RendererPageReservationToken {
|
||||
self.page_reservation
|
||||
}
|
||||
}
|
||||
|
||||
/// Owner-local V8 state staged between synchronous popup acceptance and the
|
||||
/// related auxiliary Page's initial realm bootstrap.
|
||||
pub(crate) struct RendererStagedAuxiliaryWindowProxy {
|
||||
window_proxy: v8::Global<v8::Object>,
|
||||
facade_context: v8::Global<v8::Context>,
|
||||
inherited_security_token: Option<v8::Global<v8::Value>>,
|
||||
}
|
||||
|
||||
/// Inputs captured from the creator Document for one real synchronous
|
||||
/// auxiliary initial-empty Page realm.
|
||||
pub(crate) struct RendererRelatedInitialEmptyPageRealmInit {
|
||||
pub(crate) dom_host: crate::dom::native::DomHost,
|
||||
pub(crate) loader: ResourceRequestClient,
|
||||
pub(crate) env: PageVmEnvConfig,
|
||||
pub(crate) inherited_origin: String,
|
||||
pub(crate) policy_container: crate::document_runtime::DocumentPolicyContainer,
|
||||
pub(crate) auxiliary_popup_id: u64,
|
||||
pub(crate) staged_window_proxy: RendererStagedAuxiliaryWindowProxy,
|
||||
pub(crate) opener: Option<v8::Global<v8::Object>>,
|
||||
pub(crate) window_name: String,
|
||||
}
|
||||
|
||||
impl RendererStagedAuxiliaryWindowProxy {
|
||||
pub(crate) fn new(
|
||||
window_proxy: v8::Global<v8::Object>,
|
||||
facade_context: v8::Global<v8::Context>,
|
||||
inherited_security_token: Option<v8::Global<v8::Value>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
window_proxy,
|
||||
facade_context,
|
||||
inherited_security_token,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(
|
||||
self,
|
||||
) -> (
|
||||
v8::Global<v8::Object>,
|
||||
v8::Global<v8::Context>,
|
||||
Option<v8::Global<v8::Value>>,
|
||||
) {
|
||||
(
|
||||
self.window_proxy,
|
||||
self.facade_context,
|
||||
self.inherited_security_token,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RendererStagedAuxiliaryWindowProxyRegistry {
|
||||
by_page_id: HashMap<PageId, RendererStagedAuxiliaryWindowProxy>,
|
||||
}
|
||||
|
||||
/// Page-local allocation and handoff capability for auxiliary browsing
|
||||
/// contexts created synchronously by one opener realm.
|
||||
///
|
||||
/// The registry intentionally stays owner-local: V8 handles never cross the
|
||||
/// renderer/protocol transport carried by [`RendererPendingAuxiliaryPage`]. A
|
||||
/// related Page admitted to the same script agent consumes its exact staged
|
||||
/// proxy once during its initial realm bootstrap.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RendererAuxiliaryPageReservationAllocator {
|
||||
owner: Option<owner_local_store::RendererOwnerLocalContext>,
|
||||
local_host_id: RendererOwnerLocalHostId,
|
||||
opener_page_id: PageId,
|
||||
next_page_id: Arc<AtomicU64>,
|
||||
staged_window_proxies: Rc<RefCell<RendererStagedAuxiliaryWindowProxyRegistry>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RendererAuxiliaryPageReservationAllocator {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RendererAuxiliaryPageReservationAllocator")
|
||||
.field("local_host_id", &self.local_host_id)
|
||||
.field("opener_page_id", &self.opener_page_id)
|
||||
.field(
|
||||
"staged_window_proxy_count",
|
||||
&self.staged_window_proxies.borrow().by_page_id.len(),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl RendererAuxiliaryPageReservationAllocator {
|
||||
pub(in crate::runtime) fn new_for_owner(
|
||||
owner: owner_local_store::RendererOwnerLocalContext,
|
||||
opener_page_id: PageId,
|
||||
) -> Self {
|
||||
let local_host_id = owner.local_host_id;
|
||||
let next_page_id = owner.owner_state.next_page_id.clone();
|
||||
Self {
|
||||
owner: Some(owner),
|
||||
local_host_id,
|
||||
opener_page_id,
|
||||
next_page_id,
|
||||
staged_window_proxies: Rc::new(RefCell::new(
|
||||
RendererStagedAuxiliaryWindowProxyRegistry::default(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_for_test(
|
||||
local_host_id: RendererOwnerLocalHostId,
|
||||
opener_page_id: PageId,
|
||||
next_page_id: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
owner: None,
|
||||
local_host_id,
|
||||
opener_page_id,
|
||||
next_page_id,
|
||||
staged_window_proxies: Rc::new(RefCell::new(
|
||||
RendererStagedAuxiliaryWindowProxyRegistry::default(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reserve(&self, exposes_opener: bool) -> RendererPendingAuxiliaryPage {
|
||||
let page_id = PageId::new(self.next_page_id.fetch_add(1, Ordering::Relaxed));
|
||||
let page_reservation = if exposes_opener {
|
||||
RendererPageReservationToken::new_related_auxiliary_page(
|
||||
self.local_host_id,
|
||||
page_id,
|
||||
self.opener_page_id,
|
||||
)
|
||||
} else {
|
||||
RendererPageReservationToken::new(self.local_host_id, page_id)
|
||||
};
|
||||
RendererPendingAuxiliaryPage {
|
||||
browsing_context_id: BrowsingContextId::auxiliary_top_level(page_id.as_u64()),
|
||||
page_reservation,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stage_related_window_proxy(
|
||||
&self,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
staged: RendererStagedAuxiliaryWindowProxy,
|
||||
) -> Result<()> {
|
||||
let reservation = pending.page_reservation();
|
||||
ensure!(
|
||||
reservation.local_host_id() == self.local_host_id,
|
||||
"auxiliary WindowProxy reservation belongs to a different renderer owner"
|
||||
);
|
||||
ensure!(
|
||||
matches!(
|
||||
reservation.script_agent_admission(),
|
||||
RendererScriptAgentAdmission::RelatedAuxiliaryPage { opener_page_id }
|
||||
if opener_page_id == self.opener_page_id
|
||||
),
|
||||
"only a related auxiliary Page may adopt its opener-created WindowProxy"
|
||||
);
|
||||
let mut registry = self.staged_window_proxies.borrow_mut();
|
||||
let std::collections::hash_map::Entry::Vacant(entry) =
|
||||
registry.by_page_id.entry(reservation.page_id())
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"auxiliary Page already has a staged opener WindowProxy"
|
||||
));
|
||||
};
|
||||
entry.insert(staged);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn take_related_window_proxy(
|
||||
&self,
|
||||
page_id: PageId,
|
||||
) -> Option<RendererStagedAuxiliaryWindowProxy> {
|
||||
self.staged_window_proxies
|
||||
.borrow_mut()
|
||||
.by_page_id
|
||||
.remove(&page_id)
|
||||
}
|
||||
|
||||
pub(crate) fn stage_related_initial_empty_page_in_scope(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
source_environment: &crate::script_vm::RendererPageScriptEnvironment,
|
||||
source_bridge_bindings: &crate::native_bridge::bindings::NativeBridgeBindings,
|
||||
init: RendererRelatedInitialEmptyPageRealmInit,
|
||||
) -> Result<()> {
|
||||
let owner = self.owner.as_ref().ok_or_else(|| {
|
||||
anyhow!("standalone auxiliary allocator cannot stage a production Page realm")
|
||||
})?;
|
||||
owner_local_store::stage_related_initial_empty_page_on_bound_owner_local_store(
|
||||
owner,
|
||||
scope,
|
||||
pending,
|
||||
source_environment,
|
||||
source_bridge_bindings,
|
||||
init,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed authority to consume one matching prepared document and enter its
|
||||
/// renderer bootstrap.
|
||||
#[derive(Debug)]
|
||||
@@ -604,6 +850,233 @@ impl RendererDocumentCommitPermit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of committing a prepared Document into an already-owned renderer
|
||||
/// Page.
|
||||
///
|
||||
/// Unlike initial Page creation, this value deliberately carries no
|
||||
/// [`RendererPageHandle`]. The existing handle remains the sole Page close and
|
||||
/// command authority; callers only adopt the replacement Document's state and
|
||||
/// DevTools agent identity.
|
||||
pub struct RendererPageReplacementCommit {
|
||||
pub(crate) local_host_id: RendererOwnerLocalHostId,
|
||||
pub(crate) page_id: PageId,
|
||||
pub(crate) renderer_devtools_agent_token: RendererDevToolsAgentToken,
|
||||
pub(crate) javascript_dialog_broker: RendererJavaScriptDialogBroker,
|
||||
pub(crate) devtools_target: crate::devtools::target::RendererDevToolsTargetHandle,
|
||||
pub(crate) page_state: Arc<RendererPageState>,
|
||||
pub(crate) creation_diagnostics: RendererPageCreationDiagnostics,
|
||||
pub(crate) creation_artifacts: RendererPageCreationArtifacts,
|
||||
pub(crate) pending_download: Option<RendererPendingDownloadActivation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum RendererDocumentContinuationState {
|
||||
Pending,
|
||||
Settled(RendererDocumentContinuationCompletion),
|
||||
}
|
||||
|
||||
/// State captured by the exact owner turn that settles a committed Document's
|
||||
/// requested continuation target.
|
||||
///
|
||||
/// The output predecessor and PageState belong to the same owner turn. This
|
||||
/// lets protocol adapters first project every lifecycle/Inspector record up to
|
||||
/// the target and then replace their cached Page view without issuing a later
|
||||
/// renderer command that could observe a different turn.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RendererDocumentContinuationCompletion {
|
||||
renderer_output_predecessor: Option<RendererOutputFence>,
|
||||
page_state: Option<Arc<RendererPageState>>,
|
||||
}
|
||||
|
||||
impl RendererDocumentContinuationCompletion {
|
||||
fn settled(
|
||||
renderer_output_predecessor: Option<RendererOutputFence>,
|
||||
page_state: Option<Arc<RendererPageState>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
renderer_output_predecessor,
|
||||
page_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn canceled() -> Self {
|
||||
Self::settled(None, None)
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (Option<RendererOutputFence>, Option<Arc<RendererPageState>>) {
|
||||
(self.renderer_output_predecessor, self.page_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact observer for the owner turn that finishes a DocumentCommit
|
||||
/// continuation at its requested lifecycle target.
|
||||
///
|
||||
/// The lifecycle journal may publish DOMContentLoaded while that owner turn is
|
||||
/// still committing PageState and settling Inspector output. This observer is
|
||||
/// resolved only after that concrete output publication has been sent, and
|
||||
/// carries its fence when one exists.
|
||||
#[derive(Clone)]
|
||||
pub struct RendererDocumentContinuationObserver {
|
||||
receiver: tokio::sync::watch::Receiver<RendererDocumentContinuationState>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RendererDocumentContinuationObserver {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
.debug_struct("RendererDocumentContinuationObserver")
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RendererDocumentContinuationObserver {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.receiver.same_channel(&other.receiver)
|
||||
}
|
||||
}
|
||||
|
||||
impl RendererDocumentContinuationObserver {
|
||||
pub async fn wait(mut self) -> RendererDocumentContinuationCompletion {
|
||||
loop {
|
||||
match self.receiver.borrow_and_update().clone() {
|
||||
RendererDocumentContinuationState::Pending => {}
|
||||
RendererDocumentContinuationState::Settled(completion) => return completion,
|
||||
}
|
||||
if self.receiver.changed().await.is_err() {
|
||||
return RendererDocumentContinuationCompletion::canceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::runtime) struct RendererDocumentContinuationPublisher {
|
||||
sender: Option<tokio::sync::watch::Sender<RendererDocumentContinuationState>>,
|
||||
}
|
||||
|
||||
impl RendererDocumentContinuationPublisher {
|
||||
fn channel() -> (Self, RendererDocumentContinuationObserver) {
|
||||
let (sender, receiver) =
|
||||
tokio::sync::watch::channel(RendererDocumentContinuationState::Pending);
|
||||
(
|
||||
Self {
|
||||
sender: Some(sender),
|
||||
},
|
||||
RendererDocumentContinuationObserver { receiver },
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn settle(
|
||||
mut self,
|
||||
renderer_output_predecessor: Option<RendererOutputFence>,
|
||||
page_state: Option<Arc<RendererPageState>>,
|
||||
) {
|
||||
if let Some(sender) = self.sender.take() {
|
||||
sender.send_replace(RendererDocumentContinuationState::Settled(
|
||||
RendererDocumentContinuationCompletion::settled(
|
||||
renderer_output_predecessor,
|
||||
page_state,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RendererDocumentContinuationPublisher {
|
||||
fn drop(&mut self) {
|
||||
if let Some(sender) = self.sender.take() {
|
||||
sender.send_replace(RendererDocumentContinuationState::Settled(
|
||||
RendererDocumentContinuationCompletion::canceled(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn renderer_document_continuation_channel() -> (
|
||||
RendererDocumentContinuationPublisher,
|
||||
RendererDocumentContinuationObserver,
|
||||
) {
|
||||
RendererDocumentContinuationPublisher::channel()
|
||||
}
|
||||
|
||||
/// Whether a failed prepared replacement left the stable Page's old Document
|
||||
/// usable or crossed the renderer's irreversible retirement boundary.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RendererPageReplacementCommitFailureDisposition {
|
||||
PagePreserved,
|
||||
PageRetired,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RendererPageReplacementCommitError {
|
||||
disposition: RendererPageReplacementCommitFailureDisposition,
|
||||
source: anyhow::Error,
|
||||
}
|
||||
|
||||
impl RendererPageReplacementCommitError {
|
||||
#[doc(hidden)]
|
||||
pub fn page_preserved(source: anyhow::Error) -> Self {
|
||||
Self {
|
||||
disposition: RendererPageReplacementCommitFailureDisposition::PagePreserved,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn page_retired(source: anyhow::Error) -> Self {
|
||||
Self {
|
||||
disposition: RendererPageReplacementCommitFailureDisposition::PageRetired,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disposition(&self) -> RendererPageReplacementCommitFailureDisposition {
|
||||
self.disposition
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RendererPageReplacementCommitError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.source, formatter)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RendererPageReplacementCommitError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
Some(self.source.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl RendererPageReplacementCommit {
|
||||
pub fn owner_local_host_id(&self) -> RendererOwnerLocalHostId {
|
||||
self.local_host_id
|
||||
}
|
||||
|
||||
pub fn page_id(&self) -> PageId {
|
||||
self.page_id
|
||||
}
|
||||
|
||||
pub fn renderer_devtools_agent_token(&self) -> RendererDevToolsAgentToken {
|
||||
self.renderer_devtools_agent_token
|
||||
}
|
||||
|
||||
pub fn into_parts(
|
||||
self,
|
||||
) -> (
|
||||
RendererDevToolsAgentToken,
|
||||
Arc<RendererPageState>,
|
||||
RendererPageCreationDiagnostics,
|
||||
RendererPageCreationArtifacts,
|
||||
Option<RendererPendingDownloadActivation>,
|
||||
) {
|
||||
(
|
||||
self.renderer_devtools_agent_token,
|
||||
self.page_state,
|
||||
self.creation_diagnostics,
|
||||
self.creation_artifacts,
|
||||
self.pending_download,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(in crate::runtime) enum PageVmNavigationTurnOutcome {
|
||||
Completed(Box<PageVm>),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,6 +107,33 @@ enum RendererPageCommandPendingDispatch {
|
||||
InspectorMain(Box<RendererRuntimeInspectorMainCommandRoute>),
|
||||
}
|
||||
|
||||
/// A non-owning request for reserving the next Document in one exact live
|
||||
/// renderer Page.
|
||||
///
|
||||
/// This carries only the renderer owner route and stable Page token. It may be
|
||||
/// moved into protocol background work without cloning the Page handle or its
|
||||
/// close authority.
|
||||
pub struct RendererPageReplacementReservationPending {
|
||||
render_runtime: RenderRuntimeHandle,
|
||||
token: RendererPageToken,
|
||||
_not_send: PhantomData<Rc<()>>,
|
||||
}
|
||||
|
||||
impl RendererPageReplacementReservationPending {
|
||||
pub async fn await_ready(self) -> Result<RendererPageReservationToken> {
|
||||
match self
|
||||
.render_runtime
|
||||
.dispatch(RendererOwnerCommand::ReserveLivePageReplacement { token: self.token })
|
||||
.await?
|
||||
{
|
||||
RendererOwnerReply::LivePageReplacementReserved(reservation) => Ok(reservation),
|
||||
_ => Err(anyhow!(
|
||||
"renderer owner returned non-reservation reply for live Page replacement"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RendererPageTestingHandle {
|
||||
render_runtime: RenderRuntimeHandle,
|
||||
@@ -172,6 +199,52 @@ impl RendererPageHandle {
|
||||
self.committed_document_post_response_continuation.take()
|
||||
}
|
||||
|
||||
/// Adopts the current Document's inspector identity without replacing the
|
||||
/// Page handle or its close/command authority.
|
||||
pub fn adopt_page_replacement(
|
||||
&mut self,
|
||||
replacement: &RendererPageReplacementCommit,
|
||||
) -> anyhow::Result<()> {
|
||||
anyhow::ensure!(
|
||||
replacement.owner_local_host_id() == self.owner_local_host_id()
|
||||
&& replacement.page_id() == self.renderer_page_id(),
|
||||
"renderer Page replacement belongs to a different stable Page"
|
||||
);
|
||||
self.devtools_target.detach_page(
|
||||
self.renderer_page_id(),
|
||||
"Inspector Page document was replaced",
|
||||
);
|
||||
self.javascript_dialog_broker.dismiss_pending();
|
||||
self.devtools_agent_token = replacement.renderer_devtools_agent_token();
|
||||
self.javascript_dialog_broker = replacement.javascript_dialog_broker.clone();
|
||||
self.devtools_target = replacement.devtools_target.clone();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reserves a replacement Document for this exact live Page generation.
|
||||
///
|
||||
/// The renderer owner serializes this snapshot with Page turns. Preparing
|
||||
/// the replacement fails closed if another cross-document commit changes
|
||||
/// the resident `PageVm` before the reservation is consumed. A newer
|
||||
/// unconsumed reservation for the same Page supersedes an older one.
|
||||
pub async fn reserve_replacement_document_for_navigation(
|
||||
&self,
|
||||
) -> Result<RendererPageReservationToken> {
|
||||
self.start_replacement_document_reservation()
|
||||
.await_ready()
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn start_replacement_document_reservation(
|
||||
&self,
|
||||
) -> RendererPageReplacementReservationPending {
|
||||
RendererPageReplacementReservationPending {
|
||||
render_runtime: self.render_runtime.clone(),
|
||||
token: self.token(),
|
||||
_not_send: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_pending_modal_javascript_dialogs(&self) -> Vec<RendererPendingJavaScriptDialog> {
|
||||
self.javascript_dialog_broker.take_pending()
|
||||
}
|
||||
|
||||
@@ -145,6 +145,26 @@ pub(super) fn reserve_renderer_document_isolate_on_bound_owner_local_store(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn stage_related_initial_empty_page_on_bound_owner_local_store(
|
||||
owner: &RendererOwnerLocalContext,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
pending: RendererPendingAuxiliaryPage,
|
||||
source_environment: &RendererPageScriptEnvironment,
|
||||
source_bridge_bindings: &crate::native_bridge::bindings::NativeBridgeBindings,
|
||||
init: RendererRelatedInitialEmptyPageRealmInit,
|
||||
) -> Result<()> {
|
||||
with_bound_render_runtime_owner_local_store_session(|session| {
|
||||
session.store.stage_related_initial_empty_page_for_owner(
|
||||
owner,
|
||||
scope,
|
||||
pending,
|
||||
source_environment,
|
||||
source_bridge_bindings,
|
||||
init,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn remove_reserved_renderer_document_isolate_on_bound_owner_local_store(
|
||||
token: RendererPageToken,
|
||||
reservation_id: u64,
|
||||
@@ -481,6 +501,66 @@ pub(in crate::runtime) async fn begin_post_parse_lifecycle_on_entry_via_local_ta
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::runtime) async fn begin_prepared_document_replacement_on_entry_via_local_task(
|
||||
local_executor: JsLocalExecutor,
|
||||
entry: LivePageEntry,
|
||||
bootstrap: RendererDocumentIsolateBootstrap,
|
||||
reservation: RendererDocumentIsolateReservation,
|
||||
) -> (
|
||||
LivePageEntry,
|
||||
Result<(
|
||||
PageVmRuntimeHooks,
|
||||
crate::page_task_queue::RendererTopLevelNavigationHandoff,
|
||||
)>,
|
||||
) {
|
||||
run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| {
|
||||
Box::pin(async move {
|
||||
entry.retire_document_lifecycle_turn();
|
||||
entry
|
||||
.page_vm_mut()
|
||||
.begin_prepared_document_replacement(bootstrap, reservation)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::runtime) async fn install_prepared_replacement_page_vm_on_entry_via_local_task(
|
||||
local_executor: JsLocalExecutor,
|
||||
entry: LivePageEntry,
|
||||
page_vm: PageVm,
|
||||
navigation_handoff: crate::page_task_queue::RendererTopLevelNavigationHandoff,
|
||||
metadata: PreparedReplacementDocumentMetadata,
|
||||
) -> (LivePageEntry, Result<PublishedReplacementDocument>) {
|
||||
run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| {
|
||||
Box::pin(async move {
|
||||
entry.install_prepared_replacement_page_vm(page_vm, navigation_handoff, metadata)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::runtime) async fn install_prepared_replacement_pending_phase_one_on_entry_via_local_task(
|
||||
local_executor: JsLocalExecutor,
|
||||
entry: LivePageEntry,
|
||||
pending: PageVmPendingPhaseOneNavigation,
|
||||
navigation_handoff: crate::page_task_queue::RendererTopLevelNavigationHandoff,
|
||||
metadata: PreparedReplacementDocumentMetadata,
|
||||
) -> (
|
||||
LivePageEntry,
|
||||
Result<(RendererPageToken, PublishedReplacementDocument)>,
|
||||
) {
|
||||
run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| {
|
||||
Box::pin(async move {
|
||||
entry.install_prepared_replacement_pending_phase_one(
|
||||
pending,
|
||||
navigation_handoff,
|
||||
metadata,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::runtime) async fn commit_page_state_on_entry_via_local_task(
|
||||
local_executor: JsLocalExecutor,
|
||||
entry: LivePageEntry,
|
||||
@@ -510,6 +590,53 @@ pub(in crate::runtime) async fn commit_page_state_on_entry_via_local_task_with_p
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::runtime) async fn finalize_prepared_page_replacement_on_entry_via_local_task(
|
||||
local_executor: JsLocalExecutor,
|
||||
token: RendererPageToken,
|
||||
entry: LivePageEntry,
|
||||
) -> (
|
||||
LivePageEntry,
|
||||
Result<(
|
||||
RendererPageReplacementCommit,
|
||||
Option<RendererOutputPublication>,
|
||||
)>,
|
||||
) {
|
||||
run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| {
|
||||
Box::pin(async move {
|
||||
let page_state = RendererOwnerLocalStore::commit_current_vm_page_state_on_entry(entry)?;
|
||||
let initial_runtime_realms = entry.page_vm_mut().vm_mut().runtime_realm_inventory();
|
||||
let renderer_devtools_agent_token = entry.page_vm().devtools_agent_token();
|
||||
let javascript_dialog_broker = entry.page_vm().javascript_dialog_broker();
|
||||
let devtools_target = entry.page_vm().devtools_target();
|
||||
let creation_artifacts = entry.page_vm_mut().take_page_creation_artifacts();
|
||||
let renderer_output = entry.page_vm_mut().settle_renderer_output_publication();
|
||||
let renderer_output_predecessor = entry
|
||||
.page_vm()
|
||||
.renderer_output_tail_cursor()
|
||||
.map(|cursor| entry.page_vm().declare_renderer_output_fence(cursor));
|
||||
Ok((
|
||||
RendererPageReplacementCommit {
|
||||
local_host_id: token.local_host_id,
|
||||
page_id: token.page_id,
|
||||
renderer_devtools_agent_token,
|
||||
javascript_dialog_broker,
|
||||
devtools_target,
|
||||
page_state,
|
||||
creation_diagnostics: RendererPageCreationDiagnostics {
|
||||
initial_runtime_realms,
|
||||
renderer_output_predecessor,
|
||||
document_continuation_observer: None,
|
||||
},
|
||||
creation_artifacts,
|
||||
pending_download: None,
|
||||
},
|
||||
renderer_output,
|
||||
))
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::runtime) async fn advance_network_idle_wait_turn_on_entry_via_local_task(
|
||||
local_executor: JsLocalExecutor,
|
||||
entry: LivePageEntry,
|
||||
|
||||
@@ -324,6 +324,7 @@ impl LivePageEntry {
|
||||
}
|
||||
let current = self
|
||||
.active_page_vm()
|
||||
.filter(|page_vm| page_vm.has_live_script_vm())
|
||||
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
|
||||
self.standalone_navigation_follow.claim(current, requested)
|
||||
}
|
||||
@@ -331,6 +332,7 @@ impl LivePageEntry {
|
||||
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
|
||||
let current = self
|
||||
.active_page_vm()
|
||||
.filter(|page_vm| page_vm.has_live_script_vm())
|
||||
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
|
||||
self.standalone_navigation_follow.settle(current, succeeded);
|
||||
}
|
||||
@@ -370,6 +372,13 @@ impl LivePageEntry {
|
||||
|
||||
pub(super) fn publish_replacement_document_commit(
|
||||
&mut self,
|
||||
) -> Result<PublishedReplacementDocument> {
|
||||
self.publish_replacement_document_commit_with_metadata(None)
|
||||
}
|
||||
|
||||
fn publish_replacement_document_commit_with_metadata(
|
||||
&mut self,
|
||||
metadata: Option<PreparedReplacementDocumentMetadata>,
|
||||
) -> Result<PublishedReplacementDocument> {
|
||||
let stable_before = self.slot.entry();
|
||||
ensure!(
|
||||
@@ -401,7 +410,16 @@ impl LivePageEntry {
|
||||
|
||||
self.page_vm_mut()
|
||||
.settle_replacement_document_commit(navigation_handoff)?;
|
||||
RendererOwnerLocalStore::commit_active_vm_page_state_on_entry(self)?;
|
||||
match metadata {
|
||||
Some(metadata) => {
|
||||
RendererOwnerLocalStore::commit_active_vm_page_state_on_entry_with_metadata(
|
||||
self, metadata,
|
||||
)?;
|
||||
}
|
||||
None => {
|
||||
RendererOwnerLocalStore::commit_active_vm_page_state_on_entry(self)?;
|
||||
}
|
||||
}
|
||||
let stable_after = self.slot.entry();
|
||||
ensure!(
|
||||
stable_after.vm_creation_id() == vm_creation_id,
|
||||
@@ -427,6 +445,72 @@ impl LivePageEntry {
|
||||
Ok(published)
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn validate_prepared_replacement_generation(
|
||||
&self,
|
||||
expected_vm_creation_id: u64,
|
||||
) -> Result<()> {
|
||||
let stable = self.slot.entry();
|
||||
ensure!(
|
||||
stable.is_active(),
|
||||
"prepared replacement cannot commit into a retired Page slot"
|
||||
);
|
||||
ensure!(
|
||||
!self.has_uncommitted_page_vm(),
|
||||
"prepared replacement cannot overlap another uncommitted Document"
|
||||
);
|
||||
let resident = self
|
||||
.active_page_vm()
|
||||
.ok_or_else(|| anyhow!("prepared replacement lost the resident PageVm"))?;
|
||||
ensure!(
|
||||
stable.vm_creation_id() == expected_vm_creation_id
|
||||
&& resident.creation_id == expected_vm_creation_id,
|
||||
"stale prepared replacement expected PageVm {expected_vm_creation_id}, current stable PageVm {} and resident PageVm {}",
|
||||
stable.vm_creation_id(),
|
||||
resident.creation_id
|
||||
);
|
||||
ensure!(
|
||||
resident.has_live_script_vm(),
|
||||
"prepared replacement cannot detach an already-retired Document realm"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn install_prepared_replacement_page_vm(
|
||||
&mut self,
|
||||
mut page_vm: PageVm,
|
||||
navigation_handoff: crate::page_task_queue::RendererTopLevelNavigationHandoff,
|
||||
metadata: PreparedReplacementDocumentMetadata,
|
||||
) -> Result<PublishedReplacementDocument> {
|
||||
ensure!(
|
||||
self.pending_phase_one_navigation.is_none(),
|
||||
"prepared replacement cannot install over a phase-one residence"
|
||||
);
|
||||
ensure!(
|
||||
self.vm
|
||||
.as_ref()
|
||||
.is_some_and(|page_vm| !page_vm.has_live_script_vm()),
|
||||
"prepared replacement PageVm must install after the old Document realm is detached"
|
||||
);
|
||||
page_vm.prepare_replacement_document_commit(navigation_handoff)?;
|
||||
self.retire_document_lifecycle_turn();
|
||||
self.vm = Some(page_vm);
|
||||
self.publish_replacement_document_commit_with_metadata(Some(metadata))
|
||||
}
|
||||
|
||||
pub(super) fn install_prepared_replacement_pending_phase_one(
|
||||
&mut self,
|
||||
mut pending: PageVmPendingPhaseOneNavigation,
|
||||
navigation_handoff: crate::page_task_queue::RendererTopLevelNavigationHandoff,
|
||||
metadata: PreparedReplacementDocumentMetadata,
|
||||
) -> Result<(RendererPageToken, PublishedReplacementDocument)> {
|
||||
pending
|
||||
.page_vm_mut()
|
||||
.prepare_replacement_document_commit(navigation_handoff)?;
|
||||
let wake_token = self.install_new_pending_phase_one_navigation(pending)?;
|
||||
let published = self.publish_replacement_document_commit_with_metadata(Some(metadata))?;
|
||||
Ok((wake_token, published))
|
||||
}
|
||||
|
||||
pub(in crate::runtime) fn active_page_vm(&self) -> Option<&PageVm> {
|
||||
self.pending_phase_one_navigation
|
||||
.as_ref()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user