mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 08:01:37 +00:00
fix(cdp): harden document and navigation lifecycles
This commit is contained in:
@@ -5,6 +5,9 @@ import os
|
||||
import sys
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from playwright.async_api import Error as PlaywrightError
|
||||
from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
||||
|
||||
from ..assertions import SmokeError, assert_equal, record, wait_until
|
||||
from ..helpers import attach_cdp_event_collector
|
||||
|
||||
@@ -1453,8 +1456,52 @@ async def _enable_response_stage_fetch(cdp: Any) -> None:
|
||||
|
||||
|
||||
async def _open_popup(page: Any, url: str) -> Any:
|
||||
async with page.expect_popup(timeout=5_000) as popup_info:
|
||||
await page.evaluate("(url) => window.open(url, '_blank')", url)
|
||||
opened: bool | None = None
|
||||
try:
|
||||
async with page.expect_popup(timeout=5_000) as popup_info:
|
||||
opened = await page.evaluate(
|
||||
"(url) => Boolean(window.open(url, '_blank'))", url
|
||||
)
|
||||
except PlaywrightTimeoutError as error:
|
||||
observed = []
|
||||
for candidate in page.context.pages:
|
||||
try:
|
||||
opener = await candidate.opener()
|
||||
observed.append(
|
||||
{
|
||||
"url": candidate.url,
|
||||
"opener": opener.url if opener is not None else None,
|
||||
"isExpectedOpener": opener is page,
|
||||
}
|
||||
)
|
||||
except PlaywrightError as diagnostic_error:
|
||||
observed.append(
|
||||
{
|
||||
"url": candidate.url,
|
||||
"diagnosticError": repr(diagnostic_error),
|
||||
}
|
||||
)
|
||||
target_infos: Any = None
|
||||
browser_session: Any | None = None
|
||||
try:
|
||||
browser = page.context.browser
|
||||
if browser is not None:
|
||||
browser_session = await browser.new_browser_cdp_session()
|
||||
target_infos = await browser_session.send("Target.getTargets")
|
||||
except PlaywrightError as diagnostic_error:
|
||||
target_infos = {"diagnosticError": repr(diagnostic_error)}
|
||||
finally:
|
||||
if browser_session is not None:
|
||||
try:
|
||||
await browser_session.detach()
|
||||
except PlaywrightError:
|
||||
pass
|
||||
raise SmokeError(
|
||||
f"popup event failed for {url}: {error}; "
|
||||
f"windowOpenReturned={opened!r}; pages={observed!r}; "
|
||||
f"targets={target_infos!r}"
|
||||
) from error
|
||||
assert_equal(opened, True, "window.open returned a WindowProxy")
|
||||
popup = await popup_info.value
|
||||
await wait_until(lambda: popup.url == url, "popup URL")
|
||||
await popup.wait_for_load_state("load", timeout=10_000)
|
||||
|
||||
@@ -6,6 +6,9 @@ from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from playwright.async_api import Error as PlaywrightError
|
||||
from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
||||
|
||||
from ..assertions import SmokeError, assert_equal, record, wait_until
|
||||
from ..helpers import attach_cdp_event_collector, run_worker_command
|
||||
from .multi_page_contracts import run_multi_page_contracts
|
||||
@@ -1026,11 +1029,37 @@ async def _context_teardown_with_pending_commands(
|
||||
|
||||
|
||||
async def _open_popup(page: Any, url: str, name: str) -> Any:
|
||||
async with page.expect_popup(timeout=5_000) as popup_info:
|
||||
opened = await page.evaluate(
|
||||
"({url, name}) => Boolean(window.open(url, name))",
|
||||
{"url": url, "name": name},
|
||||
)
|
||||
opened: bool | None = None
|
||||
try:
|
||||
async with page.expect_popup(timeout=5_000) as popup_info:
|
||||
opened = await page.evaluate(
|
||||
"({url, name}) => Boolean(window.open(url, name))",
|
||||
{"url": url, "name": name},
|
||||
)
|
||||
except PlaywrightTimeoutError as error:
|
||||
pages = page.context.pages
|
||||
observed = []
|
||||
for candidate in pages:
|
||||
try:
|
||||
opener = await candidate.opener()
|
||||
observed.append(
|
||||
{
|
||||
"url": candidate.url,
|
||||
"opener": opener.url if opener is not None else None,
|
||||
"isExpectedOpener": opener is page,
|
||||
}
|
||||
)
|
||||
except PlaywrightError as diagnostic_error:
|
||||
observed.append(
|
||||
{
|
||||
"url": candidate.url,
|
||||
"diagnosticError": repr(diagnostic_error),
|
||||
}
|
||||
)
|
||||
raise SmokeError(
|
||||
f"popup event for {name} failed: {error}; "
|
||||
f"windowOpenReturned={opened!r}; pages={observed!r}"
|
||||
) from error
|
||||
assert_equal(opened, True, f"window.open returned a WindowProxy for {name}")
|
||||
popup = await popup_info.value
|
||||
await popup.wait_for_load_state("load", timeout=10_000)
|
||||
|
||||
@@ -684,6 +684,8 @@ async def async_main(argv: list[str] | None = None) -> int:
|
||||
}
|
||||
if failures:
|
||||
payload["error"] = "\n".join(failures)
|
||||
if serve is not None and serve.logs:
|
||||
payload["moliLogTail"] = serve.logs[-5_000:]
|
||||
_emit_worker_payload(payload, args.result, failed=not ok)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ async def start_moli_serve(
|
||||
str(port),
|
||||
"--resource",
|
||||
"--log-level",
|
||||
"info",
|
||||
os.environ.get("MOLI_SMOKE_LOG_LEVEL", "info"),
|
||||
]
|
||||
if layout:
|
||||
command.append("--layout")
|
||||
|
||||
@@ -12,6 +12,12 @@ pub struct PendingPageCommand {
|
||||
renderer_agent_attachment_id: Option<RendererAgentAttachmentId>,
|
||||
}
|
||||
|
||||
impl PendingPageCommand {
|
||||
pub fn renderer_agent_attachment_id(&self) -> Option<RendererAgentAttachmentId> {
|
||||
self.renderer_agent_attachment_id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PendingRuntimeInspectorCommandDispatch {
|
||||
kind: PendingRuntimeInspectorCommandDispatchKind,
|
||||
}
|
||||
|
||||
@@ -188,6 +188,11 @@ pub use crate::renderer::{
|
||||
|
||||
pub struct Page {
|
||||
page_state: PageStateCache,
|
||||
// Chromium owns this on RenderFrameHostImpl::IdleManager. Keep it on the
|
||||
// current document handle instead of deriving browser-side navigation
|
||||
// policy from whichever renderer snapshot the protocol actor consumed
|
||||
// most recently.
|
||||
idle_override: Option<EmulatedIdleOverride>,
|
||||
handle: RendererPageHandle,
|
||||
renderer_agent_attachment_id: Option<RendererAgentAttachmentId>,
|
||||
renderer_devtools_command_session_id: Option<String>,
|
||||
@@ -211,8 +216,10 @@ impl Page {
|
||||
handle: RendererPageHandle,
|
||||
page_state: Arc<RendererPageState>,
|
||||
) -> Self {
|
||||
let idle_override = page_state.idle_override();
|
||||
Self {
|
||||
page_state: PageStateCache::new(page_state),
|
||||
idle_override,
|
||||
handle,
|
||||
renderer_agent_attachment_id: None,
|
||||
renderer_devtools_command_session_id: None,
|
||||
@@ -225,8 +232,10 @@ impl Page {
|
||||
page_state: Arc<RendererPageState>,
|
||||
page_creation_artifacts: RendererPageCreationArtifacts,
|
||||
) -> Self {
|
||||
let idle_override = page_state.idle_override();
|
||||
Self {
|
||||
page_state: PageStateCache::new(page_state),
|
||||
idle_override,
|
||||
handle,
|
||||
renderer_agent_attachment_id: None,
|
||||
renderer_devtools_command_session_id: None,
|
||||
|
||||
@@ -175,7 +175,7 @@ impl Page {
|
||||
}
|
||||
|
||||
pub fn idle_override(&self) -> Option<super::EmulatedIdleOverride> {
|
||||
self.page_state.state().idle_override()
|
||||
self.idle_override
|
||||
}
|
||||
|
||||
pub fn status(&self) -> u16 {
|
||||
|
||||
@@ -68,7 +68,10 @@ impl Page {
|
||||
resource_runtime: &BrowserResourceRuntime,
|
||||
) -> Result<()> {
|
||||
self.dispatch_unit_page_command_async(
|
||||
RendererPageCommand::ReplaceBrowserResourceRuntime(resource_runtime.clone()),
|
||||
RendererPageCommand::ReplaceBrowserResourceRuntime {
|
||||
resource_runtime: resource_runtime.clone(),
|
||||
navigator_identity: resource_runtime.browser_identity().clone(),
|
||||
},
|
||||
"replace browser resource runtime",
|
||||
)
|
||||
.await
|
||||
@@ -78,9 +81,21 @@ impl Page {
|
||||
&self,
|
||||
resource_runtime: &BrowserResourceRuntime,
|
||||
) -> Result<PendingPageCommand> {
|
||||
self.start_page_command(RendererPageCommand::ReplaceBrowserResourceRuntime(
|
||||
resource_runtime.clone(),
|
||||
))
|
||||
self.start_replace_browser_resource_runtime_with_navigator_identity(
|
||||
resource_runtime,
|
||||
resource_runtime.browser_identity().clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn start_replace_browser_resource_runtime_with_navigator_identity(
|
||||
&self,
|
||||
resource_runtime: &BrowserResourceRuntime,
|
||||
navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
) -> Result<PendingPageCommand> {
|
||||
self.start_page_command(RendererPageCommand::ReplaceBrowserResourceRuntime {
|
||||
resource_runtime: resource_runtime.clone(),
|
||||
navigator_identity,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finish_replace_browser_resource_runtime(
|
||||
@@ -204,10 +219,17 @@ impl Page {
|
||||
}
|
||||
|
||||
pub fn start_set_idle_override(
|
||||
&self,
|
||||
&mut self,
|
||||
idle_override: Option<EmulatedIdleOverride>,
|
||||
) -> Result<PendingPageCommand> {
|
||||
self.start_page_command(RendererPageCommand::SetIdleOverride(idle_override))
|
||||
let pending =
|
||||
self.start_page_command(RendererPageCommand::SetIdleOverride(idle_override))?;
|
||||
// SetIdleOverride is synchronous browser-side state in Chromium. Make
|
||||
// it visible at command admission so a navigation from another CDP
|
||||
// session cannot observe an older protocol snapshot after the renderer
|
||||
// has already accepted the command.
|
||||
self.idle_override = idle_override;
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
pub fn finish_set_idle_override(&mut self, completion: CompletedPageCommand) -> Result<()> {
|
||||
|
||||
@@ -210,7 +210,11 @@ pub struct PreparedDocumentPageCommitConfiguration {
|
||||
pub emulated_media: EmulatedMediaOverrides,
|
||||
pub idle_override: Option<crate::page::EmulatedIdleOverride>,
|
||||
pub viewport_surface: Option<ViewportSurface>,
|
||||
pub browser_resource_runtime: BrowserResourceRuntime,
|
||||
pub navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
pub network_offline: bool,
|
||||
pub bypass_service_worker: bool,
|
||||
pub cache_disabled: bool,
|
||||
pub blocked_url_patterns: Vec<String>,
|
||||
pub fetch_subresource_interception: (bool, Option<SubresourceResourceType>),
|
||||
}
|
||||
@@ -268,7 +272,11 @@ impl PreparedDocumentPage {
|
||||
emulated_media: configuration.emulated_media,
|
||||
idle_override: configuration.idle_override,
|
||||
viewport_surface: configuration.viewport_surface,
|
||||
browser_resource_runtime: configuration.browser_resource_runtime,
|
||||
navigator_identity: configuration.navigator_identity,
|
||||
network_offline: configuration.network_offline,
|
||||
bypass_service_worker: configuration.bypass_service_worker,
|
||||
cache_disabled: configuration.cache_disabled,
|
||||
blocked_url_patterns: configuration.blocked_url_patterns,
|
||||
fetch_subresource_interception_enabled: configuration
|
||||
.fetch_subresource_interception
|
||||
|
||||
@@ -44,9 +44,7 @@ pub(crate) use actor::spawn_cdp_scheduler_actor;
|
||||
pub(crate) use adapter_scheduler::{
|
||||
ProtocolAdapterScheduler, ProtocolAdapterSchedulerAdvance, ProtocolAdapterSchedulerInput,
|
||||
};
|
||||
pub(crate) use command_dispatch::{
|
||||
CommandDispatchState, CommandDispatchStepOutput, CommandTurnOutput,
|
||||
};
|
||||
pub(crate) use command_dispatch::{CommandDispatchState, CommandTurnOutput};
|
||||
pub(crate) use frontend_control::{CdpCookieSnapshot, CdpOwnerActorLifecycle};
|
||||
use protocol_residence::{
|
||||
ClientTurnPredecessor, ProtocolSchedulerResidence, ProtocolSchedulerStep, SchedulerQueues,
|
||||
@@ -2422,12 +2420,20 @@ impl CdpScheduler {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn ingest_renderer_publication_after_load(
|
||||
pub(crate) async fn ingest_renderer_publication_after_loads(
|
||||
&mut self,
|
||||
publication: RendererOutputTransportMessage,
|
||||
observation_id: DeferredMainDocumentLoadObservationId,
|
||||
observation_ids: Vec<DeferredMainDocumentLoadObservationId>,
|
||||
) -> ProtocolOutputSequence {
|
||||
self.ingest_renderer_publication(publication, vec![observation_id], None)
|
||||
let future_load_predecessor = observation_ids
|
||||
.is_empty()
|
||||
.then(|| {
|
||||
DeferredMainDocumentLoadPredecessorCandidate::from_renderer_publication(
|
||||
&publication,
|
||||
)
|
||||
})
|
||||
.flatten();
|
||||
self.ingest_renderer_publication(publication, observation_ids, future_load_predecessor)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,9 @@ use super::frontend_control::CdpFrontendControlState;
|
||||
use super::{
|
||||
CdpBackgroundEventReceiver, CdpBackgroundNavigationCompletionReceiver, CdpCookieSnapshot,
|
||||
CdpOwnerActorLifecycle, CdpRendererPublicationReceiver, CdpScheduler,
|
||||
CdpSchedulerEventReceivers, CommandDispatchState, CommandDispatchStepOutput,
|
||||
CommandOutputReleasePermit, CommandStartAction, CommandTaskStep, CommandTurnOutput,
|
||||
ProtocolAdapterScheduler, ProtocolAdapterSchedulerAdvance, ProtocolAdapterSchedulerInput,
|
||||
ProtocolOutputSequence,
|
||||
CdpSchedulerEventReceivers, CommandDispatchState, CommandOutputReleasePermit,
|
||||
CommandStartAction, CommandTaskStep, CommandTurnOutput, ProtocolAdapterScheduler,
|
||||
ProtocolAdapterSchedulerAdvance, ProtocolAdapterSchedulerInput, ProtocolOutputSequence,
|
||||
};
|
||||
|
||||
struct PendingRuntimeDeferredReplyState {
|
||||
@@ -157,7 +156,7 @@ impl SchedulerInputReceivers {
|
||||
&mut self,
|
||||
deferred_runtime_response_rx: &mut mpsc::UnboundedReceiver<RuntimeInspectorResponseReady>,
|
||||
has_pending_runtime_deferred_reply: bool,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
page_javascript_blocked: bool,
|
||||
) -> Option<SchedulerInput> {
|
||||
if let Some(input) = self
|
||||
@@ -242,7 +241,7 @@ async fn run_cdp_scheduler_actor(
|
||||
scheduler
|
||||
.conn
|
||||
.set_runtime_inspector_response_ready_sender(deferred_runtime_response_tx.clone());
|
||||
let mut adapter_scheduler = ProtocolAdapterScheduler::<CommandDispatchState>::default();
|
||||
let mut adapter_scheduler = ProtocolAdapterScheduler::default();
|
||||
let mut pending_runtime_deferred_replies: VecDeque<PendingRuntimeDeferredReplyState> =
|
||||
VecDeque::new();
|
||||
let (pending_command_completion_tx, mut pending_command_completion_rx) =
|
||||
@@ -314,11 +313,7 @@ async fn run_cdp_scheduler_actor(
|
||||
generation,
|
||||
visual_state,
|
||||
} = frame;
|
||||
let output = route_top_level_background_event(
|
||||
&mut adapter_scheduler,
|
||||
&mut scheduler,
|
||||
event,
|
||||
);
|
||||
let output = scheduler.route_background_event_around_inflight_navigation(event);
|
||||
if !flush_protocol_output_with_runtime_deferred_reply_routing(
|
||||
&frontend_router,
|
||||
&mut scheduler,
|
||||
@@ -430,7 +425,7 @@ async fn handle_scheduler_input(
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
deferred_runtime_response_tx: &mpsc::UnboundedSender<RuntimeInspectorResponseReady>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_command_completion_tx: &mpsc::UnboundedSender<PendingCommandCompletion>,
|
||||
in_flight_commands: &mut InFlightCommands,
|
||||
blocked_commands: &mut VecDeque<BlockedCommandDispatch>,
|
||||
@@ -475,7 +470,7 @@ async fn handle_scheduler_input(
|
||||
.await
|
||||
}
|
||||
SchedulerInput::BackgroundEvent(event) => {
|
||||
let output = route_top_level_background_event(adapter_scheduler, scheduler, event);
|
||||
let output = scheduler.route_background_event_around_inflight_navigation(event);
|
||||
flush_protocol_output_with_runtime_deferred_reply_routing(
|
||||
frontend_router,
|
||||
scheduler,
|
||||
@@ -545,7 +540,7 @@ async fn flush_renderer_publication_predecessor(
|
||||
scheduler: &mut CdpScheduler,
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
predecessor: Option<&RendererOutputFence>,
|
||||
) -> bool {
|
||||
let Some(predecessor) = predecessor else {
|
||||
@@ -577,7 +572,7 @@ async fn ingest_and_flush_renderer_publication(
|
||||
frontend_router: &CdpFrontendRouter,
|
||||
scheduler: &mut CdpScheduler,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
publication: RendererOutputTransportMessage,
|
||||
) -> bool {
|
||||
let output = adapter_scheduler
|
||||
@@ -597,7 +592,7 @@ async fn flush_background_completion_input(
|
||||
scheduler: &mut CdpScheduler,
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
input: SchedulerInput,
|
||||
) -> bool {
|
||||
let (prefix_output, mut completion_output, renderer_output_predecessor) = match input {
|
||||
@@ -1143,14 +1138,12 @@ fn runtime_deferred_reply_unexpected_pending_output(
|
||||
async fn handle_adapter_scheduler_input(
|
||||
frontend_router: &CdpFrontendRouter,
|
||||
scheduler: &mut CdpScheduler,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
input: ProtocolAdapterSchedulerInput,
|
||||
) -> bool {
|
||||
let trace_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
|
||||
let advance = adapter_scheduler
|
||||
.advance_input(scheduler, input, CommandDispatchState::pending_command)
|
||||
.await;
|
||||
let advance = adapter_scheduler.advance_input(scheduler, input).await;
|
||||
let (kind, observation_id, ok) = match advance {
|
||||
ProtocolAdapterSchedulerAdvance::Idle => ("idle", None, true),
|
||||
ProtocolAdapterSchedulerAdvance::ClientTurnYielded => ("client_turn_yielded", None, true),
|
||||
@@ -1169,10 +1162,8 @@ async fn handle_adapter_scheduler_input(
|
||||
}
|
||||
ProtocolAdapterSchedulerAdvance::DeferredLoadCompleted {
|
||||
observation_id,
|
||||
attachment,
|
||||
output,
|
||||
} => {
|
||||
let output = attachment.complete_protocol_output(output);
|
||||
let ok = flush_protocol_output_with_runtime_deferred_reply_routing(
|
||||
frontend_router,
|
||||
scheduler,
|
||||
@@ -1203,27 +1194,6 @@ async fn handle_adapter_scheduler_input(
|
||||
ok
|
||||
}
|
||||
|
||||
fn route_top_level_background_event(
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
scheduler: &mut CdpScheduler,
|
||||
event: BackgroundProtocolEvent,
|
||||
) -> ProtocolOutputSequence {
|
||||
let output = scheduler.route_background_event_around_inflight_navigation(event);
|
||||
if output.is_empty() {
|
||||
return output;
|
||||
}
|
||||
let mut events = output.into_background_events();
|
||||
let Some(event) = events.pop() else {
|
||||
return ProtocolOutputSequence::empty();
|
||||
};
|
||||
let Some(dispatch) = adapter_scheduler.pending_load_attachment_mut() else {
|
||||
return ProtocolOutputSequence::from_background_event(event);
|
||||
};
|
||||
match dispatch.route_pending_background_event(event) {
|
||||
CommandDispatchStepOutput::Emit(output) => output,
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_pending_runtime_deferred_reply_state(
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
mut pending: PendingRuntimeDeferredReplyState,
|
||||
@@ -1392,7 +1362,7 @@ async fn handle_frontend_command(
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
deferred_runtime_response_tx: &mpsc::UnboundedSender<RuntimeInspectorResponseReady>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_command_completion_tx: &mpsc::UnboundedSender<PendingCommandCompletion>,
|
||||
in_flight_commands: &mut InFlightCommands,
|
||||
blocked_commands: &mut VecDeque<BlockedCommandDispatch>,
|
||||
@@ -1436,7 +1406,7 @@ async fn handle_client_command_with_interleaved_output(
|
||||
command: ParsedCdpCommand,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
deferred_runtime_response_tx: &mpsc::UnboundedSender<RuntimeInspectorResponseReady>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_command_completion_tx: &mpsc::UnboundedSender<PendingCommandCompletion>,
|
||||
in_flight_commands: &mut InFlightCommands,
|
||||
blocked_commands: &mut VecDeque<BlockedCommandDispatch>,
|
||||
@@ -1519,7 +1489,7 @@ async fn start_ready_command_dispatch(
|
||||
mut command_context: CommandDispatchContext,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
deferred_runtime_response_tx: &mpsc::UnboundedSender<RuntimeInspectorResponseReady>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_command_completion_tx: &mpsc::UnboundedSender<PendingCommandCompletion>,
|
||||
in_flight_commands: &mut InFlightCommands,
|
||||
next_in_flight_command_token: &mut u64,
|
||||
@@ -1615,7 +1585,7 @@ async fn drain_blocked_commands_after_navigation_gate(
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
deferred_runtime_response_tx: &mpsc::UnboundedSender<RuntimeInspectorResponseReady>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_command_completion_tx: &mpsc::UnboundedSender<PendingCommandCompletion>,
|
||||
in_flight_commands: &mut InFlightCommands,
|
||||
blocked_commands: &mut VecDeque<BlockedCommandDispatch>,
|
||||
@@ -1683,7 +1653,7 @@ async fn flush_completed_command_output(
|
||||
scheduler: &mut CdpScheduler,
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
metadata: &InFlightCommandMetadata,
|
||||
mut output: CommandTurnOutput,
|
||||
) -> bool {
|
||||
@@ -1850,7 +1820,7 @@ async fn handle_pending_command_completion(
|
||||
scheduler_input_rx: &mut SchedulerInputReceivers,
|
||||
pending_runtime_deferred_replies: &mut VecDeque<PendingRuntimeDeferredReplyState>,
|
||||
deferred_runtime_response_tx: &mpsc::UnboundedSender<RuntimeInspectorResponseReady>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<CommandDispatchState>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
pending_command_completion_tx: &mpsc::UnboundedSender<PendingCommandCompletion>,
|
||||
in_flight_commands: &mut InFlightCommands,
|
||||
completion: PendingCommandCompletion,
|
||||
|
||||
@@ -15,26 +15,24 @@ use super::{CdpScheduler, ProtocolOutputSequence, protocol_residence::ProtocolSc
|
||||
/// work in a later client turn:
|
||||
///
|
||||
/// - one coalesced self-turn signal;
|
||||
/// - at most one exact main-document load observation;
|
||||
/// - an adapter-specific attachment that remains inseparable from that exact
|
||||
/// observation until its terminal is consumed.
|
||||
/// - every exact main-document load observation currently awaiting its
|
||||
/// target-local terminal.
|
||||
///
|
||||
/// It never stores a renderer publication, Page task capability or protocol
|
||||
/// transport route. Switching a Classic connection to BiDi therefore keeps
|
||||
/// this value alive instead of recreating scheduler or load-observer state.
|
||||
pub(crate) struct ProtocolAdapterScheduler<A> {
|
||||
/// transport route. Independent Pages may therefore wait for load in parallel,
|
||||
/// and switching a Classic connection to BiDi keeps the observations alive.
|
||||
pub(crate) struct ProtocolAdapterScheduler {
|
||||
turn_tx: mpsc::UnboundedSender<()>,
|
||||
turn_rx: mpsc::UnboundedReceiver<()>,
|
||||
turn_scheduled: bool,
|
||||
load_completion_tx: mpsc::UnboundedSender<CompletedDeferredMainDocumentLoadCompletion>,
|
||||
load_completion_rx: mpsc::UnboundedReceiver<CompletedDeferredMainDocumentLoadCompletion>,
|
||||
pending_load: Option<PendingAdapterLoadObservation<A>>,
|
||||
pending_loads: Vec<PendingAdapterLoadObservation>,
|
||||
}
|
||||
|
||||
struct PendingAdapterLoadObservation<A> {
|
||||
struct PendingAdapterLoadObservation {
|
||||
observation_id: DeferredMainDocumentLoadObservationId,
|
||||
output_interest: DeferredMainDocumentLoadCompletionOutputInterest,
|
||||
attachment: A,
|
||||
}
|
||||
|
||||
pub(crate) enum ProtocolAdapterSchedulerInput {
|
||||
@@ -45,9 +43,9 @@ pub(crate) enum ProtocolAdapterSchedulerInput {
|
||||
/// Result of consuming one shared adapter-scheduler input.
|
||||
///
|
||||
/// `DeferredLoadStarted` deliberately does not expose the pending observer or
|
||||
/// its wake interest. The exact identity and adapter attachment remain owned
|
||||
/// by `ProtocolAdapterScheduler` until `DeferredLoadCompleted`.
|
||||
pub(crate) enum ProtocolAdapterSchedulerAdvance<A> {
|
||||
/// its wake interest. The exact identity remains owned by
|
||||
/// `ProtocolAdapterScheduler` until `DeferredLoadCompleted`.
|
||||
pub(crate) enum ProtocolAdapterSchedulerAdvance {
|
||||
Idle,
|
||||
ClientTurnYielded,
|
||||
DeferredLoadStarted {
|
||||
@@ -56,7 +54,6 @@ pub(crate) enum ProtocolAdapterSchedulerAdvance<A> {
|
||||
ProtocolResidenceCompleted(ProtocolOutputSequence),
|
||||
DeferredLoadCompleted {
|
||||
observation_id: DeferredMainDocumentLoadObservationId,
|
||||
attachment: A,
|
||||
output: ProtocolOutputSequence,
|
||||
},
|
||||
StaleDeferredLoadCompletion {
|
||||
@@ -64,7 +61,7 @@ pub(crate) enum ProtocolAdapterSchedulerAdvance<A> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<A> Default for ProtocolAdapterScheduler<A> {
|
||||
impl Default for ProtocolAdapterScheduler {
|
||||
fn default() -> Self {
|
||||
let (turn_tx, turn_rx) = mpsc::unbounded_channel();
|
||||
let (load_completion_tx, load_completion_rx) = mpsc::unbounded_channel();
|
||||
@@ -74,20 +71,14 @@ impl<A> Default for ProtocolAdapterScheduler<A> {
|
||||
turn_scheduled: false,
|
||||
load_completion_tx,
|
||||
load_completion_rx,
|
||||
pending_load: None,
|
||||
pending_loads: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> ProtocolAdapterScheduler<A> {
|
||||
pub(crate) fn has_pending_load(&self) -> bool {
|
||||
self.pending_load.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn pending_load_attachment_mut(&mut self) -> Option<&mut A> {
|
||||
self.pending_load
|
||||
.as_mut()
|
||||
.map(|pending| &mut pending.attachment)
|
||||
impl ProtocolAdapterScheduler {
|
||||
pub(crate) fn has_pending_loads(&self) -> bool {
|
||||
!self.pending_loads.is_empty()
|
||||
}
|
||||
|
||||
/// Coalesces scheduler readiness into one later adapter turn.
|
||||
@@ -128,7 +119,7 @@ impl<A> ProtocolAdapterScheduler<A> {
|
||||
pub(crate) async fn recv_input(&mut self) -> ProtocolAdapterSchedulerInput {
|
||||
tokio::select! {
|
||||
biased;
|
||||
completion = self.load_completion_rx.recv(), if self.has_pending_load() => {
|
||||
completion = self.load_completion_rx.recv(), if self.has_pending_loads() => {
|
||||
ProtocolAdapterSchedulerInput::DeferredLoadCompletion(Box::new(
|
||||
completion.expect("shared adapter load-completion channel must remain open"),
|
||||
))
|
||||
@@ -145,51 +136,46 @@ impl<A> ProtocolAdapterScheduler<A> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ingests one concrete renderer publication using the exact load
|
||||
/// observation, if any, currently owned by this connection-local driver.
|
||||
/// Ingests one concrete renderer publication behind every matching exact
|
||||
/// load observation currently owned by this connection-local driver.
|
||||
pub(crate) async fn ingest_renderer_publication(
|
||||
&mut self,
|
||||
scheduler: &mut CdpScheduler,
|
||||
publication: RendererOutputTransportMessage,
|
||||
) -> ProtocolOutputSequence {
|
||||
let Some(pending) = self.pending_load.as_ref() else {
|
||||
if self.pending_loads.is_empty() {
|
||||
return scheduler
|
||||
.ingest_renderer_publication_for_scheduler(publication)
|
||||
.await;
|
||||
};
|
||||
let observation_id = pending.observation_id;
|
||||
match scheduler.route_renderer_output_for_deferred_load_completion(
|
||||
&publication,
|
||||
&pending.output_interest,
|
||||
) {
|
||||
DeferredMainDocumentLoadCompletionOutputAction::ProcessNow => {
|
||||
scheduler.ingest_renderer_publication_now(publication).await
|
||||
}
|
||||
DeferredMainDocumentLoadCompletionOutputAction::Queue => {
|
||||
scheduler
|
||||
.ingest_renderer_publication_after_load(publication, observation_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
let observation_ids = self
|
||||
.pending_loads
|
||||
.iter()
|
||||
.filter_map(|pending| {
|
||||
(scheduler.route_renderer_output_for_deferred_load_completion(
|
||||
&publication,
|
||||
&pending.output_interest,
|
||||
) == DeferredMainDocumentLoadCompletionOutputAction::Queue)
|
||||
.then_some(pending.observation_id)
|
||||
})
|
||||
.collect();
|
||||
scheduler
|
||||
.ingest_renderer_publication_after_loads(publication, observation_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Consumes one input and advances at most one concrete scheduler
|
||||
/// residence.
|
||||
///
|
||||
/// `make_load_attachment` is called only when this turn starts a new exact
|
||||
/// load observation. Keeping that attachment inside the shared driver
|
||||
/// prevents adapters from maintaining a parallel observation id or
|
||||
/// generation solely to associate output-routing state with the terminal.
|
||||
pub(crate) async fn advance_input(
|
||||
&mut self,
|
||||
scheduler: &mut CdpScheduler,
|
||||
input: ProtocolAdapterSchedulerInput,
|
||||
make_load_attachment: impl FnOnce() -> A,
|
||||
) -> ProtocolAdapterSchedulerAdvance<A> {
|
||||
) -> ProtocolAdapterSchedulerAdvance {
|
||||
match input {
|
||||
ProtocolAdapterSchedulerInput::Turn => {
|
||||
self.turn_scheduled = false;
|
||||
self.advance_turn(scheduler, make_load_attachment).await
|
||||
self.advance_turn(scheduler).await
|
||||
}
|
||||
ProtocolAdapterSchedulerInput::DeferredLoadCompletion(completion) => {
|
||||
self.complete_load(scheduler, *completion).await
|
||||
@@ -200,8 +186,7 @@ impl<A> ProtocolAdapterScheduler<A> {
|
||||
async fn advance_turn(
|
||||
&mut self,
|
||||
scheduler: &mut CdpScheduler,
|
||||
make_load_attachment: impl FnOnce() -> A,
|
||||
) -> ProtocolAdapterSchedulerAdvance<A> {
|
||||
) -> ProtocolAdapterSchedulerAdvance {
|
||||
match self.next_scheduler_step(scheduler) {
|
||||
ProtocolSchedulerStep::SatisfyClientTurnPredecessor => {
|
||||
scheduler.satisfy_front_protocol_residence_client_turn_predecessor();
|
||||
@@ -210,19 +195,21 @@ impl<A> ProtocolAdapterScheduler<A> {
|
||||
ProtocolSchedulerStep::CompleteReadyResidence
|
||||
if scheduler.next_ready_protocol_residence_is_main_document_load_action() =>
|
||||
{
|
||||
assert!(
|
||||
self.pending_load.is_none(),
|
||||
"one adapter scheduler cannot start a second load observation"
|
||||
);
|
||||
let pending = scheduler
|
||||
.start_next_deferred_load_completion()
|
||||
.expect("ready load residence must produce an exact pending observation");
|
||||
let observation_id = pending.observation_id();
|
||||
let output_interest = pending.output_interest();
|
||||
self.pending_load = Some(PendingAdapterLoadObservation {
|
||||
assert!(
|
||||
!self
|
||||
.pending_loads
|
||||
.iter()
|
||||
.any(|pending| pending.observation_id == observation_id),
|
||||
"one exact load observation cannot be started twice"
|
||||
);
|
||||
self.pending_loads.push(PendingAdapterLoadObservation {
|
||||
observation_id,
|
||||
output_interest,
|
||||
attachment: make_load_attachment(),
|
||||
});
|
||||
self.spawn_load_wait(pending);
|
||||
ProtocolAdapterSchedulerAdvance::DeferredLoadStarted { observation_id }
|
||||
@@ -237,33 +224,10 @@ impl<A> ProtocolAdapterScheduler<A> {
|
||||
}
|
||||
|
||||
/// Returns the next concrete-residence transition this adapter may drive.
|
||||
///
|
||||
/// An in-flight exact load observation reserves this driver's one load
|
||||
/// attachment slot; it does not precede unrelated scheduler work.
|
||||
/// `CdpScheduler` already makes exact `load_predecessors` authoritative.
|
||||
/// Keeping no-predecessor owner actions runnable is required because a
|
||||
/// replacement or termination action may itself settle the observation.
|
||||
/// Exact `load_predecessors` in `CdpScheduler` provide the target-local
|
||||
/// ordering; the adapter must not add a connection-wide capacity gate.
|
||||
fn next_scheduler_step(&self, scheduler: &CdpScheduler) -> ProtocolSchedulerStep {
|
||||
let step = scheduler.next_protocol_scheduler_step();
|
||||
self.enforce_load_attachment_capacity(
|
||||
step,
|
||||
scheduler.next_ready_protocol_residence_is_main_document_load_action(),
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_load_attachment_capacity(
|
||||
&self,
|
||||
step: ProtocolSchedulerStep,
|
||||
next_ready_residence_is_main_document_load_action: bool,
|
||||
) -> ProtocolSchedulerStep {
|
||||
if self.has_pending_load()
|
||||
&& step == ProtocolSchedulerStep::CompleteReadyResidence
|
||||
&& next_ready_residence_is_main_document_load_action
|
||||
{
|
||||
ProtocolSchedulerStep::Wait
|
||||
} else {
|
||||
step
|
||||
}
|
||||
scheduler.next_protocol_scheduler_step()
|
||||
}
|
||||
|
||||
fn spawn_load_wait(&self, pending: PendingDeferredMainDocumentLoadCompletion) {
|
||||
@@ -285,40 +249,32 @@ impl<A> ProtocolAdapterScheduler<A> {
|
||||
&mut self,
|
||||
scheduler: &mut CdpScheduler,
|
||||
completion: CompletedDeferredMainDocumentLoadCompletion,
|
||||
) -> ProtocolAdapterSchedulerAdvance<A> {
|
||||
) -> ProtocolAdapterSchedulerAdvance {
|
||||
let observation_id = completion.observation_id();
|
||||
let Ok(pending) = self.take_pending_load(observation_id) else {
|
||||
if self.take_pending_load(observation_id).is_err() {
|
||||
return ProtocolAdapterSchedulerAdvance::StaleDeferredLoadCompletion { observation_id };
|
||||
};
|
||||
}
|
||||
let output = scheduler
|
||||
.complete_deferred_load_completion(completion)
|
||||
.await;
|
||||
ProtocolAdapterSchedulerAdvance::DeferredLoadCompleted {
|
||||
observation_id,
|
||||
attachment: pending.attachment,
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
/// Claims the attachment only for the exact observation that produced a
|
||||
/// terminal.
|
||||
///
|
||||
/// A delayed terminal from an already-retired observation must not detach
|
||||
/// the current adapter mode or command-routing state. Restoring the
|
||||
/// nonmatching value here keeps that invariant independent of how each
|
||||
/// adapter reacts to `StaleDeferredLoadCompletion`.
|
||||
/// Claims only the exact observation that produced a terminal. A delayed
|
||||
/// or duplicate terminal must not retire another Page's load wait.
|
||||
fn take_pending_load(
|
||||
&mut self,
|
||||
observation_id: DeferredMainDocumentLoadObservationId,
|
||||
) -> Result<PendingAdapterLoadObservation<A>, ()> {
|
||||
let Some(pending) = self.pending_load.take() else {
|
||||
return Err(());
|
||||
};
|
||||
if pending.observation_id != observation_id {
|
||||
self.pending_load = Some(pending);
|
||||
return Err(());
|
||||
}
|
||||
Ok(pending)
|
||||
) -> Result<PendingAdapterLoadObservation, ()> {
|
||||
let index = self
|
||||
.pending_loads
|
||||
.iter()
|
||||
.position(|pending| pending.observation_id == observation_id)
|
||||
.ok_or(())?;
|
||||
Ok(self.pending_loads.remove(index))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,15 +319,14 @@ mod tests {
|
||||
scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished {
|
||||
work: protocol_observation(1),
|
||||
}]);
|
||||
let adapter = ProtocolAdapterScheduler::<()> {
|
||||
pending_load: Some(PendingAdapterLoadObservation {
|
||||
let adapter = ProtocolAdapterScheduler {
|
||||
pending_loads: vec![PendingAdapterLoadObservation {
|
||||
observation_id,
|
||||
output_interest: deferred_main_document_load_output_interest(
|
||||
page_residence(),
|
||||
None,
|
||||
),
|
||||
attachment: (),
|
||||
}),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -387,47 +342,65 @@ mod tests {
|
||||
"independent protocol work must remain runnable while the exact observation waits"
|
||||
);
|
||||
assert!(
|
||||
adapter.has_pending_load(),
|
||||
"running independent work must not release the exact load attachment"
|
||||
adapter.has_pending_loads(),
|
||||
"running independent work must not release the exact load observation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_exact_load_observation_blocks_second_load_attachment() {
|
||||
let observation_id = deferred_main_document_load_observation_id(1);
|
||||
let adapter = ProtocolAdapterScheduler::<()> {
|
||||
pending_load: Some(PendingAdapterLoadObservation {
|
||||
observation_id,
|
||||
output_interest: deferred_main_document_load_output_interest(
|
||||
page_residence(),
|
||||
None,
|
||||
),
|
||||
attachment: (),
|
||||
}),
|
||||
fn multiple_exact_load_observations_retire_independently() {
|
||||
let first = deferred_main_document_load_observation_id(1);
|
||||
let second = deferred_main_document_load_observation_id(2);
|
||||
let stale = deferred_main_document_load_observation_id(3);
|
||||
let mut adapter = ProtocolAdapterScheduler {
|
||||
pending_loads: vec![
|
||||
PendingAdapterLoadObservation {
|
||||
observation_id: first,
|
||||
output_interest: deferred_main_document_load_output_interest(
|
||||
page_residence(),
|
||||
None,
|
||||
),
|
||||
},
|
||||
PendingAdapterLoadObservation {
|
||||
observation_id: second,
|
||||
output_interest: deferred_main_document_load_output_interest(
|
||||
RendererOutputResidenceIdentity::Page {
|
||||
owner_local_host_id: RendererOwnerLocalHostId::new_for_testing(1),
|
||||
page_id: PageId::new_for_testing(8),
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(adapter.pending_loads.len(), 2);
|
||||
adapter
|
||||
.take_pending_load(second)
|
||||
.expect("the second Page terminal must claim only its observation");
|
||||
assert_eq!(
|
||||
adapter.enforce_load_attachment_capacity(
|
||||
ProtocolSchedulerStep::CompleteReadyResidence,
|
||||
true,
|
||||
),
|
||||
ProtocolSchedulerStep::Wait,
|
||||
"one adapter cannot start a second exact load observation"
|
||||
adapter
|
||||
.pending_loads
|
||||
.iter()
|
||||
.map(|pending| pending.observation_id)
|
||||
.collect::<Vec<_>>(),
|
||||
[first],
|
||||
"an out-of-order terminal must preserve the first Page wait"
|
||||
);
|
||||
assert_eq!(
|
||||
adapter.enforce_load_attachment_capacity(
|
||||
ProtocolSchedulerStep::CompleteReadyResidence,
|
||||
false,
|
||||
),
|
||||
ProtocolSchedulerStep::CompleteReadyResidence,
|
||||
"load attachment capacity must not become a global execution lock"
|
||||
assert!(
|
||||
adapter.take_pending_load(stale).is_err(),
|
||||
"an unknown terminal must not retire another Page wait"
|
||||
);
|
||||
adapter
|
||||
.take_pending_load(first)
|
||||
.expect("the first Page terminal should remain claimable");
|
||||
assert!(!adapter.has_pending_loads());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idle_adapter_input_remains_pending() {
|
||||
let mut adapter = ProtocolAdapterScheduler::<()>::default();
|
||||
let mut adapter = ProtocolAdapterScheduler::default();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = adapter.recv_input() => {
|
||||
@@ -445,18 +418,14 @@ mod tests {
|
||||
scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished {
|
||||
work: protocol_observation(1),
|
||||
}]);
|
||||
let mut adapter = ProtocolAdapterScheduler::<()>::default();
|
||||
let mut adapter = ProtocolAdapterScheduler::default();
|
||||
|
||||
adapter.schedule_turn_if_needed(&scheduler, false);
|
||||
adapter.schedule_turn_if_needed(&scheduler, false);
|
||||
let first = adapter.recv_input().await;
|
||||
assert!(matches!(first, ProtocolAdapterSchedulerInput::Turn));
|
||||
assert!(matches!(
|
||||
adapter
|
||||
.advance_input(&mut scheduler, first, || {
|
||||
panic!("ordinary protocol work cannot create a load attachment")
|
||||
})
|
||||
.await,
|
||||
adapter.advance_input(&mut scheduler, first).await,
|
||||
ProtocolAdapterSchedulerAdvance::ClientTurnYielded
|
||||
));
|
||||
assert!(
|
||||
@@ -468,66 +437,10 @@ mod tests {
|
||||
let second = adapter.recv_input().await;
|
||||
assert!(matches!(second, ProtocolAdapterSchedulerInput::Turn));
|
||||
assert!(matches!(
|
||||
adapter
|
||||
.advance_input(&mut scheduler, second, || {
|
||||
panic!("ordinary protocol work cannot create a load attachment")
|
||||
})
|
||||
.await,
|
||||
adapter.advance_input(&mut scheduler, second).await,
|
||||
ProtocolAdapterSchedulerAdvance::ProtocolResidenceCompleted(_)
|
||||
));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum AdapterMode {
|
||||
Classic,
|
||||
Bidi { session_id: &'static str },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_load_attachment_survives_adapter_switch_and_stale_terminal() {
|
||||
let current = deferred_main_document_load_observation_id(2);
|
||||
let stale = deferred_main_document_load_observation_id(1);
|
||||
let mut adapter = ProtocolAdapterScheduler::<AdapterMode> {
|
||||
pending_load: Some(PendingAdapterLoadObservation {
|
||||
observation_id: current,
|
||||
output_interest: deferred_main_document_load_output_interest(
|
||||
page_residence(),
|
||||
None,
|
||||
),
|
||||
attachment: AdapterMode::Classic,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
*adapter
|
||||
.pending_load_attachment_mut()
|
||||
.expect("exact load attachment should remain resident") = AdapterMode::Bidi {
|
||||
session_id: "SID-upgraded",
|
||||
};
|
||||
|
||||
assert!(
|
||||
adapter.take_pending_load(stale).is_err(),
|
||||
"a delayed terminal must not claim the current load attachment"
|
||||
);
|
||||
assert_eq!(
|
||||
adapter
|
||||
.pending_load_attachment_mut()
|
||||
.expect("stale terminal must preserve current attachment"),
|
||||
&AdapterMode::Bidi {
|
||||
session_id: "SID-upgraded",
|
||||
}
|
||||
);
|
||||
let claimed = adapter
|
||||
.take_pending_load(current)
|
||||
.expect("the exact terminal should claim its attachment");
|
||||
assert_eq!(
|
||||
claimed.attachment,
|
||||
AdapterMode::Bidi {
|
||||
session_id: "SID-upgraded",
|
||||
}
|
||||
);
|
||||
assert!(!adapter.has_pending_load());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ pub(crate) struct CommandTurnOutput {
|
||||
output_release_permit: Option<CommandOutputReleasePermit>,
|
||||
}
|
||||
|
||||
pub(crate) enum CommandDispatchStepOutput {
|
||||
Emit(ProtocolOutputSequence),
|
||||
}
|
||||
|
||||
impl CommandDispatchState {
|
||||
pub(crate) fn pending_command() -> Self {
|
||||
Self {
|
||||
@@ -27,13 +23,6 @@ impl CommandDispatchState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn route_pending_background_event(
|
||||
&mut self,
|
||||
event: BackgroundProtocolEvent,
|
||||
) -> CommandDispatchStepOutput {
|
||||
CommandDispatchStepOutput::Emit(ProtocolOutputSequence::from_background_event(event))
|
||||
}
|
||||
|
||||
pub(crate) fn complete_with_turn_output(
|
||||
mut self,
|
||||
turn_output: CommandTurnOutput,
|
||||
@@ -57,14 +46,6 @@ impl CommandDispatchState {
|
||||
.with_renderer_output_boundary(renderer_output_boundary, post_renderer_output)
|
||||
.with_renderer_output_predecessor(renderer_output_predecessor)
|
||||
}
|
||||
|
||||
pub(crate) fn complete_protocol_output(
|
||||
mut self,
|
||||
output: ProtocolOutputSequence,
|
||||
) -> ProtocolOutputSequence {
|
||||
self.replies = output;
|
||||
self.replies
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandTurnOutput {
|
||||
|
||||
@@ -176,7 +176,7 @@ async fn handle_bidi_session_socket_local(
|
||||
navigation_runtime_config,
|
||||
);
|
||||
actor.install_runtime_response_ready_sender(&mut scheduler);
|
||||
let mut adapter_scheduler = ProtocolAdapterScheduler::<()>::default();
|
||||
let mut adapter_scheduler = ProtocolAdapterScheduler::default();
|
||||
loop {
|
||||
let page_javascript_blocked = scheduler.has_pending_javascript_dialog();
|
||||
adapter_scheduler.schedule_turn_if_needed(&scheduler, page_javascript_blocked);
|
||||
@@ -362,7 +362,7 @@ impl BidiSocketActor {
|
||||
/// contract.
|
||||
pub(in crate::protocol_server) async fn recv_attached_input(
|
||||
&mut self,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<()>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
page_javascript_blocked: bool,
|
||||
) -> BidiSocketActorInput {
|
||||
tokio::select! {
|
||||
@@ -425,7 +425,7 @@ impl BidiSocketActor {
|
||||
|
||||
pub(in crate::protocol_server) async fn handle_renderer_publication(
|
||||
&mut self,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<()>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
scheduler: &mut CdpScheduler,
|
||||
receivers: &mut CdpSchedulerEventReceivers,
|
||||
publication: RendererOutputTransportMessage,
|
||||
@@ -465,15 +465,12 @@ impl BidiSocketActor {
|
||||
|
||||
pub(in crate::protocol_server) async fn handle_adapter_scheduler_input(
|
||||
&mut self,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<()>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
scheduler: &mut CdpScheduler,
|
||||
receivers: &mut CdpSchedulerEventReceivers,
|
||||
input: ProtocolAdapterSchedulerInput,
|
||||
) -> bool {
|
||||
let output = match adapter_scheduler
|
||||
.advance_input(scheduler, input, || ())
|
||||
.await
|
||||
{
|
||||
let output = match adapter_scheduler.advance_input(scheduler, input).await {
|
||||
ProtocolAdapterSchedulerAdvance::ProtocolResidenceCompleted(output)
|
||||
| ProtocolAdapterSchedulerAdvance::DeferredLoadCompleted { output, .. } => output,
|
||||
ProtocolAdapterSchedulerAdvance::Idle
|
||||
|
||||
@@ -1348,7 +1348,7 @@ async fn classic_session_runtime_loop(
|
||||
navigation_runtime_config,
|
||||
);
|
||||
let mut attached_bidi: Option<ClassicAttachedBidiSocket> = None;
|
||||
let mut adapter_scheduler = ProtocolAdapterScheduler::<()>::default();
|
||||
let mut adapter_scheduler = ProtocolAdapterScheduler::default();
|
||||
loop {
|
||||
if receivers.renderer_publication_rx.is_closed() {
|
||||
break;
|
||||
@@ -1532,7 +1532,7 @@ async fn classic_session_runtime_loop(
|
||||
}
|
||||
input = adapter_scheduler.recv_input(), if !page_javascript_blocked => {
|
||||
let _ = adapter_scheduler
|
||||
.advance_input(&mut scheduler, input, || ())
|
||||
.advance_input(&mut scheduler, input)
|
||||
.await;
|
||||
}
|
||||
request = rx.recv() => {
|
||||
@@ -1874,7 +1874,7 @@ impl ValueExt for serde_json::Value {
|
||||
}
|
||||
|
||||
async fn classic_session_ingest_ready_renderer_publications(
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler<()>,
|
||||
adapter_scheduler: &mut ProtocolAdapterScheduler,
|
||||
scheduler: &mut CdpScheduler,
|
||||
receivers: &mut CdpSchedulerEventReceivers,
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use moli_cookie_jar::{StoredCookie, StoredCookieQueryReport};
|
||||
use moli_fetch::FetchConfig;
|
||||
use parking_lot::Mutex;
|
||||
@@ -1163,7 +1164,11 @@ pub struct CdpConnection {
|
||||
/// Chromium/Playwright auto-attach can ask new targets to wait until
|
||||
/// Runtime.runIfWaitingForDebugger before their initial document proceeds.
|
||||
pub auto_attach_wait_for_debugger_on_start: bool,
|
||||
auto_attach_owner_sessions: HashMap<Option<String>, AutoAttachOwnerPolicy>,
|
||||
// Insertion order is protocol state: the first matching owner supplies
|
||||
// the primary auto-attached Page session, while later owners attach as
|
||||
// auxiliary sessions. A randomized HashMap iteration order made that
|
||||
// choice vary between otherwise identical processes.
|
||||
auto_attach_owner_sessions: IndexMap<Option<String>, AutoAttachOwnerPolicy>,
|
||||
target_control: TargetControlPlane,
|
||||
default_target_lifecycle: DefaultTargetLifecycle,
|
||||
service_worker_auto_attach_related_owners: Vec<ServiceWorkerAutoAttachRelatedOwner>,
|
||||
@@ -1475,7 +1480,7 @@ impl CdpConnection {
|
||||
target_info_change_events_enabled: false,
|
||||
target_discovery_filter: None,
|
||||
auto_attach_wait_for_debugger_on_start: false,
|
||||
auto_attach_owner_sessions: HashMap::new(),
|
||||
auto_attach_owner_sessions: IndexMap::new(),
|
||||
target_control: TargetControlPlane::default(),
|
||||
default_target_lifecycle: DefaultTargetLifecycle::default(),
|
||||
service_worker_auto_attach_related_owners: Vec::new(),
|
||||
@@ -1881,6 +1886,37 @@ impl CdpConnection {
|
||||
self.loaded_page_mut_for_interruptible_protocol_access_for_route(session_id, owner_route)
|
||||
}
|
||||
|
||||
/// Returns the Page that currently carries target-scoped configuration.
|
||||
///
|
||||
/// A cross-Document navigation keeps its outgoing Page attached until the
|
||||
/// replacement commits. Network and Emulation settings belong to the
|
||||
/// stable target/session, so they must remain writable during that window:
|
||||
/// the outgoing Page needs the update if navigation fails, while commit
|
||||
/// configuration replays the same target state into the replacement Page.
|
||||
/// Document-reading commands must continue to use
|
||||
/// [`Self::loaded_page_mut_for_protocol_access`] and observe the navigation
|
||||
/// gate instead.
|
||||
pub(crate) fn loaded_page_mut_for_target_configuration(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<&mut Page, String> {
|
||||
let none_session_owner_route = self.none_session_owner_route_override();
|
||||
self.loaded_page_mut_for_target_configuration_for_route(
|
||||
session_id,
|
||||
none_session_owner_route.as_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn loaded_page_mut_for_target_configuration_for_route(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
owner_route: Option<&CdpSessionRoute>,
|
||||
) -> Result<&mut Page, String> {
|
||||
self.runtime_session_owner_slot_mut_for_route(session_id, owner_route)?
|
||||
.loaded_page_mut()
|
||||
.ok_or_else(|| "NoDocumentLoaded".to_owned())
|
||||
}
|
||||
|
||||
/// Returns the exact Page that remains attached while a cross-Document
|
||||
/// navigation is suspended.
|
||||
///
|
||||
@@ -2259,12 +2295,12 @@ impl CdpConnection {
|
||||
Some(route),
|
||||
));
|
||||
}
|
||||
Some(CommandOwnerScope::from_session_and_owner_route(
|
||||
Some(CommandOwnerScope::capture(
|
||||
self,
|
||||
context
|
||||
.session_id
|
||||
.as_ref()
|
||||
.map(|session_id| session_id.as_str()),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +414,22 @@ impl CdpConnection {
|
||||
session_id: Option<&str>,
|
||||
body: CapturedBody,
|
||||
) -> Result<String, String> {
|
||||
let Some(mut owner) = self.target_session_owner_mut(session_id) else {
|
||||
let none_session_owner_route = self.none_session_owner_route_override();
|
||||
self.open_io_stream_body_source_for_route(
|
||||
session_id,
|
||||
none_session_owner_route.as_ref(),
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn open_io_stream_body_source_for_route(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
owner_route: Option<&CdpSessionRoute>,
|
||||
body: CapturedBody,
|
||||
) -> Result<String, String> {
|
||||
let Some(mut owner) = self.target_session_owner_mut_for_route(session_id, owner_route)
|
||||
else {
|
||||
return Err("NoDocumentLoaded".to_owned());
|
||||
};
|
||||
owner.open_scoped_io_stream_body_source(body)
|
||||
|
||||
@@ -1053,9 +1053,8 @@ impl CdpConnection {
|
||||
};
|
||||
if refresh_active_engine {
|
||||
self.apply_active_engine_fetch_overrides();
|
||||
return self.start_rebuild_resource_runtime_for_session_owner(session_id);
|
||||
}
|
||||
Ok(None)
|
||||
self.start_rebuild_resource_runtime_for_session_owner(session_id)
|
||||
}
|
||||
|
||||
fn start_set_non_page_browser_identity_override(
|
||||
@@ -1116,9 +1115,8 @@ impl CdpConnection {
|
||||
};
|
||||
if refresh_active_engine {
|
||||
self.apply_active_engine_fetch_overrides();
|
||||
return self.start_rebuild_resource_runtime_for_session_owner(session_id);
|
||||
}
|
||||
Ok(None)
|
||||
self.start_rebuild_resource_runtime_for_session_owner(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn start_set_tls_verify_host_for_session_owner(
|
||||
|
||||
@@ -173,7 +173,10 @@ pub(crate) struct TargetNavigationLoadInputs {
|
||||
storage_handles: TargetNavigationStorageHandles,
|
||||
pub(crate) root_frame_id: Option<String>,
|
||||
pub(crate) renderer_runtime: RendererBrowserContextRuntimeOwnerAccess,
|
||||
/// Browser-side identity used for navigation request headers.
|
||||
pub(crate) browser_identity_override: Option<moli_browser_profile::BrowserIdentityProfile>,
|
||||
/// Renderer-agent identity exposed through the committed Document's Navigator.
|
||||
pub(crate) navigator_identity_override: Option<moli_browser_profile::BrowserIdentityProfile>,
|
||||
pub(crate) http_proxy_override: Option<String>,
|
||||
pub(crate) http_no_proxy_override: Option<String>,
|
||||
pub(crate) tls_verify_host_override: Option<bool>,
|
||||
@@ -270,6 +273,8 @@ impl TargetNavigationLoadInputs {
|
||||
renderer_runtime: browser_context.renderer_runtime_owner_access(),
|
||||
browser_identity_override: browser_context
|
||||
.effective_active_browser_identity_override_owned(),
|
||||
navigator_identity_override: browser_context
|
||||
.effective_active_renderer_browser_identity_override_owned(),
|
||||
http_proxy_override: browser_context.effective_active_http_proxy_override_owned(),
|
||||
http_no_proxy_override: browser_context.effective_active_http_no_proxy_override_owned(),
|
||||
tls_verify_host_override: browser_context.effective_active_tls_verify_host_override(),
|
||||
@@ -324,6 +329,8 @@ impl TargetNavigationLoadInputs {
|
||||
inputs.browser_context_id = Some(browser_context.id.clone());
|
||||
inputs.browser_identity_override =
|
||||
browser_context.effective_active_browser_identity_override_owned();
|
||||
inputs.navigator_identity_override =
|
||||
browser_context.effective_active_renderer_browser_identity_override_owned();
|
||||
inputs.http_proxy_override = browser_context.effective_active_http_proxy_override_owned();
|
||||
inputs.http_no_proxy_override =
|
||||
browser_context.effective_active_http_no_proxy_override_owned();
|
||||
@@ -351,6 +358,7 @@ impl TargetNavigationLoadInputs {
|
||||
root_frame_id: None,
|
||||
renderer_runtime,
|
||||
browser_identity_override: None,
|
||||
navigator_identity_override: None,
|
||||
http_proxy_override: None,
|
||||
http_no_proxy_override: None,
|
||||
tls_verify_host_override: None,
|
||||
@@ -805,6 +813,9 @@ impl<'a> TargetSessionOwnerRef<'a> {
|
||||
.network_policy
|
||||
.browser_identity_override_owned()
|
||||
.or_else(|| browser_context.default_browser_identity_override_owned()),
|
||||
navigator_identity_override: page_state
|
||||
.effective_renderer_browser_identity_override_owned()
|
||||
.or_else(|| browser_context.default_browser_identity_override_owned()),
|
||||
http_proxy_override: page_state
|
||||
.http_proxy_override
|
||||
.clone()
|
||||
|
||||
@@ -10,7 +10,16 @@ impl CommandOwnerScope {
|
||||
pub(crate) fn capture(conn: &CdpConnection, session_id: Option<&str>) -> Self {
|
||||
let none_session_owner_route = session_id
|
||||
.is_none()
|
||||
.then(|| conn.none_session_owner_route_override())
|
||||
.then(|| {
|
||||
conn.none_session_owner_route_override().or_else(|| {
|
||||
let browser_context = conn.browser_context.as_ref()?;
|
||||
let target_id = browser_context.active_target_id_owned()?;
|
||||
Some(CdpSessionRoute::ActiveTarget {
|
||||
browser_context_id: browser_context.id.clone(),
|
||||
target_id: Some(target_id),
|
||||
})
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
Self {
|
||||
session_id: session_id.map(str::to_owned),
|
||||
@@ -35,8 +44,9 @@ impl CommandOwnerScope {
|
||||
/// Returns the exact route captured for an implicit-session command.
|
||||
///
|
||||
/// A concrete CDP session remains authoritative through `session_id`; the
|
||||
/// route is only needed for protocol-neutral and deferred work which uses
|
||||
/// Chromium's implicit primary Page attachment.
|
||||
/// route freezes Chromium's implicit primary Page attachment at command
|
||||
/// admission, so deferred completion cannot follow a later foreground
|
||||
/// selection.
|
||||
pub(crate) fn session_owner_route(&self) -> Option<&CdpSessionRoute> {
|
||||
self.session_owner_route.as_ref()
|
||||
}
|
||||
@@ -48,3 +58,28 @@ impl CommandOwnerScope {
|
||||
conn.scoped_optional_none_session_owner_route_override(self.session_owner_route.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conn::BrowserContext;
|
||||
|
||||
#[test]
|
||||
fn implicit_scope_freezes_the_concrete_active_target() {
|
||||
let mut conn = CdpConnection::default();
|
||||
let mut browser_context = BrowserContext::new("BID-scope".to_owned());
|
||||
browser_context.set_active_target_id("TID-original");
|
||||
conn.browser_context = Some(browser_context);
|
||||
|
||||
let scope = CommandOwnerScope::capture(&conn, None);
|
||||
conn.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context")
|
||||
.set_active_target_id("TID-replacement");
|
||||
|
||||
assert_eq!(
|
||||
conn.target_owner_identity_for_route(scope.session_id(), scope.session_owner_route(),),
|
||||
Some(("BID-scope".to_owned(), Some("TID-original".to_owned())))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +374,8 @@ impl BrowserContext {
|
||||
let previous_bypass = state.network_policy.bypass_service_worker();
|
||||
let previous_cache_disabled = state.network_policy.cache_disabled();
|
||||
let previous_browser_identity = state.network_policy.browser_identity_override_owned();
|
||||
let previous_renderer_browser_identity =
|
||||
state.effective_renderer_browser_identity_override_owned();
|
||||
let previous_locale = state.locale_override.clone();
|
||||
let previous_timezone = state.timezone_override.clone();
|
||||
let routed_session_id = is_auxiliary.then_some(session_id);
|
||||
@@ -386,6 +388,10 @@ impl BrowserContext {
|
||||
previous_cache_disabled != state.network_policy.cache_disabled();
|
||||
let browser_identity_changed =
|
||||
previous_browser_identity != state.network_policy.browser_identity_override_owned();
|
||||
let renderer_browser_identity_changed = previous_renderer_browser_identity
|
||||
!= state.effective_renderer_browser_identity_override_owned();
|
||||
let any_browser_identity_changed =
|
||||
browser_identity_changed || renderer_browser_identity_changed;
|
||||
let locale_changed = previous_locale != state.locale_override;
|
||||
let timezone_changed = previous_timezone != state.timezone_override;
|
||||
|
||||
@@ -395,7 +401,7 @@ impl BrowserContext {
|
||||
&& !locale_changed
|
||||
&& !timezone_changed
|
||||
{
|
||||
return Ok(browser_identity_changed);
|
||||
return Ok(any_browser_identity_changed);
|
||||
}
|
||||
|
||||
let (
|
||||
@@ -433,7 +439,7 @@ impl BrowserContext {
|
||||
.and_then(|target| target.runtime_slot.loaded_page_mut())
|
||||
};
|
||||
let Some(page) = page else {
|
||||
return Ok(browser_identity_changed);
|
||||
return Ok(any_browser_identity_changed);
|
||||
};
|
||||
if extra_headers_changed || bypass_service_worker_changed || cache_disabled_changed {
|
||||
page.set_network_request_policy_async(
|
||||
@@ -456,7 +462,7 @@ impl BrowserContext {
|
||||
.await
|
||||
.map_err(|error| format!("failed to restore detached session timezone: {error}"))?;
|
||||
}
|
||||
Ok(browser_identity_changed)
|
||||
Ok(any_browser_identity_changed)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_auxiliary_sessions_for_target(&mut self, target_id: &str) -> Vec<String> {
|
||||
|
||||
@@ -228,6 +228,11 @@ impl CdpConnection {
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Option<PendingPageCommand>, String> {
|
||||
let load_inputs = self.navigation_load_inputs_for_session_owner(session_id);
|
||||
let navigator_identity = load_inputs
|
||||
.navigator_identity_override
|
||||
.clone()
|
||||
.or_else(|| self.global_browser_identity_override.clone())
|
||||
.unwrap_or_else(|| self.base_browser_identity.clone());
|
||||
let storage = load_inputs.resource_storage_handles();
|
||||
let request_client = self
|
||||
.configured_navigation_engine_for_load_inputs_mut(&load_inputs)
|
||||
@@ -239,9 +244,12 @@ impl CdpConnection {
|
||||
let Some(page) = self.resource_runtime_apply_page_for_session_owner(session_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
page.start_replace_browser_resource_runtime(&request_client.browser_resource_runtime())
|
||||
.map(Some)
|
||||
.map_err(|error| format!("failed to update page resource runtime: {error}"))
|
||||
page.start_replace_browser_resource_runtime_with_navigator_identity(
|
||||
&request_client.browser_resource_runtime(),
|
||||
navigator_identity,
|
||||
)
|
||||
.map(Some)
|
||||
.map_err(|error| format!("failed to update page resource runtime: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn finish_rebuild_resource_runtime_for_session_owner(
|
||||
@@ -249,11 +257,10 @@ impl CdpConnection {
|
||||
session_id: Option<&str>,
|
||||
completion: CompletedPageCommand,
|
||||
) -> Result<(), String> {
|
||||
let Some(page) = self.resource_runtime_apply_page_for_session_owner(session_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
page.finish_replace_browser_resource_runtime(completion)
|
||||
.map_err(|error| format!("failed to update page resource runtime: {error}"))
|
||||
finish_resource_runtime_update_on_current_attachment(
|
||||
self.resource_runtime_apply_page_for_session_owner(session_id),
|
||||
completion,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn finish_rebuild_resource_runtime_for_route(
|
||||
@@ -262,11 +269,10 @@ impl CdpConnection {
|
||||
owner_route: Option<&super::CdpSessionRoute>,
|
||||
completion: CompletedPageCommand,
|
||||
) -> Result<(), String> {
|
||||
let Some(page) = self.resource_runtime_apply_page_for_route(session_id, owner_route) else {
|
||||
return Ok(());
|
||||
};
|
||||
page.finish_replace_browser_resource_runtime(completion)
|
||||
.map_err(|error| format!("failed to update page resource runtime: {error}"))
|
||||
finish_resource_runtime_update_on_current_attachment(
|
||||
self.resource_runtime_apply_page_for_route(session_id, owner_route),
|
||||
completion,
|
||||
)
|
||||
}
|
||||
|
||||
fn resource_runtime_apply_page_for_session_owner(
|
||||
@@ -282,7 +288,8 @@ impl CdpConnection {
|
||||
.as_mut()
|
||||
.and_then(|bc| bc.active_target.runtime_slot.loaded_page_mut());
|
||||
}
|
||||
self.loaded_page_mut_for_protocol_access(session_id).ok()
|
||||
self.loaded_page_mut_for_target_configuration(session_id)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn resource_runtime_apply_page_for_route(
|
||||
@@ -299,7 +306,28 @@ impl CdpConnection {
|
||||
.as_mut()
|
||||
.and_then(|bc| bc.active_target.runtime_slot.loaded_page_mut());
|
||||
}
|
||||
self.loaded_page_mut_for_protocol_access_for_route(session_id, owner_route)
|
||||
self.loaded_page_mut_for_target_configuration_for_route(session_id, owner_route)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_resource_runtime_update_on_current_attachment(
|
||||
page: Option<&mut moli_core::page::Page>,
|
||||
completion: CompletedPageCommand,
|
||||
) -> Result<(), String> {
|
||||
let completion_attachment = completion.renderer_agent_attachment_id();
|
||||
if let Some(page) = page
|
||||
&& page.renderer_agent_attachment_id() == completion_attachment
|
||||
{
|
||||
return page
|
||||
.finish_replace_browser_resource_runtime(completion)
|
||||
.map_err(|error| format!("failed to update page resource runtime: {error}"));
|
||||
}
|
||||
|
||||
completion
|
||||
.into_unit_page_command_turn()
|
||||
.map(drop)
|
||||
.map_err(|error| {
|
||||
format!("stale resource-runtime update returned an unexpected reply: {error}")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3082,6 +3082,44 @@ impl CdpConnection {
|
||||
session_id: Option<&str>,
|
||||
dispatched_attachment_id: RendererAgentAttachmentId,
|
||||
messages: &mut Vec<RendererRuntimeInspectorMessage>,
|
||||
) {
|
||||
self.restore_frontend_command_ids_in_devtools_session_output_with_projection(
|
||||
session_id,
|
||||
dispatched_attachment_id,
|
||||
messages,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolves a terminal response which won its renderer response lease
|
||||
/// before navigation, but whose old Page journal reached protocol ingress
|
||||
/// after the replacement attachment committed.
|
||||
///
|
||||
/// The exact `(session, renderer call, attachment)` correlation remains
|
||||
/// the terminal authority in this race. Notifications and V8 state from
|
||||
/// the retired attachment are discarded by the caller, and remote-object
|
||||
/// ownership is intentionally not projected onto the replacement
|
||||
/// document.
|
||||
pub(crate) fn restore_frontend_command_ids_in_retired_devtools_session_output(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
dispatched_attachment_id: RendererAgentAttachmentId,
|
||||
messages: &mut Vec<RendererRuntimeInspectorMessage>,
|
||||
) {
|
||||
self.restore_frontend_command_ids_in_devtools_session_output_with_projection(
|
||||
session_id,
|
||||
dispatched_attachment_id,
|
||||
messages,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn restore_frontend_command_ids_in_devtools_session_output_with_projection(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
dispatched_attachment_id: RendererAgentAttachmentId,
|
||||
messages: &mut Vec<RendererRuntimeInspectorMessage>,
|
||||
project_runtime_object_ownership: bool,
|
||||
) {
|
||||
messages.retain_mut(|message| {
|
||||
let RendererRuntimeInspectorMessage::Protocol(message) = message else {
|
||||
@@ -3117,7 +3155,7 @@ impl CdpConnection {
|
||||
return false;
|
||||
};
|
||||
message.value_mut()["id"] = json!(correlation.frontend_command_id().get());
|
||||
if message.value().get("result").is_some() {
|
||||
if project_runtime_object_ownership && message.value().get("result").is_some() {
|
||||
if let Some(object_group) = result_object_group.as_deref() {
|
||||
self.register_runtime_remote_object_ids_from_value_for_session_owner_with_group(
|
||||
session_id,
|
||||
|
||||
@@ -1560,14 +1560,27 @@ impl CdpConnection {
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
final_url: &Url,
|
||||
) -> PreparedDocumentPageCommitConfiguration {
|
||||
) -> Result<PreparedDocumentPageCommitConfiguration, String> {
|
||||
let idle_override = self.idle_override_for_navigation(session_id, final_url);
|
||||
let load_inputs = self.navigation_load_inputs_for_session_owner(session_id);
|
||||
// The renderer runtime is shared by the BrowserContext, but each Page
|
||||
// target owns its NavigationEngine and may have a different transport
|
||||
// identity. Resolve through that target's engine at the commit
|
||||
// boundary instead of copying whichever runtime another target most
|
||||
// recently registered on the shared renderer context.
|
||||
let browser_resource_runtime = self
|
||||
.ensure_resource_request_client_for_navigation_load_inputs(&load_inputs)?
|
||||
.browser_resource_runtime();
|
||||
let navigator_identity = load_inputs
|
||||
.navigator_identity_override
|
||||
.clone()
|
||||
.or_else(|| self.global_browser_identity_override.clone())
|
||||
.unwrap_or_else(|| self.base_browser_identity.clone());
|
||||
let runtime_isolated_worlds = self
|
||||
.prepare_loaded_navigation_commit_for_session_owner(session_id)
|
||||
.map(|commit_state| commit_state.isolated_worlds)
|
||||
.unwrap_or_default();
|
||||
PreparedDocumentPageCommitConfiguration {
|
||||
Ok(PreparedDocumentPageCommitConfiguration {
|
||||
document_start_scripts: load_inputs.document_start_scripts,
|
||||
runtime_bindings: load_inputs.runtime_bindings,
|
||||
runtime_inspector_session_restore_snapshots: load_inputs
|
||||
@@ -1583,10 +1596,14 @@ impl CdpConnection {
|
||||
emulated_media: load_inputs.emulated_media,
|
||||
idle_override,
|
||||
viewport_surface: load_inputs.viewport_surface,
|
||||
browser_resource_runtime,
|
||||
navigator_identity,
|
||||
network_offline: load_inputs.network_offline,
|
||||
bypass_service_worker: load_inputs.bypass_service_worker,
|
||||
cache_disabled: load_inputs.cache_disabled,
|
||||
blocked_url_patterns: load_inputs.blocked_url_patterns,
|
||||
fetch_subresource_interception: load_inputs.fetch_subresource_interception,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn idle_override_for_navigation(
|
||||
@@ -1594,11 +1611,10 @@ impl CdpConnection {
|
||||
session_id: Option<&str>,
|
||||
final_url: &Url,
|
||||
) -> Option<moli_core::page::EmulatedIdleOverride> {
|
||||
// Commit configuration is assembled after the document navigation has
|
||||
// entered its pending state, where ordinary protocol access to the old
|
||||
// document is intentionally blocked. The outgoing page remains the
|
||||
// owner of frame-host state until commit and is the source Chromium
|
||||
// preserves when a same-site navigation reuses that frame host.
|
||||
// Chromium stores this override on RenderFrameHostImpl's IdleManager,
|
||||
// not on the DevTools target. Preserve it only while a same-site
|
||||
// navigation can retain that frame-host state; a cross-site renderer
|
||||
// replacement must start with the actual idle state.
|
||||
let page = self
|
||||
.runtime_session_owner_slot(session_id)
|
||||
.ok()?
|
||||
@@ -1801,7 +1817,13 @@ impl CdpConnection {
|
||||
.is_some_and(moli_url::is_about_blank)
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_session_owner_should_start_initial_document_navigation(
|
||||
/// Returns whether the materialized initial `about:blank` still needs to
|
||||
/// be replaced by the target URL.
|
||||
///
|
||||
/// This is a structural lifecycle query. It deliberately does not look
|
||||
/// at `waitForDebuggerOnStart`: the explicit debugger-resume path uses it
|
||||
/// after the paused session has been released.
|
||||
pub(crate) fn runtime_session_owner_needs_initial_document_navigation(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
@@ -1816,6 +1838,22 @@ impl CdpConnection {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns whether an ordinary Page/Target command may opportunistically
|
||||
/// start the initial target-URL navigation.
|
||||
///
|
||||
/// A target created with `waitForDebuggerOnStart` must remain on its
|
||||
/// initial document until `Runtime.runIfWaitingForDebugger` has published
|
||||
/// its terminal response. Keeping that admission rule here prevents
|
||||
/// commands such as `Page.enable` and `Page.createIsolatedWorld` from
|
||||
/// racing each other into replacing the paused renderer attachment.
|
||||
pub(crate) fn runtime_session_owner_can_start_initial_document_navigation(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
!self.session_owner_target_has_waiting_for_debugger_session(session_id)
|
||||
&& self.runtime_session_owner_needs_initial_document_navigation(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_session_owner_initial_empty_document_has_replacement_url(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
@@ -2182,7 +2220,7 @@ impl CdpConnection {
|
||||
let configuration = self.prepared_document_commit_configuration_for_session_owner(
|
||||
session_id,
|
||||
navigation.final_url(),
|
||||
);
|
||||
)?;
|
||||
navigation
|
||||
.update_commit_configuration(configuration)
|
||||
.await?;
|
||||
|
||||
@@ -1074,6 +1074,24 @@ impl BrowserContext {
|
||||
.is_none_or(TargetOwnerState::can_install_current_initial_empty_document_page)
|
||||
}
|
||||
|
||||
/// Reports whether one exact Page target is still on its materialized
|
||||
/// initial empty Document and has a non-empty target URL left to load.
|
||||
///
|
||||
/// This target-addressed query is used when the last debugger barrier is
|
||||
/// released by detaching its session, after that session can no longer be
|
||||
/// used as a routing key.
|
||||
pub(crate) fn target_needs_initial_document_navigation(&self, target_id: &str) -> bool {
|
||||
let Some(target) = self.page_target(target_id) else {
|
||||
return false;
|
||||
};
|
||||
let owner_state = &target.owner_state;
|
||||
let Some(initial_url) = owner_state.initial_empty_document_url_if_current() else {
|
||||
return false;
|
||||
};
|
||||
target.target_url() != initial_url
|
||||
&& !owner_state.initial_empty_document_pending_cross_document_navigation()
|
||||
}
|
||||
|
||||
pub(crate) fn loaded_document_renderer_owner_ids_for_diagnostics(&self) -> HashSet<u64> {
|
||||
let mut owner_ids = HashSet::new();
|
||||
if let Some(page) = self.loaded_page() {
|
||||
@@ -1634,6 +1652,15 @@ impl BrowserContext {
|
||||
self.effective_active_browser_identity_override().cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn effective_active_renderer_browser_identity_override_owned(
|
||||
&self,
|
||||
) -> Option<moli_browser_profile::BrowserIdentityProfile> {
|
||||
self.page_targets
|
||||
.active()
|
||||
.and_then(|host| host.effective_renderer_browser_identity_override_owned())
|
||||
.or_else(|| self.default_browser_identity.profile_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn default_browser_identity_override(
|
||||
&self,
|
||||
) -> Option<&moli_browser_profile::BrowserIdentityProfile> {
|
||||
|
||||
@@ -62,6 +62,7 @@ pub(crate) struct DevToolsSessionState {
|
||||
pub(crate) struct DevToolsSessionRegistry {
|
||||
states: BTreeMap<DevToolsSessionKey, DevToolsSessionState>,
|
||||
attached_order: Vec<String>,
|
||||
browser_identity_activation_order: Vec<DevToolsSessionKey>,
|
||||
}
|
||||
|
||||
impl Default for DevToolsSessionRegistry {
|
||||
@@ -72,6 +73,7 @@ impl Default for DevToolsSessionRegistry {
|
||||
DevToolsSessionState::default(),
|
||||
)]),
|
||||
attached_order: Vec::new(),
|
||||
browser_identity_activation_order: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +141,8 @@ impl DevToolsSessionRegistry {
|
||||
if removed.is_some() {
|
||||
self.attached_order
|
||||
.retain(|attached| attached != session_id);
|
||||
self.browser_identity_activation_order
|
||||
.retain(|candidate| candidate != &key);
|
||||
}
|
||||
removed
|
||||
}
|
||||
@@ -147,6 +151,8 @@ impl DevToolsSessionRegistry {
|
||||
self.states
|
||||
.retain(|key, _state| matches!(key, DevToolsSessionKey::Primary));
|
||||
self.attached_order.clear();
|
||||
self.browser_identity_activation_order
|
||||
.retain(|key| matches!(key, DevToolsSessionKey::Primary));
|
||||
}
|
||||
|
||||
pub(crate) fn attached_len(&self) -> usize {
|
||||
@@ -207,19 +213,49 @@ impl DevToolsSessionRegistry {
|
||||
aggregate
|
||||
}
|
||||
|
||||
pub(crate) fn effective_browser_identity_override(
|
||||
pub(crate) fn effective_network_browser_identity_override(
|
||||
&self,
|
||||
) -> Option<moli_browser_profile::BrowserIdentityProfile> {
|
||||
Self::aggregate_browser_identity_overrides(self.states_in_attachment_order().filter_map(
|
||||
|state| {
|
||||
state
|
||||
.emulation_session_state
|
||||
.browser_identity_override
|
||||
.as_ref()
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Resolves the identity exposed by the live renderer Document.
|
||||
///
|
||||
/// Chromium's renderer agents enter the instrumenting-agent list when a
|
||||
/// session first enables a non-empty UA override. Updating that session
|
||||
/// does not move it in the list, so this order intentionally differs from
|
||||
/// the browser-side attachment order used for navigation request headers.
|
||||
pub(crate) fn effective_renderer_browser_identity_override(
|
||||
&self,
|
||||
) -> Option<moli_browser_profile::BrowserIdentityProfile> {
|
||||
Self::aggregate_browser_identity_overrides(
|
||||
self.browser_identity_activation_order
|
||||
.iter()
|
||||
.filter_map(|key| self.states.get(key))
|
||||
.filter_map(|state| {
|
||||
state
|
||||
.emulation_session_state
|
||||
.browser_identity_override
|
||||
.as_ref()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn aggregate_browser_identity_overrides<'a>(
|
||||
contributions: impl Iterator<Item = &'a DevToolsBrowserIdentityOverride>,
|
||||
) -> Option<moli_browser_profile::BrowserIdentityProfile> {
|
||||
let mut identity_base = None;
|
||||
let mut user_agent = None;
|
||||
let mut accept_language = None;
|
||||
let mut navigator_platform = None;
|
||||
for contribution in self.states_in_attachment_order().filter_map(|state| {
|
||||
state
|
||||
.emulation_session_state
|
||||
.browser_identity_override
|
||||
.as_ref()
|
||||
}) {
|
||||
for contribution in contributions {
|
||||
identity_base = Some(&contribution.base);
|
||||
if contribution.user_agent.is_some() {
|
||||
user_agent = Some(contribution);
|
||||
@@ -257,6 +293,12 @@ impl DevToolsSessionRegistry {
|
||||
session_id: Option<&str>,
|
||||
browser_identity_override: Option<DevToolsBrowserIdentityOverride>,
|
||||
) {
|
||||
let key = Self::routed_key(is_attached_session, session_id);
|
||||
if browser_identity_override.is_some()
|
||||
&& !self.browser_identity_activation_order.contains(&key)
|
||||
{
|
||||
self.browser_identity_activation_order.push(key);
|
||||
}
|
||||
let state = self.routed_mut_or_insert(is_attached_session, session_id);
|
||||
state.emulation_session_state.browser_identity_override = browser_identity_override;
|
||||
}
|
||||
@@ -342,6 +384,8 @@ impl DevToolsSessionRegistry {
|
||||
if let Some(state) = self.states.get_mut(&key) {
|
||||
state.emulation_session_state = DevToolsEmulationSessionState::default();
|
||||
}
|
||||
self.browser_identity_activation_order
|
||||
.retain(|candidate| candidate != &key);
|
||||
}
|
||||
|
||||
pub(crate) fn states_mut(&mut self) -> impl Iterator<Item = &mut DevToolsSessionState> {
|
||||
@@ -350,6 +394,8 @@ impl DevToolsSessionRegistry {
|
||||
|
||||
pub(crate) fn reset(&mut self, preserve_attached_sessions: bool) {
|
||||
*self.primary_mut() = DevToolsSessionState::default();
|
||||
self.browser_identity_activation_order
|
||||
.retain(|key| !matches!(key, DevToolsSessionKey::Primary));
|
||||
if !preserve_attached_sessions {
|
||||
self.clear_attached();
|
||||
}
|
||||
@@ -1128,10 +1174,18 @@ mod tests {
|
||||
sessions: &DevToolsSessionRegistry,
|
||||
) -> moli_browser_profile::BrowserIdentityProfile {
|
||||
sessions
|
||||
.effective_browser_identity_override()
|
||||
.effective_network_browser_identity_override()
|
||||
.expect("browser identity contribution should be effective")
|
||||
}
|
||||
|
||||
fn effective_renderer_identity(
|
||||
sessions: &DevToolsSessionRegistry,
|
||||
) -> moli_browser_profile::BrowserIdentityProfile {
|
||||
sessions
|
||||
.effective_renderer_browser_identity_override()
|
||||
.expect("renderer browser identity contribution should be effective")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_owns_primary_and_attached_sessions_in_stable_order() {
|
||||
let mut sessions = DevToolsSessionRegistry::default();
|
||||
@@ -1203,7 +1257,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_identity_override_uses_attachment_order_not_setter_order() {
|
||||
fn browser_identity_uses_distinct_network_and_renderer_agent_order() {
|
||||
let mut sessions = DevToolsSessionRegistry::default();
|
||||
sessions.ensure_attached("SID-later");
|
||||
sessions.set_browser_identity_override(
|
||||
@@ -1217,41 +1271,63 @@ mod tests {
|
||||
identity_override("Moli/Primary-1", None, None),
|
||||
);
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Later-1");
|
||||
|
||||
sessions.set_browser_identity_override(
|
||||
false,
|
||||
None,
|
||||
identity_override("Moli/Primary-2", None, None),
|
||||
);
|
||||
assert_eq!(
|
||||
effective_identity(&sessions).user_agent(),
|
||||
"Moli/Later-1",
|
||||
"a later setter must not outrank a later-attached session"
|
||||
effective_renderer_identity(&sessions).user_agent(),
|
||||
"Moli/Primary-1",
|
||||
"the renderer follows agent activation order, not attachment order"
|
||||
);
|
||||
|
||||
sessions.set_browser_identity_override(true, Some("SID-later"), None);
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Primary-2");
|
||||
|
||||
sessions.set_browser_identity_override(
|
||||
true,
|
||||
Some("SID-later"),
|
||||
identity_override("Moli/Later-2", None, None),
|
||||
);
|
||||
assert_eq!(
|
||||
effective_identity(&sessions).user_agent(),
|
||||
"Moli/Later-2",
|
||||
"the browser-side winner remains the later-attached session"
|
||||
);
|
||||
assert_eq!(
|
||||
effective_renderer_identity(&sessions).user_agent(),
|
||||
"Moli/Primary-1",
|
||||
"updating an enabled renderer agent must not reorder it"
|
||||
);
|
||||
|
||||
sessions.set_browser_identity_override(
|
||||
false,
|
||||
None,
|
||||
identity_override("Moli/Primary-2", None, None),
|
||||
);
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Later-2");
|
||||
assert_eq!(
|
||||
effective_renderer_identity(&sessions).user_agent(),
|
||||
"Moli/Primary-2"
|
||||
);
|
||||
|
||||
sessions.set_browser_identity_override(false, None, None);
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Later-2");
|
||||
assert_eq!(
|
||||
effective_renderer_identity(&sessions).user_agent(),
|
||||
"Moli/Later-2"
|
||||
);
|
||||
|
||||
sessions.set_browser_identity_override(
|
||||
false,
|
||||
None,
|
||||
identity_override("Moli/Primary-3", Some("fr-FR"), Some("PrimaryPlatform")),
|
||||
);
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Later-2");
|
||||
let renderer_identity = effective_renderer_identity(&sessions);
|
||||
assert_eq!(renderer_identity.user_agent(), "Moli/Primary-3");
|
||||
assert_eq!(renderer_identity.accept_language(), "fr-FR");
|
||||
assert_eq!(renderer_identity.navigator_platform(), "PrimaryPlatform");
|
||||
|
||||
sessions.remove_attached("SID-later");
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Primary-2");
|
||||
|
||||
sessions.ensure_attached("SID-later");
|
||||
sessions.set_browser_identity_override(
|
||||
true,
|
||||
Some("SID-later"),
|
||||
identity_override("", Some("fr-FR"), Some("AuxPlatform")),
|
||||
assert_eq!(effective_identity(&sessions).user_agent(), "Moli/Primary-3");
|
||||
assert_eq!(
|
||||
effective_renderer_identity(&sessions).user_agent(),
|
||||
"Moli/Primary-3"
|
||||
);
|
||||
let identity = effective_identity(&sessions);
|
||||
assert_eq!(identity.user_agent(), "Moli/Primary-2");
|
||||
assert_eq!(identity.accept_language(), "fr-FR");
|
||||
assert_eq!(identity.navigator_platform(), "AuxPlatform");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -149,6 +149,14 @@ impl Default for TargetPageState {
|
||||
}
|
||||
|
||||
impl TargetPageState {
|
||||
pub(crate) fn effective_renderer_browser_identity_override_owned(
|
||||
&self,
|
||||
) -> Option<moli_browser_profile::BrowserIdentityProfile> {
|
||||
self.devtools_sessions
|
||||
.effective_renderer_browser_identity_override()
|
||||
.or_else(|| self.network_policy.base_browser_identity_override_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn mutate_devtools_network_session_state<T>(
|
||||
&mut self,
|
||||
is_attached_session: bool,
|
||||
@@ -225,8 +233,9 @@ impl TargetPageState {
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_devtools_emulation_policy(&mut self) {
|
||||
self.network_policy.devtools_browser_identity_override =
|
||||
self.devtools_sessions.effective_browser_identity_override();
|
||||
self.network_policy.devtools_browser_identity_override = self
|
||||
.devtools_sessions
|
||||
.effective_network_browser_identity_override();
|
||||
self.locale_override = self
|
||||
.devtools_sessions
|
||||
.effective_locale_override()
|
||||
@@ -649,6 +658,12 @@ impl Default for TargetNetworkPolicyState {
|
||||
}
|
||||
|
||||
impl TargetNetworkPolicyState {
|
||||
pub(crate) fn base_browser_identity_override_owned(
|
||||
&self,
|
||||
) -> Option<moli_browser_profile::BrowserIdentityProfile> {
|
||||
self.base_browser_identity.profile_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn cache_disabled(&self) -> bool {
|
||||
self.base_cache_disabled || self.devtools_cache_disabled
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl CdpConnection {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
self.auto_attach_owner_sessions.remove(&key);
|
||||
self.auto_attach_owner_sessions.shift_remove(&key);
|
||||
}
|
||||
self.sync_auto_attach_flags_from_owners();
|
||||
}
|
||||
@@ -88,7 +88,47 @@ impl CdpConnection {
|
||||
pub(crate) fn clear_auto_attach_owner(&mut self, session_id: Option<&str>) {
|
||||
self.clear_service_worker_auto_attach_related_owner(session_id);
|
||||
let key = session_id.map(str::to_owned);
|
||||
self.auto_attach_owner_sessions.remove(&key);
|
||||
self.auto_attach_owner_sessions.shift_remove(&key);
|
||||
self.sync_auto_attach_flags_from_owners();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testing::TestContext;
|
||||
|
||||
#[test]
|
||||
fn auto_attach_owners_keep_protocol_attachment_order() {
|
||||
let mut ctx = TestContext::new();
|
||||
let page_filter = CdpTargetFilter::default_auto_attach();
|
||||
ctx.conn
|
||||
.set_auto_attach_owner(None, true, false, page_filter.clone());
|
||||
ctx.conn
|
||||
.set_auto_attach_owner(Some("SID-browser"), true, true, page_filter.clone());
|
||||
|
||||
assert_eq!(
|
||||
ctx.conn.auto_attach_owner_sessions_for_target_type("page"),
|
||||
vec![None, Some("SID-browser".to_owned())]
|
||||
);
|
||||
|
||||
// Updating an existing owner's policy is not a new attachment and
|
||||
// must not move it behind newer owners.
|
||||
ctx.conn
|
||||
.set_auto_attach_owner(None, true, true, page_filter.clone());
|
||||
assert_eq!(
|
||||
ctx.conn.auto_attach_owner_sessions_for_target_type("page"),
|
||||
vec![None, Some("SID-browser".to_owned())]
|
||||
);
|
||||
|
||||
// Removing and attaching again is a new protocol attachment.
|
||||
ctx.conn
|
||||
.set_auto_attach_owner(None, false, false, page_filter.clone());
|
||||
ctx.conn
|
||||
.set_auto_attach_owner(None, true, false, page_filter);
|
||||
assert_eq!(
|
||||
ctx.conn.auto_attach_owner_sessions_for_target_type("page"),
|
||||
vec![Some("SID-browser".to_owned()), None]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +482,11 @@ impl TargetControlPlane {
|
||||
.target_has_waiting_for_debugger_session(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn release_waiting_for_debugger_session(&mut self, session_id: &str) -> bool {
|
||||
self.sessions
|
||||
.release_waiting_for_debugger_session(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn auto_attached_sessions_for_owner(
|
||||
&self,
|
||||
owner_session_id: Option<&str>,
|
||||
@@ -714,7 +719,7 @@ mod tests {
|
||||
assert_eq!(detached.target_id(), "TID-page");
|
||||
assert_eq!(detached.route(), Some(&route));
|
||||
assert!(!detached.auto_attached());
|
||||
assert!(!detached.waiting_for_debugger());
|
||||
assert!(!detached.was_waiting_for_debugger());
|
||||
|
||||
let events = plan.into_background_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
|
||||
@@ -87,8 +87,7 @@ impl DetachedTargetSession {
|
||||
self.auto_attached
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn waiting_for_debugger(&self) -> bool {
|
||||
pub(crate) fn was_waiting_for_debugger(&self) -> bool {
|
||||
self.waiting_for_debugger
|
||||
}
|
||||
}
|
||||
@@ -367,6 +366,21 @@ impl TargetSessionRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
/// Releases the debugger-on-start barrier contributed by one attached
|
||||
/// session.
|
||||
///
|
||||
/// V8 keeps one barrier per inspector session and resumes the target only
|
||||
/// after every waiting session has run `Runtime.runIfWaitingForDebugger`
|
||||
/// (or detached). Keep that per-session transition in the attachment
|
||||
/// registry instead of treating `waitingForDebugger` as immutable event
|
||||
/// metadata.
|
||||
pub(crate) fn release_waiting_for_debugger_session(&mut self, session_id: &str) -> bool {
|
||||
let Some(session) = self.attached_sessions.get_mut(session_id) else {
|
||||
return false;
|
||||
};
|
||||
std::mem::take(&mut session.waiting_for_debugger)
|
||||
}
|
||||
|
||||
pub(crate) fn attached_session_owner_session_id(&self, session_id: &str) -> Option<&str> {
|
||||
self.attached_sessions
|
||||
.get(session_id)?
|
||||
@@ -773,6 +787,29 @@ mod tests {
|
||||
assert!(!registry.target_has_waiting_for_debugger_session("TID-unattached"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_session_registry_releases_each_debugger_barrier_exactly_once() {
|
||||
let mut registry = TargetSessionRegistry::default();
|
||||
for session_id in ["SID-first", "SID-second"] {
|
||||
registry.commit_attached_session(PreparedAttachSession::new(
|
||||
session_id.to_owned(),
|
||||
Some("SID-owner"),
|
||||
"TID-page",
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
assert!(registry.target_has_waiting_for_debugger_session("TID-page"));
|
||||
assert!(registry.release_waiting_for_debugger_session("SID-first"));
|
||||
assert!(registry.target_has_waiting_for_debugger_session("TID-page"));
|
||||
assert!(!registry.release_waiting_for_debugger_session("SID-first"));
|
||||
assert!(registry.release_waiting_for_debugger_session("SID-second"));
|
||||
assert!(!registry.target_has_waiting_for_debugger_session("TID-page"));
|
||||
assert!(!registry.release_waiting_for_debugger_session("SID-missing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_session_registry_clears_attached_session_and_auto_attached_indexes() {
|
||||
let mut registry = TargetSessionRegistry::default();
|
||||
|
||||
@@ -545,10 +545,20 @@ impl CdpConnection {
|
||||
reason,
|
||||
parent_session_id,
|
||||
);
|
||||
let released_debugger_barrier = plan
|
||||
.detached_sessions()
|
||||
.iter()
|
||||
.any(|session| session.target_id() == target_id && session.was_waiting_for_debugger());
|
||||
self.clear_detached_target_session_owner_state(session_id);
|
||||
if let Some(attached_state_delta_plan) = attached_state_delta_plan {
|
||||
plan.extend(attached_state_delta_plan);
|
||||
}
|
||||
if released_debugger_barrier && !self.target_has_waiting_for_debugger_session(target_id) {
|
||||
crate::domains::target::schedule_initial_document_target_url_navigation_after_debugger_barrier_release_for_target(
|
||||
self,
|
||||
target_id,
|
||||
);
|
||||
}
|
||||
plan
|
||||
}
|
||||
|
||||
@@ -1076,6 +1086,16 @@ impl CdpConnection {
|
||||
.target_has_waiting_for_debugger_session(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn release_waiting_for_debugger_session(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
session_id.is_some_and(|session_id| {
|
||||
self.target_control
|
||||
.release_waiting_for_debugger_session(session_id)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn auto_attached_sessions_for_owner(
|
||||
&self,
|
||||
owner_session_id: Option<&str>,
|
||||
|
||||
@@ -48,10 +48,12 @@ async fn commit_navigation_outcome_for_session_test(
|
||||
match outcome {
|
||||
NavigationLoadOutcome::ResponseCommitReady(navigation) => {
|
||||
let navigation = *navigation;
|
||||
let configuration = conn.prepared_document_commit_configuration_for_session_owner(
|
||||
session_id,
|
||||
navigation.final_url(),
|
||||
);
|
||||
let configuration = conn
|
||||
.prepared_document_commit_configuration_for_session_owner(
|
||||
session_id,
|
||||
navigation.final_url(),
|
||||
)
|
||||
.expect("test navigation commit configuration should resolve");
|
||||
navigation
|
||||
.update_commit_configuration(configuration)
|
||||
.await
|
||||
|
||||
@@ -111,10 +111,21 @@ impl PreparedProtocolOutputs {
|
||||
.append_to_output_sink(&mut prepared);
|
||||
}
|
||||
RendererProtocolObservation::RuntimeInspector(batch) => {
|
||||
let source_batches = vec![batch.clone()];
|
||||
let mut batches = conn.route_current_renderer_inspector_output_for_session_owner(
|
||||
session_id,
|
||||
vec![batch.clone()],
|
||||
source_batches.clone(),
|
||||
);
|
||||
if batches.is_empty() {
|
||||
crate::domains::runtime::RuntimePreparedOutputs::
|
||||
from_retired_renderer_runtime_inspector_session_responses(
|
||||
conn,
|
||||
session_id,
|
||||
&source_batches,
|
||||
)
|
||||
.append_to_output_sink(&mut prepared);
|
||||
return prepared;
|
||||
}
|
||||
for batch in &mut batches {
|
||||
let Some(attachment) = conn
|
||||
.target_page_protocol_attachment_identity_for_renderer_inspector_route(
|
||||
|
||||
@@ -72,6 +72,7 @@ struct PendingEmulationPageCommand {
|
||||
struct CompletedEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget,
|
||||
operation: PendingEmulationPageOperation,
|
||||
dispatched_attachment_id: Option<moli_core::page::RendererAgentAttachmentId>,
|
||||
completed: Result<CompletedPageCommand, String>,
|
||||
}
|
||||
|
||||
@@ -80,10 +81,7 @@ enum PendingEmulationPageTarget {
|
||||
SessionOwner {
|
||||
owner_scope: CommandOwnerScope,
|
||||
},
|
||||
BrowserContextActive {
|
||||
browser_context_id: String,
|
||||
},
|
||||
BrowserContextBackground {
|
||||
BrowserContextTarget {
|
||||
browser_context_id: String,
|
||||
target_id: String,
|
||||
},
|
||||
@@ -108,6 +106,27 @@ enum PendingEmulationPageOperation {
|
||||
RuntimeProtocolMessage,
|
||||
}
|
||||
|
||||
impl PendingEmulationPageOperation {
|
||||
fn has_authoritative_replay_state(&self) -> bool {
|
||||
match self {
|
||||
Self::SetExtraHttpHeaders
|
||||
| Self::SetLocaleOverride
|
||||
| Self::SetNetworkConditions
|
||||
| Self::SetCpuThrottlingRate
|
||||
| Self::SetTimezoneOverride
|
||||
| Self::SetEmulatedMedia
|
||||
| Self::SetViewportSurface
|
||||
| Self::SetUserAgentLoader
|
||||
| Self::ReplaceBrowserResourceRuntime
|
||||
| Self::RuntimeProtocolMessage => true,
|
||||
// Chromium owns this state on RenderFrameHostImpl::IdleManager.
|
||||
// It can survive same-site RFH reuse, but it is not target policy
|
||||
// that may be replayed after an arbitrary attachment replacement.
|
||||
Self::SetIdleOverride => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PendingEmulationCommandDispatch {
|
||||
pub(crate) async fn wait(self) -> CompletedEmulationCommandDispatch {
|
||||
let completed = match self.pending {
|
||||
@@ -120,6 +139,7 @@ impl PendingEmulationCommandDispatch {
|
||||
pending,
|
||||
runtime_response_rx,
|
||||
} = pending;
|
||||
let dispatched_attachment_id = pending.renderer_agent_attachment_id();
|
||||
let completed_page = pending.wait().await.map_err(|error| error.to_string());
|
||||
if completed_page.is_ok()
|
||||
&& let Some(response_rx) = runtime_response_rx
|
||||
@@ -129,6 +149,7 @@ impl PendingEmulationCommandDispatch {
|
||||
completed.push(CompletedEmulationPageCommand {
|
||||
target,
|
||||
operation,
|
||||
dispatched_attachment_id,
|
||||
completed: completed_page,
|
||||
});
|
||||
}
|
||||
@@ -296,7 +317,7 @@ fn start_cpu_throttling_rate_command(
|
||||
));
|
||||
}
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let Some(page) = loaded_page_mut_for_session(conn, cmd.session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else {
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
};
|
||||
match page.start_set_cpu_throttling_rate(params.rate) {
|
||||
@@ -361,7 +382,7 @@ fn start_script_execution_disabled_command(
|
||||
"BrowserContextNotLoaded",
|
||||
));
|
||||
}
|
||||
let Some(attachment_id) = loaded_page_mut_for_session(conn, cmd.session_id)
|
||||
let Some(attachment_id) = loaded_page_mut_for_target_configuration(conn, cmd.session_id)
|
||||
.and_then(|page| page.renderer_agent_attachment_id())
|
||||
else {
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
@@ -374,7 +395,7 @@ fn start_script_execution_disabled_command(
|
||||
if cmd.id.is_none()
|
||||
|| response_delivery == moli_page_types::RendererInspectorResponseDelivery::CommandReply
|
||||
{
|
||||
let page = loaded_page_mut_for_session(conn, cmd.session_id)
|
||||
let page = loaded_page_mut_for_target_configuration(conn, cmd.session_id)
|
||||
.expect("the captured Emulation Page must remain loaded synchronously");
|
||||
let pending = page.start_set_script_execution_disabled_from_io(params.value);
|
||||
return EmulationCommandTaskStep::Pending(PendingEmulationCommandDispatch {
|
||||
@@ -408,7 +429,7 @@ fn start_script_execution_disabled_command(
|
||||
response_rx.is_none(),
|
||||
"Emulation session output must not allocate a command-reply receiver",
|
||||
);
|
||||
let pending = loaded_page_mut_for_session(conn, cmd.session_id)
|
||||
let pending = loaded_page_mut_for_target_configuration(conn, cmd.session_id)
|
||||
.filter(|page| page.renderer_agent_attachment_id() == Some(attachment_id))
|
||||
.ok_or_else(|| "Emulation renderer attachment changed before IO dispatch".to_owned())
|
||||
.and_then(|page| {
|
||||
@@ -537,7 +558,7 @@ fn start_update_idle_override_command(
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
}
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let Some(page) = loaded_page_mut_for_session(conn, cmd.session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else {
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
};
|
||||
match page.start_set_idle_override(idle_override) {
|
||||
@@ -582,7 +603,7 @@ fn start_timezone_override_command(
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::error(code, message));
|
||||
}
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let Some(page) = loaded_page_mut_for_session(conn, cmd.session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else {
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
};
|
||||
match page.start_set_timezone_override(timezone_override.as_deref()) {
|
||||
@@ -712,7 +733,7 @@ fn start_emulated_media_command(
|
||||
}
|
||||
} else {
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let Some(page) = loaded_page_mut_for_session(conn, cmd.session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else {
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
};
|
||||
match page.start_set_emulated_media(&page_overrides) {
|
||||
@@ -868,7 +889,7 @@ fn start_clear_device_metrics_override_command(
|
||||
}
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let runtime_call_id = conn.next_internal_runtime_command_id();
|
||||
let Some(page) = loaded_page_mut_for_session(conn, cmd.session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else {
|
||||
return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({})));
|
||||
};
|
||||
let pending_viewport = match page.start_set_viewport_surface(None) {
|
||||
@@ -939,7 +960,7 @@ fn start_devtools_set_viewport_command(
|
||||
}
|
||||
let owner_scope = CommandOwnerScope::capture(conn, session_id);
|
||||
let runtime_call_id = conn.next_internal_runtime_command_id();
|
||||
let Some(page) = loaded_page_mut_for_session(conn, session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, session_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let session_id = session_id.map(str::to_owned);
|
||||
@@ -1225,7 +1246,7 @@ async fn execute_geolocation_surface_updates_for_routes(
|
||||
) -> Result<DevToolsCommandResult, DevToolsError> {
|
||||
let mut pending = Vec::new();
|
||||
for route in routes {
|
||||
let target = pending_emulation_target_for_route(&route)?;
|
||||
let target = pending_emulation_target_for_route(conn, &route)?;
|
||||
let result = {
|
||||
let mut route_scope = conn.scoped_none_session_owner_route_override(route);
|
||||
start_surface_override_for_route(route_scope.conn_mut(), target)
|
||||
@@ -1251,7 +1272,7 @@ fn start_geolocation_override_for_current_route(
|
||||
"BrowserContextNotLoaded".to_owned(),
|
||||
));
|
||||
}
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
start_surface_override_for_route(conn, target)
|
||||
.map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))
|
||||
}
|
||||
@@ -1353,18 +1374,19 @@ fn start_network_conditions_update_for_current_route(
|
||||
conn: &mut CdpConnection,
|
||||
route: &CdpSessionRoute,
|
||||
) -> Result<Vec<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let effective_offline = match &target {
|
||||
PendingEmulationPageTarget::BrowserContextActive { browser_context_id } => conn
|
||||
.browser_context_by_id(browser_context_id)
|
||||
.is_some_and(|browser_context| browser_context.effective_active_network_offline()),
|
||||
PendingEmulationPageTarget::BrowserContextBackground {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => conn
|
||||
.browser_context_by_id(browser_context_id)
|
||||
.is_some_and(|browser_context| {
|
||||
browser_context.effective_parked_network_offline(target_id)
|
||||
if browser_context.is_active_target(target_id) {
|
||||
browser_context.effective_active_network_offline()
|
||||
} else {
|
||||
browser_context.effective_parked_network_offline(target_id)
|
||||
}
|
||||
}),
|
||||
PendingEmulationPageTarget::SessionOwner { .. } => false,
|
||||
};
|
||||
@@ -1399,7 +1421,7 @@ fn start_extra_headers_for_current_route(
|
||||
route: &CdpSessionRoute,
|
||||
headers: Vec<(String, String)>,
|
||||
) -> Result<Vec<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let pending = conn
|
||||
.start_set_target_extra_http_headers_for_session_owner(None, headers)
|
||||
.map_err(devtools_emulation_owner_error)?;
|
||||
@@ -1431,17 +1453,20 @@ fn start_extra_headers_update_for_route(
|
||||
conn: &mut CdpConnection,
|
||||
route: &CdpSessionRoute,
|
||||
) -> Result<Vec<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let headers = match &target {
|
||||
PendingEmulationPageTarget::BrowserContextActive { browser_context_id } => conn
|
||||
.browser_context_by_id(browser_context_id)
|
||||
.map(|browser_context| browser_context.effective_extra_headers()),
|
||||
PendingEmulationPageTarget::BrowserContextBackground {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => conn
|
||||
.browser_context_by_id(browser_context_id)
|
||||
.map(|browser_context| browser_context.effective_parked_extra_headers(target_id)),
|
||||
.map(|browser_context| {
|
||||
if browser_context.is_active_target(target_id) {
|
||||
browser_context.effective_extra_headers()
|
||||
} else {
|
||||
browser_context.effective_parked_extra_headers(target_id)
|
||||
}
|
||||
}),
|
||||
PendingEmulationPageTarget::SessionOwner { .. } => None,
|
||||
};
|
||||
let Some(headers) = headers else {
|
||||
@@ -1466,23 +1491,21 @@ fn loaded_page_mut_for_pending_emulation_target<'a>(
|
||||
target: &PendingEmulationPageTarget,
|
||||
) -> Option<&'a moli_core::page::Page> {
|
||||
match target {
|
||||
PendingEmulationPageTarget::BrowserContextActive { browser_context_id } => conn
|
||||
.browser_context_by_id_mut(browser_context_id)
|
||||
.and_then(|browser_context| {
|
||||
browser_context.active_target.runtime_slot.loaded_page_mut()
|
||||
})
|
||||
.map(|page| &*page),
|
||||
PendingEmulationPageTarget::BrowserContextBackground {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => conn
|
||||
.browser_context_by_id_mut(browser_context_id)
|
||||
.and_then(|browser_context| browser_context.background_target_mut(target_id))
|
||||
.and_then(|browser_context| browser_context.page_target_mut(target_id))
|
||||
.and_then(|target| target.loaded_page_mut())
|
||||
.map(|page| &*page),
|
||||
PendingEmulationPageTarget::SessionOwner { owner_scope } => {
|
||||
loaded_page_mut_for_session(conn, owner_scope.session_id()).map(|page| &*page)
|
||||
}
|
||||
PendingEmulationPageTarget::SessionOwner { owner_scope } => conn
|
||||
.loaded_page_mut_for_target_configuration_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.ok()
|
||||
.map(|page| &*page),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1651,7 +1674,7 @@ fn start_user_agent_override_for_current_route(
|
||||
route: &CdpSessionRoute,
|
||||
user_agent: Option<String>,
|
||||
) -> Result<Option<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let pending = conn
|
||||
.start_set_base_user_agent_override_for_session_owner(None, user_agent)
|
||||
.map_err(devtools_emulation_owner_error)?;
|
||||
@@ -1670,12 +1693,12 @@ fn start_user_agent_loader_update_for_current_route(
|
||||
conn: &mut CdpConnection,
|
||||
route: &CdpSessionRoute,
|
||||
) -> Result<Option<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let load_inputs = conn.navigation_load_inputs_for_session_owner(None);
|
||||
let resource_runtime = conn
|
||||
.build_registered_browser_resource_runtime_for_navigation_load_inputs(&load_inputs)
|
||||
.map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?;
|
||||
let Some(page) = loaded_page_mut_for_session(conn, None) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, None) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let pending = page
|
||||
@@ -1784,11 +1807,11 @@ fn start_locale_update_for_current_route(
|
||||
if let Some(identity_update) = start_user_agent_loader_update_for_current_route(conn, route)? {
|
||||
pending.push(identity_update);
|
||||
}
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let Some(locale_override) = locale_override_for_session(conn, None) else {
|
||||
return Ok(pending);
|
||||
};
|
||||
let Some(page) = loaded_page_mut_for_session(conn, None) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, None) else {
|
||||
return Ok(pending);
|
||||
};
|
||||
pending.extend(
|
||||
@@ -1892,9 +1915,9 @@ fn start_timezone_update_for_current_route(
|
||||
conn: &mut CdpConnection,
|
||||
route: &CdpSessionRoute,
|
||||
) -> Result<Option<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let target = pending_emulation_target_for_route(route)?;
|
||||
let target = pending_emulation_target_for_route(conn, route)?;
|
||||
let load_inputs = conn.navigation_load_inputs_for_session_owner(None);
|
||||
let Some(page) = loaded_page_mut_for_session(conn, None) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, None) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let pending = page
|
||||
@@ -1909,18 +1932,32 @@ fn start_timezone_update_for_current_route(
|
||||
}
|
||||
|
||||
fn pending_emulation_target_for_route(
|
||||
conn: &CdpConnection,
|
||||
route: &CdpSessionRoute,
|
||||
) -> Result<PendingEmulationPageTarget, DevToolsError> {
|
||||
match route {
|
||||
CdpSessionRoute::ActiveTarget {
|
||||
browser_context_id, ..
|
||||
} => Ok(PendingEmulationPageTarget::BrowserContextActive {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
}),
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => {
|
||||
let target_id = target_id
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
conn.browser_context_by_id(browser_context_id)
|
||||
.and_then(BrowserContext::active_target_id_owned)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget")
|
||||
})?;
|
||||
Ok(PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id,
|
||||
})
|
||||
}
|
||||
CdpSessionRoute::PageTargetHost {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => Ok(PendingEmulationPageTarget::BrowserContextBackground {
|
||||
} => Ok(PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id: target_id.clone(),
|
||||
}),
|
||||
@@ -2296,15 +2333,18 @@ fn start_browser_context_default_device_metrics_page_commands(
|
||||
runtime_call_ids: &mut Vec<u64>,
|
||||
) -> Result<Vec<PendingEmulationPageCommand>, DevToolsError> {
|
||||
let browser_context_id = browser_context.id.clone();
|
||||
let active_target_id = browser_context.active_target_id_owned();
|
||||
let mut pending = Vec::new();
|
||||
let viewport_surface = Some(metrics.viewport_surface().to_page_viewport_surface());
|
||||
if let Some(active_target) = browser_context.page_targets.active_mut()
|
||||
if let Some(active_target_id) = active_target_id
|
||||
&& let Some(active_target) = browser_context.page_targets.active_mut()
|
||||
&& active_target.emulated_device_metrics.is_none()
|
||||
&& let Some(page) = active_target.runtime_slot.loaded_page_mut()
|
||||
{
|
||||
pending.push(PendingEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget::BrowserContextActive {
|
||||
target: PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id: active_target_id.clone(),
|
||||
},
|
||||
operation: PendingEmulationPageOperation::SetViewportSurface,
|
||||
pending: page
|
||||
@@ -2323,8 +2363,9 @@ fn start_browser_context_default_device_metrics_page_commands(
|
||||
)
|
||||
.map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?;
|
||||
pending.push(PendingEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget::BrowserContextActive {
|
||||
target: PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id: active_target_id,
|
||||
},
|
||||
operation: PendingEmulationPageOperation::RuntimeProtocolMessage,
|
||||
pending: pending_runtime,
|
||||
@@ -2350,7 +2391,7 @@ fn start_browser_context_default_device_metrics_page_commands(
|
||||
continue;
|
||||
};
|
||||
pending.push(PendingEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget::BrowserContextBackground {
|
||||
target: PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id: target_id.clone(),
|
||||
},
|
||||
@@ -2371,7 +2412,7 @@ fn start_browser_context_default_device_metrics_page_commands(
|
||||
)
|
||||
.map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?;
|
||||
pending.push(PendingEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget::BrowserContextBackground {
|
||||
target: PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id,
|
||||
},
|
||||
@@ -2394,16 +2435,30 @@ fn complete_pending_devtools_emulation_command(
|
||||
));
|
||||
};
|
||||
for completed_page in completed_pages {
|
||||
let completion = completed_page
|
||||
.completed
|
||||
let CompletedEmulationPageCommand {
|
||||
target,
|
||||
operation,
|
||||
dispatched_attachment_id,
|
||||
completed,
|
||||
} = completed_page;
|
||||
let completion = match completed {
|
||||
Ok(completion) => completion,
|
||||
Err(_)
|
||||
if pending_emulation_page_configuration_will_be_replayed(
|
||||
conn,
|
||||
&target,
|
||||
&operation,
|
||||
dispatched_attachment_id,
|
||||
) =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(DevToolsError::new(DevToolsErrorKind::Internal, error));
|
||||
}
|
||||
};
|
||||
finish_pending_emulation_page_command(conn, operation, target, completion)
|
||||
.map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?;
|
||||
finish_pending_emulation_page_command(
|
||||
conn,
|
||||
completed_page.operation,
|
||||
completed_page.target,
|
||||
completion,
|
||||
)
|
||||
.map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?;
|
||||
}
|
||||
Ok(DevToolsCommandResult::Empty)
|
||||
}
|
||||
@@ -2494,16 +2549,27 @@ pub(crate) fn complete_pending_emulation_command(
|
||||
}
|
||||
};
|
||||
for completed_page in completed_pages {
|
||||
let completion = match completed_page.completed {
|
||||
let CompletedEmulationPageCommand {
|
||||
target,
|
||||
operation,
|
||||
dispatched_attachment_id,
|
||||
completed,
|
||||
} = completed_page;
|
||||
let completion = match completed {
|
||||
Ok(completion) => completion,
|
||||
Err(_)
|
||||
if pending_emulation_page_configuration_will_be_replayed(
|
||||
conn,
|
||||
&target,
|
||||
&operation,
|
||||
dispatched_attachment_id,
|
||||
) =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Err(error) => return CommandOutputPlan::error(-32000, error),
|
||||
};
|
||||
let result = finish_pending_emulation_page_command(
|
||||
conn,
|
||||
completed_page.operation,
|
||||
completed_page.target,
|
||||
completion,
|
||||
);
|
||||
let result = finish_pending_emulation_page_command(conn, operation, target, completion);
|
||||
if let Err(error) = result {
|
||||
return CommandOutputPlan::error(-32000, error);
|
||||
}
|
||||
@@ -2511,11 +2577,70 @@ pub(crate) fn complete_pending_emulation_command(
|
||||
CommandOutputPlan::result(json!({}))
|
||||
}
|
||||
|
||||
fn loaded_page_mut_for_session<'a>(
|
||||
fn pending_emulation_page_configuration_will_be_replayed(
|
||||
conn: &CdpConnection,
|
||||
target: &PendingEmulationPageTarget,
|
||||
operation: &PendingEmulationPageOperation,
|
||||
dispatched_attachment_id: Option<moli_core::page::RendererAgentAttachmentId>,
|
||||
) -> bool {
|
||||
if !operation.has_authoritative_replay_state() {
|
||||
return false;
|
||||
}
|
||||
let Some(dispatched_attachment_id) = dispatched_attachment_id else {
|
||||
return false;
|
||||
};
|
||||
let current_attachment_id = match target {
|
||||
PendingEmulationPageTarget::SessionOwner { owner_scope } => {
|
||||
let Some((browser_context_id, target_id)) = conn.target_owner_identity_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let Some(browser_context) = conn.browser_context_by_id(&browser_context_id) else {
|
||||
return false;
|
||||
};
|
||||
let target = match target_id.as_deref() {
|
||||
Some(target_id) => browser_context.page_target(target_id),
|
||||
None => browser_context.page_targets.active(),
|
||||
};
|
||||
let Some(target) = target else {
|
||||
return false;
|
||||
};
|
||||
target
|
||||
.loaded_page()
|
||||
.and_then(moli_core::page::Page::renderer_agent_attachment_id)
|
||||
}
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => {
|
||||
let Some(target) = conn
|
||||
.browser_context_by_id(browser_context_id)
|
||||
.and_then(|browser_context| browser_context.page_target(target_id))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
target
|
||||
.loaded_page()
|
||||
.and_then(moli_core::page::Page::renderer_agent_attachment_id)
|
||||
}
|
||||
};
|
||||
|
||||
// Target-configuration operations store their authoritative state before
|
||||
// dispatch. If that exact target has moved away from the dispatched Page,
|
||||
// commit configuration either replayed it into the replacement or will do
|
||||
// so when the in-flight navigation commits. A cancellation from that
|
||||
// retired renderer is therefore not a protocol failure.
|
||||
current_attachment_id != Some(dispatched_attachment_id)
|
||||
}
|
||||
|
||||
fn loaded_page_mut_for_target_configuration<'a>(
|
||||
conn: &'a mut CdpConnection,
|
||||
session_id: Option<&str>,
|
||||
) -> Option<&'a mut moli_core::page::Page> {
|
||||
conn.loaded_page_mut_for_protocol_access(session_id).ok()
|
||||
conn.loaded_page_mut_for_target_configuration(session_id)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_emulated_media_for_detached_session_async(
|
||||
@@ -2540,7 +2665,7 @@ pub(crate) async fn clear_emulated_media_for_detached_session_async(
|
||||
}
|
||||
|
||||
let page_overrides: moli_core::page::EmulatedMediaOverrides = (&overrides).into();
|
||||
let Some(page) = loaded_page_mut_for_session(conn, Some(session_id)) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, Some(session_id)) else {
|
||||
return Ok(());
|
||||
};
|
||||
page.set_emulated_media_async(&page_overrides)
|
||||
@@ -2586,11 +2711,15 @@ fn start_context_emulated_media_page_commands(
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let browser_context_id = browser_context.id.clone();
|
||||
let active_target_id = browser_context.active_target_id_owned();
|
||||
let mut pending = Vec::new();
|
||||
if let Some(page) = browser_context.active_target.runtime_slot.loaded_page_mut() {
|
||||
if let Some(active_target_id) = active_target_id
|
||||
&& let Some(page) = browser_context.active_target.runtime_slot.loaded_page_mut()
|
||||
{
|
||||
pending.push(PendingEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget::BrowserContextActive {
|
||||
target: PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id: active_target_id,
|
||||
},
|
||||
operation: PendingEmulationPageOperation::SetEmulatedMedia,
|
||||
pending: page
|
||||
@@ -2605,7 +2734,7 @@ fn start_context_emulated_media_page_commands(
|
||||
continue;
|
||||
};
|
||||
pending.push(PendingEmulationPageCommand {
|
||||
target: PendingEmulationPageTarget::BrowserContextBackground {
|
||||
target: PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id,
|
||||
},
|
||||
@@ -2627,7 +2756,7 @@ fn start_session_locale_override_page_commands(
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, session_id);
|
||||
let Some(page) = loaded_page_mut_for_session(conn, session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, session_id) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
start_locale_override_page_command(
|
||||
@@ -2647,11 +2776,15 @@ fn start_context_locale_override_page_commands(
|
||||
.chain(conn.inactive_browser_contexts.iter_mut())
|
||||
{
|
||||
let browser_context_id = browser_context.id.clone();
|
||||
let active_target_id = browser_context.active_target_id_owned();
|
||||
let active_locale = browser_context.effective_active_locale_override_owned();
|
||||
if let Some(page) = browser_context.active_target.runtime_slot.loaded_page_mut() {
|
||||
if let Some(active_target_id) = active_target_id
|
||||
&& let Some(page) = browser_context.active_target.runtime_slot.loaded_page_mut()
|
||||
{
|
||||
pending.extend(start_locale_override_page_command(
|
||||
PendingEmulationPageTarget::BrowserContextActive {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id: active_target_id,
|
||||
},
|
||||
page,
|
||||
active_locale.as_deref(),
|
||||
@@ -2670,7 +2803,7 @@ fn start_context_locale_override_page_commands(
|
||||
continue;
|
||||
};
|
||||
pending.extend(start_locale_override_page_command(
|
||||
PendingEmulationPageTarget::BrowserContextBackground {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: browser_context_id.clone(),
|
||||
target_id,
|
||||
},
|
||||
@@ -2697,11 +2830,17 @@ fn start_geolocation_surface_override_page_commands(
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let browser_context_id = browser_context.id.clone();
|
||||
let Some(target_id) = browser_context.active_target_id_owned() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(page) = browser_context.active_target.runtime_slot.loaded_page_mut() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
start_surface_override_page_command(
|
||||
PendingEmulationPageTarget::BrowserContextActive { browser_context_id },
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
},
|
||||
page,
|
||||
script,
|
||||
runtime_call_id,
|
||||
@@ -2735,7 +2874,7 @@ fn start_session_surface_override_page_command(
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, session_id);
|
||||
let runtime_call_id = conn.next_internal_runtime_command_id();
|
||||
let Some(page) = loaded_page_mut_for_session(conn, session_id) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, session_id) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
start_surface_override_page_command(
|
||||
@@ -2752,20 +2891,18 @@ fn start_surface_override_for_route(
|
||||
target: PendingEmulationPageTarget,
|
||||
) -> Result<Vec<PendingEmulationPageCommand>, String> {
|
||||
let script = match &target {
|
||||
PendingEmulationPageTarget::BrowserContextActive { browser_context_id } => {
|
||||
let Some(browser_context) = conn.browser_context_by_id(browser_context_id) else {
|
||||
return Err("BrowserContextNotLoaded".to_owned());
|
||||
};
|
||||
browser_context.generated_surface_override_script_for_active_target()
|
||||
}
|
||||
PendingEmulationPageTarget::BrowserContextBackground {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => {
|
||||
let Some(browser_context) = conn.browser_context_by_id(browser_context_id) else {
|
||||
return Err("BrowserContextNotLoaded".to_owned());
|
||||
};
|
||||
browser_context.generated_surface_override_script_for_parked_target(target_id)
|
||||
if browser_context.is_active_target(target_id) {
|
||||
browser_context.generated_surface_override_script_for_active_target()
|
||||
} else {
|
||||
browser_context.generated_surface_override_script_for_parked_target(target_id)
|
||||
}
|
||||
}
|
||||
PendingEmulationPageTarget::SessionOwner { owner_scope } => {
|
||||
return start_session_surface_override_page_command(conn, owner_scope.session_id());
|
||||
@@ -2775,7 +2912,7 @@ fn start_surface_override_for_route(
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let runtime_call_id = conn.next_internal_runtime_command_id();
|
||||
let Some(page) = loaded_page_mut_for_session(conn, None) else {
|
||||
let Some(page) = loaded_page_mut_for_target_configuration(conn, None) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
start_surface_override_page_command(target, page, script, runtime_call_id)
|
||||
@@ -2844,28 +2981,20 @@ fn finish_pending_emulation_page_command(
|
||||
);
|
||||
}
|
||||
let page = conn
|
||||
.loaded_page_mut_for_interruptible_protocol_access_for_route(
|
||||
.loaded_page_mut_for_target_configuration_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.ok();
|
||||
finish_emulation_page_operation_on_current_attachment(page, operation, completion)
|
||||
}
|
||||
PendingEmulationPageTarget::BrowserContextActive { browser_context_id } => {
|
||||
let page = conn
|
||||
.browser_context_by_id_mut(&browser_context_id)
|
||||
.and_then(|browser_context| {
|
||||
browser_context.active_target.runtime_slot.loaded_page_mut()
|
||||
});
|
||||
finish_emulation_page_operation_on_current_attachment(page, operation, completion)
|
||||
}
|
||||
PendingEmulationPageTarget::BrowserContextBackground {
|
||||
PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
} => {
|
||||
let page = conn
|
||||
.browser_context_by_id_mut(&browser_context_id)
|
||||
.and_then(|browser_context| browser_context.background_target_mut(&target_id))
|
||||
.and_then(|browser_context| browser_context.page_target_mut(&target_id))
|
||||
.and_then(|target| target.loaded_page_mut());
|
||||
finish_emulation_page_operation_on_current_attachment(page, operation, completion)
|
||||
}
|
||||
@@ -2886,10 +3015,9 @@ fn finish_emulation_page_operation_on_current_attachment(
|
||||
|
||||
// The renderer command has already settled successfully. A cross-Document
|
||||
// navigation may replace its Page before the protocol actor decodes that
|
||||
// frozen completion; do not turn that success into NoDocumentLoaded or
|
||||
// apply the old PageState snapshot to the replacement attachment. The
|
||||
// authoritative emulation state was stored before dispatch and is replayed
|
||||
// while the replacement Page is installed.
|
||||
// frozen completion; decode the terminal reply, but never apply the old
|
||||
// PageState snapshot to the replacement attachment. Whether state carries
|
||||
// across the navigation is decided separately at the commit boundary.
|
||||
let output = match operation {
|
||||
PendingEmulationPageOperation::RuntimeProtocolMessage => {
|
||||
completion.into_runtime_protocol_message_command_turn()
|
||||
|
||||
@@ -361,6 +361,209 @@ async fn emulated_media_can_complete_through_pending_command_dispatch() {
|
||||
assert_eq!(media.color_scheme.as_deref(), Some("dark"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn pending_emulation_completion_follows_the_exact_target_across_activation_and_navigation() {
|
||||
let mut ctx = TestContext::new();
|
||||
load_session_page_for_pending_emulation_test(&mut ctx).await;
|
||||
let dispatched_attachment_id = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|browser_context| browser_context.page_target("TID-1"))
|
||||
.and_then(PageTargetHost::loaded_page)
|
||||
.and_then(moli_core::page::Page::renderer_agent_attachment_id)
|
||||
.expect("the original target should have a renderer attachment");
|
||||
let target = super::PendingEmulationPageTarget::BrowserContextTarget {
|
||||
browser_context_id: "BID-1".to_owned(),
|
||||
target_id: "TID-1".to_owned(),
|
||||
};
|
||||
|
||||
let browser_context = ctx.conn.browser_context.as_mut().unwrap();
|
||||
assert!(
|
||||
browser_context.insert_page_target_host(PageTargetHost::with_url(
|
||||
"TID-2".to_owned(),
|
||||
None,
|
||||
"about:blank".to_owned(),
|
||||
))
|
||||
);
|
||||
browser_context.set_active_target_id("TID-2");
|
||||
assert!(
|
||||
!super::pending_emulation_page_configuration_will_be_replayed(
|
||||
&ctx.conn,
|
||||
&target,
|
||||
&super::PendingEmulationPageOperation::SetTimezoneOverride,
|
||||
Some(dispatched_attachment_id),
|
||||
),
|
||||
"changing foreground selection must not make an error from the same Page look stale"
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 9_104,
|
||||
"sessionId": "SID-1",
|
||||
"method": "Page.navigate",
|
||||
"params": { "url": "data:text/html,<body>replacement</body>" }
|
||||
}))
|
||||
.await;
|
||||
assert!(ctx.take_response_by_id(9_104)["result"]["loaderId"].is_string());
|
||||
assert!(
|
||||
super::pending_emulation_page_configuration_will_be_replayed(
|
||||
&ctx.conn,
|
||||
&target,
|
||||
&super::PendingEmulationPageOperation::SetTimezoneOverride,
|
||||
Some(dispatched_attachment_id),
|
||||
),
|
||||
"only replacement of the exact target attachment may retire its renderer error"
|
||||
);
|
||||
assert!(
|
||||
!super::pending_emulation_page_configuration_will_be_replayed(
|
||||
&ctx.conn,
|
||||
&target,
|
||||
&super::PendingEmulationPageOperation::SetIdleOverride,
|
||||
Some(dispatched_attachment_id),
|
||||
),
|
||||
"frame-host idle state must not use the target-policy replay path",
|
||||
);
|
||||
|
||||
let result = super::complete_pending_devtools_emulation_command(
|
||||
&mut ctx.conn,
|
||||
super::CompletedEmulationCommandDispatch {
|
||||
command_id: None,
|
||||
session_id: Some("SID-1".to_owned()),
|
||||
completed: super::CompletedEmulationRendererDispatch::Pages(vec![
|
||||
super::CompletedEmulationPageCommand {
|
||||
target,
|
||||
operation: super::PendingEmulationPageOperation::SetTimezoneOverride,
|
||||
dispatched_attachment_id: Some(dispatched_attachment_id),
|
||||
completed: Err("renderer attachment retired".to_owned()),
|
||||
},
|
||||
]),
|
||||
},
|
||||
)
|
||||
.expect("the stored target policy should be replayed into the replacement document");
|
||||
assert!(matches!(result, DevToolsCommandResult::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn pending_idle_override_response_does_not_replay_into_replacement_page() {
|
||||
let mut ctx = TestContext::new();
|
||||
load_session_page_for_pending_emulation_test(&mut ctx).await;
|
||||
|
||||
let raw = json!({
|
||||
"id": 9_105,
|
||||
"sessionId": "SID-1",
|
||||
"method": "Emulation.setIdleOverride",
|
||||
"params": { "isUserActive": false, "isScreenUnlocked": false }
|
||||
})
|
||||
.to_string();
|
||||
let CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) else {
|
||||
panic!("the loaded Page should receive the idle override command");
|
||||
};
|
||||
let completed = pending.wait().await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 9_106,
|
||||
"sessionId": "SID-1",
|
||||
"method": "Page.navigate",
|
||||
"params": { "url": "data:text/html,<body>replacement</body>" }
|
||||
}))
|
||||
.await;
|
||||
assert!(ctx.take_response_by_id(9_106)["result"]["loaderId"].is_string());
|
||||
|
||||
let CdpCommandTaskStep::Complete(outcome) =
|
||||
ctx.conn.complete_pending_command_dispatch(completed).await
|
||||
else {
|
||||
panic!("the retired idle override should settle in one protocol phase");
|
||||
};
|
||||
assert!(outcome.into_parts().0.iter().any(|message| {
|
||||
message["id"] == json!(9_105)
|
||||
&& message["sessionId"] == json!("SID-1")
|
||||
&& message["result"] == json!({})
|
||||
}));
|
||||
let page = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|context| context.page_target("TID-1"))
|
||||
.and_then(PageTargetHost::loaded_page)
|
||||
.expect("replacement Page");
|
||||
assert_eq!(
|
||||
page.idle_override(),
|
||||
None,
|
||||
"a settled command on the retired frame host must not become target-level policy",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn admitted_idle_override_is_visible_to_concurrent_same_site_navigation() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
axum::serve(
|
||||
listener,
|
||||
Router::new().route(
|
||||
"/",
|
||||
get(|| async { "<!doctype html><title>idle navigation</title>" }),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
let mut ctx = TestContext::new();
|
||||
load_session_page_for_pending_emulation_test_at_url(
|
||||
&mut ctx,
|
||||
&format!("http://{address}/?initial"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let raw = json!({
|
||||
"id": 9_107,
|
||||
"sessionId": "SID-1",
|
||||
"method": "Emulation.setIdleOverride",
|
||||
"params": { "isUserActive": false, "isScreenUnlocked": false }
|
||||
})
|
||||
.to_string();
|
||||
let CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) else {
|
||||
panic!("the loaded Page should receive the idle override command");
|
||||
};
|
||||
let completed = pending.wait().await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 9_108,
|
||||
"sessionId": "SID-1",
|
||||
"method": "Page.navigate",
|
||||
"params": { "url": format!("http://{address}/?replacement") }
|
||||
}))
|
||||
.await;
|
||||
assert!(ctx.take_response_by_id(9_108)["result"]["loaderId"].is_string());
|
||||
|
||||
let CdpCommandTaskStep::Complete(outcome) =
|
||||
ctx.conn.complete_pending_command_dispatch(completed).await
|
||||
else {
|
||||
panic!("the retired idle override should settle in one protocol phase");
|
||||
};
|
||||
assert!(outcome.into_parts().0.iter().any(|message| {
|
||||
message["id"] == json!(9_107)
|
||||
&& message["sessionId"] == json!("SID-1")
|
||||
&& message["result"] == json!({})
|
||||
}));
|
||||
let page = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|context| context.page_target("TID-1"))
|
||||
.and_then(PageTargetHost::loaded_page)
|
||||
.expect("replacement Page");
|
||||
assert_eq!(
|
||||
page.idle_override(),
|
||||
Some(moli_core::page::EmulatedIdleOverride {
|
||||
is_user_active: false,
|
||||
is_screen_unlocked: false,
|
||||
}),
|
||||
"same-site commit must read admitted state from the outgoing document handle",
|
||||
);
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn idle_override_updates_idle_detector_and_clear_restores_actual_state() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
@@ -463,7 +666,6 @@ async fn idle_override_updates_idle_detector_and_clear_restores_actual_state() {
|
||||
&& message["sessionId"] == json!("SID-1")
|
||||
&& message["result"] == json!({})
|
||||
}));
|
||||
|
||||
let page = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
@@ -501,14 +703,18 @@ async fn idle_override_updates_idle_detector_and_clear_restores_actual_state() {
|
||||
}));
|
||||
|
||||
ctx.conn
|
||||
.start_document_navigation_for_session_owner(Some("SID-1"), "LID-idle-same-site".to_owned())
|
||||
.expect("same-site navigation should enter the pending state");
|
||||
.start_document_navigation_for_session_owner(
|
||||
Some("SID-1"),
|
||||
"LID-idle-cross-document".to_owned(),
|
||||
)
|
||||
.expect("cross-Document navigation should enter the pending state");
|
||||
let configuration = ctx
|
||||
.conn
|
||||
.prepared_document_commit_configuration_for_session_owner(
|
||||
Some("SID-1"),
|
||||
&url::Url::parse("http://127.0.0.1:65530/same-site-different-origin").unwrap(),
|
||||
);
|
||||
)
|
||||
.expect("commit configuration should resolve the target resource runtime");
|
||||
assert_eq!(
|
||||
configuration.idle_override,
|
||||
Some(moli_core::page::EmulatedIdleOverride {
|
||||
@@ -516,6 +722,17 @@ async fn idle_override_updates_idle_detector_and_clear_restores_actual_state() {
|
||||
is_screen_unlocked: false,
|
||||
})
|
||||
);
|
||||
let cross_site_configuration = ctx
|
||||
.conn
|
||||
.prepared_document_commit_configuration_for_session_owner(
|
||||
Some("SID-1"),
|
||||
&url::Url::parse("http://idle-override-cross-site.test/").unwrap(),
|
||||
)
|
||||
.expect("cross-site commit configuration should resolve the target resource runtime");
|
||||
assert_eq!(
|
||||
cross_site_configuration.idle_override, None,
|
||||
"a cross-site renderer replacement must not inherit frame-host idle state",
|
||||
);
|
||||
server.abort();
|
||||
}
|
||||
|
||||
@@ -909,6 +1126,18 @@ async fn multi_session_browser_identity_uses_attachment_order_and_field_contribu
|
||||
.user_agent_override(),
|
||||
Some("Moli/Aux-1")
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.expect("browser context")
|
||||
.active_page_state()
|
||||
.effective_renderer_browser_identity_override_owned()
|
||||
.expect("renderer identity")
|
||||
.user_agent(),
|
||||
"Moli/Primary-2",
|
||||
"renderer agents use first-enable order rather than browser attachment order"
|
||||
);
|
||||
|
||||
expect_session_command_result(
|
||||
&mut ctx,
|
||||
|
||||
@@ -248,7 +248,9 @@ impl PendingFetchCommandDispatch {
|
||||
| CdpSessionRoute::DedicatedWorkerTarget { .. }
|
||||
| CdpSessionRoute::ServiceWorkerTarget { .. },
|
||||
) => None,
|
||||
None if session_id.is_none() => conn.none_session_owner_route_override(),
|
||||
None if session_id.is_none() => CommandOwnerScope::capture(conn, None)
|
||||
.session_owner_route()
|
||||
.cloned(),
|
||||
None => None,
|
||||
};
|
||||
Self {
|
||||
|
||||
@@ -1892,10 +1892,10 @@ fn continue_streaming_document_response_in_background(
|
||||
prepared_document,
|
||||
} = pending;
|
||||
let session_id = navigation.navigate_session_id.clone();
|
||||
let none_session_owner_route = session_id
|
||||
.is_none()
|
||||
.then(|| conn.none_session_owner_route_override())
|
||||
.flatten();
|
||||
let none_session_owner_route =
|
||||
crate::conn::CommandOwnerScope::capture(conn, session_id.as_deref())
|
||||
.session_owner_route()
|
||||
.cloned();
|
||||
let cancellation = response.cancellation_handle();
|
||||
if response_code.is_none()
|
||||
&& response_headers.is_empty()
|
||||
|
||||
@@ -182,6 +182,44 @@ async fn deferred_fetch_command_keeps_its_exact_page_for_implicit_work() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn deferred_sessionless_fetch_command_freezes_the_active_page_at_admission() {
|
||||
let mut ctx = TestContext::new();
|
||||
let mut browser_context = BrowserContext::new("BID-sessionless".to_owned());
|
||||
browser_context.set_active_target_id("TID-original".to_owned());
|
||||
browser_context.insert_page_target_host(PageTargetHost::with_url(
|
||||
"TID-next".to_owned(),
|
||||
None,
|
||||
"https://example.test/next".to_owned(),
|
||||
));
|
||||
ctx.conn.browser_context = Some(browser_context);
|
||||
|
||||
let completed = PendingFetchCommandDispatch::new(
|
||||
&ctx.conn,
|
||||
Some(71),
|
||||
None,
|
||||
PendingFetchCommandKind::GetResponseBody,
|
||||
PendingFetchCommandOperation::Ready,
|
||||
)
|
||||
.wait()
|
||||
.await;
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context")
|
||||
.set_active_target_id("TID-next");
|
||||
let mut scope = completed.owner_scope.enter(&mut ctx.conn);
|
||||
|
||||
assert_eq!(
|
||||
scope.conn_mut().target_owner_identity_for_session(None),
|
||||
Some((
|
||||
"BID-sessionless".to_owned(),
|
||||
Some("TID-original".to_owned())
|
||||
)),
|
||||
"a deferred root Fetch command must not follow a later foreground selection"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn continue_request_registers_correlation_before_renderer_completion() {
|
||||
let mut ctx = context_with_loaded_fetch_page().await;
|
||||
|
||||
@@ -5025,7 +5025,8 @@ async fn stop_loading_aborts_paused_response_stage_runtime_xhr_subresource() {
|
||||
.cloned()
|
||||
.expect("network loadingFailed event");
|
||||
assert_eq!(failed["params"]["type"], "XHR");
|
||||
assert_eq!(failed["params"]["errorText"], "Navigation stopped");
|
||||
assert_eq!(failed["params"]["errorText"], "net::ERR_ABORTED");
|
||||
assert_eq!(failed["params"]["canceled"], true);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 792,
|
||||
@@ -5437,7 +5438,8 @@ async fn stop_loading_aborts_paused_runtime_xhr_auth_subresource() {
|
||||
.cloned()
|
||||
.expect("network loadingFailed event");
|
||||
assert_eq!(failed["params"]["type"], "XHR");
|
||||
assert_eq!(failed["params"]["errorText"], "Navigation stopped");
|
||||
assert_eq!(failed["params"]["errorText"], "net::ERR_ABORTED");
|
||||
assert_eq!(failed["params"]["canceled"], true);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 7937,
|
||||
|
||||
@@ -7687,7 +7687,8 @@ async fn stop_loading_aborts_paused_runtime_fetch_subresource() {
|
||||
.cloned()
|
||||
.expect("network loadingFailed event");
|
||||
assert_eq!(failed["params"]["type"], "Fetch");
|
||||
assert_eq!(failed["params"]["errorText"], "Navigation stopped");
|
||||
assert_eq!(failed["params"]["errorText"], "net::ERR_ABORTED");
|
||||
assert_eq!(failed["params"]["canceled"], true);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 785,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::conn::{CdpConnection, Cmd};
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope};
|
||||
use crate::devtools_runtime::{
|
||||
DevToolsAddNetworkDataCollectorCommand, DevToolsBrowserContextId, DevToolsCommand,
|
||||
DevToolsCommandResult, DevToolsError, DevToolsErrorKind,
|
||||
@@ -122,28 +122,43 @@ use settings::clear_browser_cache_command_output_plan;
|
||||
|
||||
pub(crate) struct PendingNetworkCommandDispatch {
|
||||
command_id: Option<u64>,
|
||||
session_id: Option<String>,
|
||||
owner_scope: CommandOwnerScope,
|
||||
kind: PendingNetworkCommandKind,
|
||||
pending: PendingNetworkCommandWork,
|
||||
}
|
||||
|
||||
pub(crate) struct CompletedNetworkCommandDispatch {
|
||||
command_id: Option<u64>,
|
||||
session_id: Option<String>,
|
||||
owner_scope: CommandOwnerScope,
|
||||
kind: PendingNetworkCommandKind,
|
||||
completed: CompletedNetworkCommandWork,
|
||||
}
|
||||
|
||||
enum PendingNetworkCommandWork {
|
||||
Page(moli_core::page::PendingPageCommand),
|
||||
Page {
|
||||
attachment_id: Option<moli_core::page::RendererAgentAttachmentId>,
|
||||
pending: moli_core::page::PendingPageCommand,
|
||||
},
|
||||
Resource(Box<moli_core::page::RendererPreparedNetworkResourceLoad>),
|
||||
}
|
||||
|
||||
enum CompletedNetworkCommandWork {
|
||||
Page(Result<Box<moli_core::page::CompletedPageCommand>, String>),
|
||||
Page {
|
||||
attachment_id: Option<moli_core::page::RendererAgentAttachmentId>,
|
||||
completed: Result<Box<moli_core::page::CompletedPageCommand>, String>,
|
||||
},
|
||||
Resource(moli_core::page::RendererNetworkResourceLoadOutcome),
|
||||
}
|
||||
|
||||
impl PendingNetworkCommandWork {
|
||||
fn page(pending: moli_core::page::PendingPageCommand) -> Self {
|
||||
Self::Page {
|
||||
attachment_id: pending.renderer_agent_attachment_id(),
|
||||
pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum NetworkCommandTaskStep {
|
||||
Pending(PendingNetworkCommandDispatch),
|
||||
Complete(CommandOutputPlan),
|
||||
@@ -164,25 +179,29 @@ enum PendingNetworkCommandKind {
|
||||
|
||||
impl PendingNetworkCommandDispatch {
|
||||
pub(crate) fn session_id(&self) -> Option<&str> {
|
||||
self.session_id.as_deref()
|
||||
self.owner_scope.session_id()
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(self) -> CompletedNetworkCommandDispatch {
|
||||
let completed = match self.pending {
|
||||
PendingNetworkCommandWork::Page(pending) => CompletedNetworkCommandWork::Page(
|
||||
pending
|
||||
PendingNetworkCommandWork::Page {
|
||||
attachment_id,
|
||||
pending,
|
||||
} => CompletedNetworkCommandWork::Page {
|
||||
attachment_id,
|
||||
completed: pending
|
||||
.wait()
|
||||
.await
|
||||
.map(Box::new)
|
||||
.map_err(|error| error.to_string()),
|
||||
),
|
||||
},
|
||||
PendingNetworkCommandWork::Resource(pending) => {
|
||||
CompletedNetworkCommandWork::Resource((*pending).execute().await)
|
||||
}
|
||||
};
|
||||
CompletedNetworkCommandDispatch {
|
||||
command_id: self.command_id,
|
||||
session_id: self.session_id,
|
||||
owner_scope: self.owner_scope,
|
||||
kind: self.kind,
|
||||
completed,
|
||||
}
|
||||
@@ -195,7 +214,7 @@ impl CompletedNetworkCommandDispatch {
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self) -> Option<&str> {
|
||||
self.session_id.as_deref()
|
||||
self.owner_scope.session_id()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,17 +468,22 @@ fn validate_top_level_target_ids(
|
||||
}
|
||||
|
||||
fn pending_network_page_command_step(
|
||||
conn: &mut CdpConnection,
|
||||
command_id: Option<u64>,
|
||||
session_id: Option<&str>,
|
||||
kind: PendingNetworkCommandKind,
|
||||
result: Result<Option<moli_core::page::PendingPageCommand>, String>,
|
||||
start: impl FnOnce(
|
||||
&mut CdpConnection,
|
||||
) -> Result<Option<moli_core::page::PendingPageCommand>, String>,
|
||||
) -> NetworkCommandTaskStep {
|
||||
let owner_scope = CommandOwnerScope::capture(conn, session_id);
|
||||
let result = start(conn);
|
||||
match result {
|
||||
Ok(Some(pending)) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch {
|
||||
command_id,
|
||||
session_id: session_id.map(str::to_owned),
|
||||
owner_scope,
|
||||
kind,
|
||||
pending: PendingNetworkCommandWork::Page(pending),
|
||||
pending: PendingNetworkCommandWork::page(pending),
|
||||
}),
|
||||
Ok(None) => NetworkCommandTaskStep::Complete(CommandOutputPlan::success()),
|
||||
Err(message) if message == "BrowserContextNotLoaded" => NetworkCommandTaskStep::Complete(
|
||||
@@ -491,12 +515,13 @@ fn start_set_network_domain_enabled_command(
|
||||
} else {
|
||||
PendingNetworkCommandKind::Disable
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
match conn.start_replay_effective_network_request_policy_for_session_owner(cmd.session_id) {
|
||||
Ok(Some(pending)) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch {
|
||||
command_id: cmd.id,
|
||||
session_id: cmd.session_id.map(str::to_owned),
|
||||
owner_scope,
|
||||
kind,
|
||||
pending: PendingNetworkCommandWork::Page(pending),
|
||||
pending: PendingNetworkCommandWork::page(pending),
|
||||
}),
|
||||
Ok(None) => NetworkCommandTaskStep::Complete(if enabled {
|
||||
settings::enabled_command_output_plan(conn, cmd.session_id)
|
||||
@@ -516,10 +541,11 @@ fn start_set_extra_http_headers_command(
|
||||
Err(plan) => return NetworkCommandTaskStep::Complete(plan),
|
||||
};
|
||||
pending_network_page_command_step(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
PendingNetworkCommandKind::SetExtraHttpHeaders,
|
||||
conn.start_set_extra_http_headers_for_session_owner(cmd.session_id, headers),
|
||||
|conn| conn.start_set_extra_http_headers_for_session_owner(cmd.session_id, headers),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -532,10 +558,11 @@ fn start_set_cache_disabled_command(
|
||||
Err(plan) => return NetworkCommandTaskStep::Complete(plan),
|
||||
};
|
||||
pending_network_page_command_step(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
PendingNetworkCommandKind::SetCacheDisabled,
|
||||
conn.start_set_cache_disabled_for_session_owner(cmd.session_id, cache_disabled),
|
||||
|conn| conn.start_set_cache_disabled_for_session_owner(cmd.session_id, cache_disabled),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -548,10 +575,11 @@ fn start_set_blocked_urls_command(
|
||||
Err(plan) => return NetworkCommandTaskStep::Complete(plan),
|
||||
};
|
||||
pending_network_page_command_step(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
PendingNetworkCommandKind::SetBlockedUrls,
|
||||
conn.start_set_blocked_url_patterns_for_session_owner(cmd.session_id, patterns),
|
||||
|conn| conn.start_set_blocked_url_patterns_for_session_owner(cmd.session_id, patterns),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -564,10 +592,11 @@ fn start_set_bypass_service_worker_command(
|
||||
Err(plan) => return NetworkCommandTaskStep::Complete(plan),
|
||||
};
|
||||
pending_network_page_command_step(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
PendingNetworkCommandKind::SetBypassServiceWorker,
|
||||
conn.start_set_bypass_service_worker_for_session_owner(cmd.session_id, bypass),
|
||||
|conn| conn.start_set_bypass_service_worker_for_session_owner(cmd.session_id, bypass),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -580,17 +609,20 @@ fn start_emulate_network_conditions_command(
|
||||
Err(plan) => return NetworkCommandTaskStep::Complete(plan),
|
||||
};
|
||||
pending_network_page_command_step(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
PendingNetworkCommandKind::EmulateNetworkConditions,
|
||||
conn.start_set_emulated_network_conditions_for_session_owner(
|
||||
cmd.session_id,
|
||||
conditions.offline,
|
||||
conditions.latency,
|
||||
conditions.download_throughput,
|
||||
conditions.upload_throughput,
|
||||
conditions.connection_type,
|
||||
),
|
||||
|conn| {
|
||||
conn.start_set_emulated_network_conditions_for_session_owner(
|
||||
cmd.session_id,
|
||||
conditions.offline,
|
||||
conditions.latency,
|
||||
conditions.download_throughput,
|
||||
conditions.upload_throughput,
|
||||
conditions.connection_type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -604,13 +636,16 @@ fn start_set_user_agent_override_command(
|
||||
Err(plan) => return NetworkCommandTaskStep::Complete(plan),
|
||||
};
|
||||
pending_network_page_command_step(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
PendingNetworkCommandKind::SetUserAgentOverride,
|
||||
conn.start_set_devtools_browser_identity_override_for_session_owner(
|
||||
cmd.session_id,
|
||||
browser_identity,
|
||||
),
|
||||
|conn| {
|
||||
conn.start_set_devtools_browser_identity_override_for_session_owner(
|
||||
cmd.session_id,
|
||||
browser_identity,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -670,6 +705,7 @@ pub(crate) fn complete_pending_network_command(
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum NetworkPageCommandFinish {
|
||||
RequestPolicy,
|
||||
ExtraHttpHeaders,
|
||||
BlockedUrls,
|
||||
BypassServiceWorker,
|
||||
@@ -681,31 +717,43 @@ fn complete_network_policy_refresh(
|
||||
completed: CompletedNetworkCommandDispatch,
|
||||
enabled: bool,
|
||||
) -> CommandOutputPlan {
|
||||
let owner_scope = completed.owner_scope.clone();
|
||||
let session_id = owner_scope.session_id().map(str::to_owned);
|
||||
let completion = match completed.completed {
|
||||
CompletedNetworkCommandWork::Page(Ok(completion)) => *completion,
|
||||
CompletedNetworkCommandWork::Page(Err(error)) => {
|
||||
CompletedNetworkCommandWork::Page {
|
||||
completed: Ok(completion),
|
||||
..
|
||||
} => *completion,
|
||||
CompletedNetworkCommandWork::Page {
|
||||
attachment_id,
|
||||
completed: Err(error),
|
||||
} => {
|
||||
if network_page_configuration_will_be_replayed(conn, &owner_scope, attachment_id) {
|
||||
return if enabled {
|
||||
settings::enabled_command_output_plan(conn, session_id.as_deref())
|
||||
} else {
|
||||
CommandOutputPlan::success()
|
||||
};
|
||||
}
|
||||
return CommandOutputPlan::error(-32000, error);
|
||||
}
|
||||
CompletedNetworkCommandWork::Resource(_) => {
|
||||
return CommandOutputPlan::error(-32000, "InvalidNetworkCommandCompletion");
|
||||
}
|
||||
};
|
||||
let page = match conn.loaded_page_mut_for_protocol_access(completed.session_id.as_deref()) {
|
||||
Ok(page) => page,
|
||||
Err(message) if message == "NoDocumentLoaded" => {
|
||||
return if enabled {
|
||||
settings::enabled_command_output_plan(conn, completed.session_id.as_deref())
|
||||
} else {
|
||||
CommandOutputPlan::success()
|
||||
};
|
||||
}
|
||||
Err(message) => return CommandOutputPlan::error(-32000, message),
|
||||
};
|
||||
if let Err(error) = page.finish_set_network_request_policy(completion) {
|
||||
return CommandOutputPlan::error(-32000, error.to_string());
|
||||
if let Err(error) = finish_network_page_operation_on_current_attachment(
|
||||
conn.loaded_page_mut_for_target_configuration_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.ok(),
|
||||
NetworkPageCommandFinish::RequestPolicy,
|
||||
completion,
|
||||
) {
|
||||
return CommandOutputPlan::error(-32000, error);
|
||||
}
|
||||
if enabled {
|
||||
settings::enabled_command_output_plan(conn, completed.session_id.as_deref())
|
||||
settings::enabled_command_output_plan(conn, owner_scope.session_id())
|
||||
} else {
|
||||
CommandOutputPlan::success()
|
||||
}
|
||||
@@ -716,53 +764,103 @@ fn complete_unit_page_network_command(
|
||||
completed: CompletedNetworkCommandDispatch,
|
||||
finish: NetworkPageCommandFinish,
|
||||
) -> CommandOutputPlan {
|
||||
let owner_scope = completed.owner_scope.clone();
|
||||
let completion = match completed.completed {
|
||||
CompletedNetworkCommandWork::Page(Ok(completion)) => *completion,
|
||||
CompletedNetworkCommandWork::Page(Err(error)) => {
|
||||
CompletedNetworkCommandWork::Page {
|
||||
completed: Ok(completion),
|
||||
..
|
||||
} => *completion,
|
||||
CompletedNetworkCommandWork::Page {
|
||||
attachment_id,
|
||||
completed: Err(error),
|
||||
} => {
|
||||
if network_page_configuration_will_be_replayed(conn, &owner_scope, attachment_id) {
|
||||
return CommandOutputPlan::success();
|
||||
}
|
||||
return CommandOutputPlan::error(-32000, error);
|
||||
}
|
||||
CompletedNetworkCommandWork::Resource(_) => {
|
||||
return CommandOutputPlan::error(-32000, "InvalidNetworkCommandCompletion");
|
||||
}
|
||||
};
|
||||
let page = match conn.loaded_page_mut_for_protocol_access(completed.session_id.as_deref()) {
|
||||
Ok(page) => page,
|
||||
Err(message) if message == "NoDocumentLoaded" => {
|
||||
return CommandOutputPlan::success();
|
||||
}
|
||||
Err(message) => return CommandOutputPlan::error(-32000, message),
|
||||
};
|
||||
let result = match finish {
|
||||
NetworkPageCommandFinish::ExtraHttpHeaders => {
|
||||
page.finish_set_extra_http_headers(completion)
|
||||
}
|
||||
NetworkPageCommandFinish::BlockedUrls => page.finish_set_blocked_url_patterns(completion),
|
||||
NetworkPageCommandFinish::BypassServiceWorker => {
|
||||
page.finish_set_bypass_service_worker(completion)
|
||||
}
|
||||
NetworkPageCommandFinish::NetworkOffline => page.finish_set_network_offline(completion),
|
||||
};
|
||||
match result {
|
||||
match finish_network_page_operation_on_current_attachment(
|
||||
conn.loaded_page_mut_for_target_configuration_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.ok(),
|
||||
finish,
|
||||
completion,
|
||||
) {
|
||||
Ok(()) => CommandOutputPlan::success(),
|
||||
Err(error) => CommandOutputPlan::error(-32000, error.to_string()),
|
||||
Err(error) => CommandOutputPlan::error(-32000, error),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_network_page_operation_on_current_attachment(
|
||||
page: Option<&mut moli_core::page::Page>,
|
||||
finish: NetworkPageCommandFinish,
|
||||
completion: moli_core::page::CompletedPageCommand,
|
||||
) -> Result<(), String> {
|
||||
let completion_attachment = completion.renderer_agent_attachment_id();
|
||||
if let Some(page) = page
|
||||
&& page.renderer_agent_attachment_id() == completion_attachment
|
||||
{
|
||||
let result = match finish {
|
||||
NetworkPageCommandFinish::RequestPolicy => {
|
||||
page.finish_set_network_request_policy(completion)
|
||||
}
|
||||
NetworkPageCommandFinish::ExtraHttpHeaders => {
|
||||
page.finish_set_extra_http_headers(completion)
|
||||
}
|
||||
NetworkPageCommandFinish::BlockedUrls => {
|
||||
page.finish_set_blocked_url_patterns(completion)
|
||||
}
|
||||
NetworkPageCommandFinish::BypassServiceWorker => {
|
||||
page.finish_set_bypass_service_worker(completion)
|
||||
}
|
||||
NetworkPageCommandFinish::NetworkOffline => page.finish_set_network_offline(completion),
|
||||
};
|
||||
return result.map_err(|error| error.to_string());
|
||||
}
|
||||
|
||||
// Target/session policy was committed before renderer dispatch. If a
|
||||
// navigation installs another attachment before this frozen unit reply is
|
||||
// decoded, consume the old turn without applying its PageState snapshot
|
||||
// to the replacement. Prepared-document commit configuration carries the
|
||||
// authoritative policy into that replacement.
|
||||
completion
|
||||
.into_unit_page_command_turn()
|
||||
.map(drop)
|
||||
.map_err(|error| format!("stale Network command returned an unexpected reply: {error}"))
|
||||
}
|
||||
|
||||
fn complete_rebuild_loader_network_command(
|
||||
conn: &mut CdpConnection,
|
||||
completed: CompletedNetworkCommandDispatch,
|
||||
) -> CommandOutputPlan {
|
||||
let owner_scope = completed.owner_scope.clone();
|
||||
let completion = match completed.completed {
|
||||
CompletedNetworkCommandWork::Page(Ok(completion)) => *completion,
|
||||
CompletedNetworkCommandWork::Page(Err(error)) => {
|
||||
CompletedNetworkCommandWork::Page {
|
||||
completed: Ok(completion),
|
||||
..
|
||||
} => *completion,
|
||||
CompletedNetworkCommandWork::Page {
|
||||
attachment_id,
|
||||
completed: Err(error),
|
||||
} => {
|
||||
if network_page_configuration_will_be_replayed(conn, &owner_scope, attachment_id) {
|
||||
return CommandOutputPlan::success();
|
||||
}
|
||||
return CommandOutputPlan::error(-32000, error);
|
||||
}
|
||||
CompletedNetworkCommandWork::Resource(_) => {
|
||||
return CommandOutputPlan::error(-32000, "InvalidNetworkCommandCompletion");
|
||||
}
|
||||
};
|
||||
match conn.finish_rebuild_resource_runtime_for_session_owner(
|
||||
completed.session_id.as_deref(),
|
||||
match conn.finish_rebuild_resource_runtime_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
completion,
|
||||
) {
|
||||
Ok(()) => CommandOutputPlan::success(),
|
||||
@@ -770,6 +868,30 @@ fn complete_rebuild_loader_network_command(
|
||||
}
|
||||
}
|
||||
|
||||
fn network_page_configuration_will_be_replayed(
|
||||
conn: &mut CdpConnection,
|
||||
owner_scope: &CommandOwnerScope,
|
||||
dispatched_attachment: Option<moli_core::page::RendererAgentAttachmentId>,
|
||||
) -> bool {
|
||||
let Some(dispatched_attachment) = dispatched_attachment else {
|
||||
return false;
|
||||
};
|
||||
let owner_still_exists = conn
|
||||
.target_owner_identity_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.is_some();
|
||||
let current_attachment = conn
|
||||
.loaded_page_mut_for_target_configuration_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|page| page.renderer_agent_attachment_id());
|
||||
owner_still_exists && current_attachment != Some(dispatched_attachment)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod status_text_tests {
|
||||
use super::http_status_text;
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde_json::{Map, Value, json};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
conn::{CapturedBody, CdpConnection, Cmd},
|
||||
conn::{CapturedBody, CdpConnection, Cmd, CommandOwnerScope},
|
||||
domains::command_output::CommandOutputPlan,
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ pub(super) fn start_load_network_resource_command(
|
||||
"Parameter frameId must be provided for frame targets",
|
||||
));
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let pending = match conn.loaded_page_mut_for_protocol_access(cmd.session_id) {
|
||||
Ok(page) => page.start_prepare_network_resource_load(
|
||||
frame_id,
|
||||
@@ -71,9 +72,9 @@ pub(super) fn start_load_network_resource_command(
|
||||
match pending {
|
||||
Ok(pending) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch {
|
||||
command_id: cmd.id,
|
||||
session_id: cmd.session_id.map(str::to_owned),
|
||||
owner_scope,
|
||||
kind: PendingNetworkCommandKind::PrepareNetworkResourceLoad,
|
||||
pending: PendingNetworkCommandWork::Page(pending),
|
||||
pending: PendingNetworkCommandWork::page(pending),
|
||||
}),
|
||||
Err(error) => {
|
||||
NetworkCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error.to_string()))
|
||||
@@ -85,9 +86,16 @@ pub(super) fn complete_network_resource_preparation(
|
||||
conn: &mut CdpConnection,
|
||||
completed: CompletedNetworkCommandDispatch,
|
||||
) -> NetworkCommandTaskStep {
|
||||
let owner_scope = completed.owner_scope.clone();
|
||||
let completion = match completed.completed {
|
||||
CompletedNetworkCommandWork::Page(Ok(completion)) => *completion,
|
||||
CompletedNetworkCommandWork::Page(Err(error)) => {
|
||||
CompletedNetworkCommandWork::Page {
|
||||
completed: Ok(completion),
|
||||
..
|
||||
} => *completion,
|
||||
CompletedNetworkCommandWork::Page {
|
||||
completed: Err(error),
|
||||
..
|
||||
} => {
|
||||
return NetworkCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error));
|
||||
}
|
||||
CompletedNetworkCommandWork::Resource(_) => {
|
||||
@@ -95,8 +103,14 @@ pub(super) fn complete_network_resource_preparation(
|
||||
}
|
||||
};
|
||||
let preparation = match conn
|
||||
.loaded_page_mut_for_protocol_access(completed.session_id.as_deref())
|
||||
.loaded_page_mut_for_protocol_access_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
)
|
||||
.and_then(|page| {
|
||||
if page.renderer_agent_attachment_id() != completion.renderer_agent_attachment_id() {
|
||||
return Err("Document changed while preparing the network resource load".to_owned());
|
||||
}
|
||||
page.finish_prepare_network_resource_load(completion)
|
||||
.map_err(|error| error.to_string())
|
||||
}) {
|
||||
@@ -109,7 +123,7 @@ pub(super) fn complete_network_resource_preparation(
|
||||
moli_core::page::RendererNetworkResourceLoadPreparation::Ready(pending) => {
|
||||
NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch {
|
||||
command_id: completed.command_id,
|
||||
session_id: completed.session_id,
|
||||
owner_scope,
|
||||
kind: PendingNetworkCommandKind::FetchNetworkResource,
|
||||
pending: PendingNetworkCommandWork::Resource(pending),
|
||||
})
|
||||
@@ -133,9 +147,10 @@ pub(super) fn complete_network_resource_fetch(
|
||||
conn: &mut CdpConnection,
|
||||
completed: CompletedNetworkCommandDispatch,
|
||||
) -> CommandOutputPlan {
|
||||
let owner_scope = completed.owner_scope.clone();
|
||||
let outcome = match completed.completed {
|
||||
CompletedNetworkCommandWork::Resource(outcome) => outcome,
|
||||
CompletedNetworkCommandWork::Page(_) => {
|
||||
CompletedNetworkCommandWork::Page { .. } => {
|
||||
return invalid_completion_plan();
|
||||
}
|
||||
};
|
||||
@@ -162,8 +177,9 @@ pub(super) fn complete_network_resource_fetch(
|
||||
Some(headers),
|
||||
);
|
||||
}
|
||||
let stream = match conn.open_io_stream_body_source_for_session_owner(
|
||||
completed.session_id.as_deref(),
|
||||
let stream = match conn.open_io_stream_body_source_for_route(
|
||||
owner_scope.session_id(),
|
||||
owner_scope.session_owner_route(),
|
||||
CapturedBody::from_bytes_spooled(response.body),
|
||||
) {
|
||||
Ok(stream) => stream,
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
use super::*;
|
||||
|
||||
async fn install_network_session_page(ctx: &mut TestContext, url: &str) {
|
||||
let mut browser_context = BrowserContext::new("BID-navigation".into());
|
||||
browser_context.set_active_target_id("TID-navigation");
|
||||
browser_context.attach_active_session("SID-navigation");
|
||||
ctx.conn.browser_context = Some(browser_context);
|
||||
let page = ctx
|
||||
.conn
|
||||
.load_page_via_runtime_async(url)
|
||||
.await
|
||||
.expect("the target should have a committed document");
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.set_loaded_page_for_test(page);
|
||||
}
|
||||
|
||||
/// Network.enable without a browser context fails.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn enable_no_bc_error() {
|
||||
@@ -26,6 +45,215 @@ async fn enable_with_bc_succeeds() {
|
||||
.primary_network_events_enabled()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn network_configuration_commands_succeed_while_the_target_is_changing_documents() {
|
||||
let mut ctx = TestContext::new();
|
||||
install_network_session_page(&mut ctx, "data:text/html,committed").await;
|
||||
ctx.conn
|
||||
.start_document_navigation_for_session_owner(
|
||||
Some("SID-navigation"),
|
||||
"LID-pending".to_owned(),
|
||||
)
|
||||
.expect("the replacement navigation should start");
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 2,
|
||||
"method": "Network.enable",
|
||||
"sessionId": "SID-navigation",
|
||||
}))
|
||||
.await;
|
||||
|
||||
ctx.expect_result(2, json!({}), Some("SID-navigation"));
|
||||
|
||||
for (id, method, params) in [
|
||||
(
|
||||
3,
|
||||
"Network.setCacheDisabled",
|
||||
json!({ "cacheDisabled": true }),
|
||||
),
|
||||
(
|
||||
4,
|
||||
"Network.setBypassServiceWorker",
|
||||
json!({ "bypass": true }),
|
||||
),
|
||||
(
|
||||
5,
|
||||
"Network.setExtraHTTPHeaders",
|
||||
json!({ "headers": { "X-During-Navigation": "current" } }),
|
||||
),
|
||||
(
|
||||
6,
|
||||
"Network.setBlockedURLs",
|
||||
json!({ "urls": ["*://blocked.example/*"] }),
|
||||
),
|
||||
(
|
||||
7,
|
||||
"Network.emulateNetworkConditions",
|
||||
json!({
|
||||
"offline": true,
|
||||
"latency": 0,
|
||||
"downloadThroughput": -1,
|
||||
"uploadThroughput": -1,
|
||||
}),
|
||||
),
|
||||
(
|
||||
8,
|
||||
"Network.setUserAgentOverride",
|
||||
json!({ "userAgent": "Moli/During-Navigation" }),
|
||||
),
|
||||
] {
|
||||
ctx.process_async(json!({
|
||||
"id": id,
|
||||
"method": method,
|
||||
"sessionId": "SID-navigation",
|
||||
"params": params,
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(id, json!({}), Some("SID-navigation"));
|
||||
}
|
||||
|
||||
assert!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.primary_network_events_enabled()
|
||||
);
|
||||
let configuration = ctx
|
||||
.conn
|
||||
.prepared_document_commit_configuration_for_session_owner(
|
||||
Some("SID-navigation"),
|
||||
&url::Url::parse("data:text/html,committed").unwrap(),
|
||||
)
|
||||
.expect("commit configuration should resolve the target resource runtime");
|
||||
assert!(configuration.cache_disabled);
|
||||
assert!(configuration.bypass_service_worker);
|
||||
assert!(configuration.network_offline);
|
||||
assert_eq!(
|
||||
configuration.extra_http_headers,
|
||||
[("X-During-Navigation".to_owned(), "current".to_owned())]
|
||||
);
|
||||
assert_eq!(
|
||||
configuration.blocked_url_patterns,
|
||||
["*://blocked.example/*".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
moli_core::network::ResourceRequestClient::from_browser_resource_runtime(
|
||||
configuration.browser_resource_runtime,
|
||||
)
|
||||
.user_agent(),
|
||||
"Moli/During-Navigation",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn network_configuration_completion_does_not_restore_a_replaced_document() {
|
||||
let mut ctx = TestContext::new();
|
||||
install_network_session_page(&mut ctx, "data:text/html,<body id=outgoing>outgoing</body>")
|
||||
.await;
|
||||
|
||||
let raw = json!({
|
||||
"id": 20,
|
||||
"method": "Network.setExtraHTTPHeaders",
|
||||
"sessionId": "SID-navigation",
|
||||
"params": { "headers": { "X-Replacement-Race": "configured" } },
|
||||
})
|
||||
.to_string();
|
||||
let crate::conn::CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw)
|
||||
else {
|
||||
panic!("the loaded Page should receive the Network configuration command");
|
||||
};
|
||||
let completed = pending.wait().await;
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 21,
|
||||
"method": "Page.navigate",
|
||||
"sessionId": "SID-navigation",
|
||||
"params": {
|
||||
"url": "data:text/html,<body id=replacement>replacement</body>"
|
||||
},
|
||||
}))
|
||||
.await;
|
||||
let navigate = ctx.take_response_by_id(21);
|
||||
assert!(navigate["result"]["loaderId"].is_string());
|
||||
|
||||
let crate::conn::CdpCommandTaskStep::Complete(outcome) =
|
||||
ctx.conn.complete_pending_command_dispatch(completed).await
|
||||
else {
|
||||
panic!("the settled Network command should complete in one protocol phase");
|
||||
};
|
||||
assert!(outcome.into_parts().0.iter().any(|message| {
|
||||
message["id"] == json!(20)
|
||||
&& message["sessionId"] == json!("SID-navigation")
|
||||
&& message["result"] == json!({})
|
||||
}));
|
||||
|
||||
let html = ctx
|
||||
.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.active_target
|
||||
.runtime_slot
|
||||
.loaded_page_mut()
|
||||
.expect("the replacement Page should remain installed")
|
||||
.serialize_html_async()
|
||||
.await
|
||||
.expect("the replacement Page should remain usable");
|
||||
assert!(html.contains("id=\"replacement\""));
|
||||
assert!(!html.contains("id=\"outgoing\""));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn commit_configuration_resolves_the_exact_target_network_runtime() {
|
||||
let mut ctx = TestContext::new();
|
||||
let mut browser_context = BrowserContext::new("BID-runtime".into());
|
||||
browser_context.set_active_target_id("TID-a");
|
||||
browser_context.attach_active_session("SID-a");
|
||||
browser_context.insert_page_target_host(PageTargetHost::with_url(
|
||||
"TID-b".to_owned(),
|
||||
Some("SID-b".to_owned()),
|
||||
"about:blank".to_owned(),
|
||||
));
|
||||
ctx.conn.browser_context = Some(browser_context);
|
||||
|
||||
for (id, session_id, user_agent) in [
|
||||
(30, "SID-a", "Moli/Target-A"),
|
||||
(31, "SID-b", "Moli/Target-B"),
|
||||
] {
|
||||
ctx.process_async(json!({
|
||||
"id": id,
|
||||
"method": "Network.setUserAgentOverride",
|
||||
"sessionId": session_id,
|
||||
"params": { "userAgent": user_agent },
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(id, json!({}), Some(session_id));
|
||||
}
|
||||
|
||||
for (session_id, expected_user_agent) in [
|
||||
("SID-b", "Moli/Target-B"),
|
||||
("SID-a", "Moli/Target-A"),
|
||||
("SID-b", "Moli/Target-B"),
|
||||
] {
|
||||
let configuration = ctx
|
||||
.conn
|
||||
.prepared_document_commit_configuration_for_session_owner(
|
||||
Some(session_id),
|
||||
&url::Url::parse("about:blank").unwrap(),
|
||||
)
|
||||
.expect("the target-specific resource runtime should resolve");
|
||||
let request_client =
|
||||
moli_core::network::ResourceRequestClient::from_browser_resource_runtime(
|
||||
configuration.browser_resource_runtime,
|
||||
);
|
||||
assert_eq!(request_client.user_agent(), expected_user_agent);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn auxiliary_network_enable_does_not_enable_primary_session() {
|
||||
let mut ctx = TestContext::new();
|
||||
|
||||
@@ -6510,13 +6510,10 @@ fn try_start_page_enable_command(
|
||||
"BrowserContextNotLoaded",
|
||||
)));
|
||||
}
|
||||
if conn.session_owner_target_has_waiting_for_debugger_session(cmd.session_id) {
|
||||
return Some(PageCommandTaskStep::Complete(CommandOutputPlan::success()));
|
||||
}
|
||||
match conn.runtime_session_owner_slot(cmd.session_id) {
|
||||
Ok(slot)
|
||||
if slot.has_loaded_page()
|
||||
&& conn.runtime_session_owner_should_start_initial_document_navigation(
|
||||
&& conn.runtime_session_owner_can_start_initial_document_navigation(
|
||||
cmd.session_id,
|
||||
) =>
|
||||
{
|
||||
|
||||
@@ -3077,11 +3077,12 @@ fn start_navigate_to_url_command_with_background_policy_and_request(
|
||||
"NavigationRequestNotCurrent",
|
||||
));
|
||||
};
|
||||
let none_session_owner_route = completion_state
|
||||
.navigate_session_id
|
||||
.is_none()
|
||||
.then(|| conn.none_session_owner_route_override())
|
||||
.flatten();
|
||||
let none_session_owner_route = crate::conn::CommandOwnerScope::capture(
|
||||
conn,
|
||||
completion_state.navigate_session_id.as_deref(),
|
||||
)
|
||||
.session_owner_route()
|
||||
.cloned();
|
||||
tokio::task::spawn_local(async move {
|
||||
let body_completion_sink = crate::conn::BackgroundNavigationBodyCompletionSink::new(
|
||||
sender.clone(),
|
||||
@@ -3382,11 +3383,14 @@ pub(crate) async fn complete_materialized_navigation_into_buffer_async(
|
||||
match navigation {
|
||||
network::MaterializedNavigationLoadOutcome::ResponseCommitReady(navigation) => {
|
||||
let navigation = *navigation;
|
||||
let configuration = conn.prepared_document_commit_configuration_for_session_owner(
|
||||
let update_result = match conn.prepared_document_commit_configuration_for_session_owner(
|
||||
state.navigate_session_id.as_deref(),
|
||||
navigation.final_url(),
|
||||
);
|
||||
if let Err(error) = navigation.update_commit_configuration(configuration).await {
|
||||
) {
|
||||
Ok(configuration) => navigation.update_commit_configuration(configuration).await,
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
if let Err(error) = update_result {
|
||||
push_navigation_commit_error(out, &state, error);
|
||||
} else {
|
||||
let renderer_page = navigation.renderer_page_residence_identity();
|
||||
|
||||
@@ -1283,7 +1283,7 @@ fn start_create_isolated_world_initial_navigation_or_renderer_phase(
|
||||
mut task: CreateIsolatedWorldCommandTask,
|
||||
) -> PageCommandTaskStep {
|
||||
let should_start_target_url_navigation =
|
||||
conn.runtime_session_owner_should_start_initial_document_navigation(session_id);
|
||||
conn.runtime_session_owner_can_start_initial_document_navigation(session_id);
|
||||
if !should_start_target_url_navigation {
|
||||
return start_create_isolated_world_frame_or_world_phase(
|
||||
conn, command_id, session_id, task,
|
||||
|
||||
@@ -135,7 +135,8 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
conn: &mut CdpConnection,
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
session_id: Option<&str>,
|
||||
error_text: &str,
|
||||
navigation_error_text: &str,
|
||||
subresource_error_text: &str,
|
||||
pending_navigations: Vec<PendingFetchNavigation>,
|
||||
pending_auth_navigations: Vec<crate::conn::PendingFetchAuthNavigation>,
|
||||
pending_response_navigations: Vec<crate::conn::PausedDocumentTransfer>,
|
||||
@@ -146,6 +147,10 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
crate::conn::PendingSubresourceFetchResponseRequest,
|
||||
)>,
|
||||
) -> Option<RendererOutputFence> {
|
||||
// A protocol navigation waiter may expose an operation-specific failure,
|
||||
// while Network.loadingFailed must retain the underlying net error. For
|
||||
// Page.stopLoading Chromium reports ERR_ABORTED/canceled=true even though
|
||||
// Moli's pending navigation reply remains "Navigation stopped".
|
||||
let mut renderer_output_predecessor = None;
|
||||
for pending in pending_navigations {
|
||||
let token = pending.document_navigation_token;
|
||||
@@ -153,7 +158,7 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
let navigation = network::materialize_navigation_failure_preserving_committed_document(
|
||||
conn,
|
||||
&navigation_state,
|
||||
error_text.to_owned(),
|
||||
navigation_error_text.to_owned(),
|
||||
);
|
||||
let predecessor = complete_tokened_materialized_navigation_background_events_async(
|
||||
conn,
|
||||
@@ -171,7 +176,7 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
let navigation = network::materialize_navigation_failure_preserving_committed_document(
|
||||
conn,
|
||||
&navigation_state,
|
||||
error_text.to_owned(),
|
||||
navigation_error_text.to_owned(),
|
||||
);
|
||||
let predecessor = complete_tokened_materialized_navigation_background_events_async(
|
||||
conn,
|
||||
@@ -184,11 +189,11 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
merge_renderer_output_predecessor(&mut renderer_output_predecessor, predecessor);
|
||||
}
|
||||
for pending in pending_response_navigations {
|
||||
let (token, navigation, _) = pending.fail(error_text.to_owned());
|
||||
let (token, navigation, _) = pending.fail(navigation_error_text.to_owned());
|
||||
let result = network::materialize_navigation_failure_preserving_committed_document(
|
||||
conn,
|
||||
&navigation,
|
||||
error_text.to_owned(),
|
||||
navigation_error_text.to_owned(),
|
||||
);
|
||||
let predecessor = complete_tokened_materialized_navigation_background_events_async(
|
||||
conn, out, token, navigation, result,
|
||||
@@ -204,7 +209,7 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
.fail_pending_subresource_fetch_for_session_owner_async(
|
||||
session_id,
|
||||
pending.internal_id,
|
||||
error_text.to_owned(),
|
||||
subresource_error_text.to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -229,7 +234,7 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
.fail_pending_subresource_auth_for_session_owner_async(
|
||||
session_id,
|
||||
pending.internal_id,
|
||||
error_text.to_owned(),
|
||||
subresource_error_text.to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -254,7 +259,7 @@ pub(crate) async fn fail_pending_fetch_state_background_events_async(
|
||||
.fail_pending_subresource_response_for_session_owner_async(
|
||||
session_id,
|
||||
pending.internal_id,
|
||||
error_text.to_owned(),
|
||||
subresource_error_text.to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -387,6 +392,7 @@ pub(super) async fn complete_stop_loading_command_dispatch(
|
||||
&mut out,
|
||||
session_id,
|
||||
"Navigation stopped",
|
||||
moli_fetch::NET_ERR_ABORTED_ERROR_TEXT,
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
@@ -504,6 +510,7 @@ pub(super) async fn complete_crash_command_dispatch(
|
||||
&mut out,
|
||||
fail_session_id,
|
||||
"Page crashed",
|
||||
"Page crashed",
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
@@ -617,6 +624,7 @@ pub(super) async fn complete_close_command_dispatch(
|
||||
&mut out,
|
||||
fail_session_id,
|
||||
"Page closed",
|
||||
"Page closed",
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
|
||||
@@ -44,15 +44,7 @@ struct RuntimeBindingCallBatch {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct RuntimeInspectorMessageBatch {
|
||||
/// Exact Page and protocol attachment that owned these messages when the
|
||||
/// renderer snapshot was captured.
|
||||
///
|
||||
/// `RuntimeInspectorMessageBatch` may contain command responses as well as
|
||||
/// notifications. A projection-time session fallback would therefore be
|
||||
/// able to deliver an old response to a replacement Page. The attachment
|
||||
/// is captured before the output can cross an async or scheduler boundary
|
||||
/// and is revalidated immediately before delivery.
|
||||
attachment: crate::conn::TargetPageProtocolAttachmentIdentity,
|
||||
authority: RuntimeInspectorMessageAuthority,
|
||||
messages: Vec<RuntimeInspectorMessage>,
|
||||
/// Contexts created by this exact Inspector batch.
|
||||
///
|
||||
@@ -63,6 +55,55 @@ struct RuntimeInspectorMessageBatch {
|
||||
created_execution_context_ids: Vec<i64>,
|
||||
}
|
||||
|
||||
/// Projection authority for one prepared Inspector batch.
|
||||
///
|
||||
/// Ordinary observations remain tied to the exact Page attachment that
|
||||
/// produced them. A retired terminal response is a distinct state rather than
|
||||
/// an absent attachment: its exact renderer correlation was already consumed
|
||||
/// at ingress, and no notification or renderer state can inhabit this variant.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum RuntimeInspectorMessageAuthority {
|
||||
CurrentPage(crate::conn::TargetPageProtocolAttachmentIdentity),
|
||||
AuthorizedRetiredTerminal {
|
||||
browser_context_id: String,
|
||||
target_id: Option<String>,
|
||||
session_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl RuntimeInspectorMessageAuthority {
|
||||
fn session_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::CurrentPage(attachment) => attachment.session_id(),
|
||||
Self::AuthorizedRetiredTerminal { session_id, .. } => session_id.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn permits_projection(&self, conn: &CdpConnection) -> bool {
|
||||
match self {
|
||||
Self::CurrentPage(attachment) => {
|
||||
conn.target_page_protocol_attachment_identity_is_current(attachment)
|
||||
}
|
||||
Self::AuthorizedRetiredTerminal {
|
||||
browser_context_id,
|
||||
target_id,
|
||||
session_id,
|
||||
} => {
|
||||
let Some((current_browser_context_id, current_target_id)) =
|
||||
conn.target_owner_identity_for_session(session_id.as_deref())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let current_target_id = current_target_id.or_else(|| {
|
||||
conn.browser_context_by_id(¤t_browser_context_id)
|
||||
.and_then(|context| context.active_target_id_owned())
|
||||
});
|
||||
current_browser_context_id == *browser_context_id && current_target_id == *target_id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum RuntimeInspectorMessage {
|
||||
Context(RuntimeContextProtocolEvent),
|
||||
@@ -150,12 +191,10 @@ impl RuntimeOutputProjectionStep {
|
||||
})
|
||||
{
|
||||
for batch in batches {
|
||||
if !conn
|
||||
.target_page_protocol_attachment_identity_is_current(&batch.attachment)
|
||||
{
|
||||
if !batch.authority.permits_projection(conn) {
|
||||
continue;
|
||||
}
|
||||
let session_id = batch.attachment.session_id().map(str::to_owned);
|
||||
let session_id = batch.authority.session_id().map(str::to_owned);
|
||||
push_runtime_inspector_messages_for_session(
|
||||
conn,
|
||||
context.command.protocol_events_mut(),
|
||||
@@ -278,7 +317,7 @@ impl RuntimePreparedOutputs {
|
||||
}
|
||||
}
|
||||
let prepared = RuntimeInspectorMessageBatch {
|
||||
attachment,
|
||||
authority: RuntimeInspectorMessageAuthority::CurrentPage(attachment),
|
||||
messages,
|
||||
created_execution_context_ids,
|
||||
};
|
||||
@@ -294,6 +333,92 @@ impl RuntimePreparedOutputs {
|
||||
outputs
|
||||
}
|
||||
|
||||
/// Preserves only terminal session responses from an attachment that was
|
||||
/// current when the response lease was claimed but retired before its Page
|
||||
/// journal crossed protocol ingress.
|
||||
///
|
||||
/// The exact renderer correlation is consumed here. This makes the
|
||||
/// resulting response an already-authorized session fact, while all
|
||||
/// notifications, context events, V8 state, and remote-object projection
|
||||
/// from the retired document are discarded.
|
||||
pub(crate) fn from_retired_renderer_runtime_inspector_session_responses(
|
||||
conn: &mut CdpConnection,
|
||||
source_session_id: Option<&str>,
|
||||
batches: &[RendererRuntimeInspectorMessageBatch],
|
||||
) -> Self {
|
||||
let mut outputs = Self::default();
|
||||
for batch in batches {
|
||||
let Some(renderer_agent_attachment_id) = batch.renderer_agent_attachment_id() else {
|
||||
continue;
|
||||
};
|
||||
let Some(protocol_attachment) = conn
|
||||
.target_page_protocol_attachment_identity_for_renderer_inspector_route(
|
||||
source_session_id,
|
||||
batch.session.wire_session_id(),
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let session_id = protocol_attachment.session_id().map(str::to_owned);
|
||||
if conn.renderer_agent_attachment_is_current_for_session_owner(
|
||||
session_id.as_deref(),
|
||||
renderer_agent_attachment_id,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut responses = batch
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
matches!(
|
||||
message,
|
||||
RendererRuntimeInspectorMessage::Protocol(message)
|
||||
if message.renderer_call_id().is_some()
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
conn.restore_frontend_command_ids_in_retired_devtools_session_output(
|
||||
session_id.as_deref(),
|
||||
renderer_agent_attachment_id,
|
||||
&mut responses,
|
||||
);
|
||||
if responses.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let target_id = protocol_attachment.page_owner().target_id();
|
||||
let messages = responses
|
||||
.into_iter()
|
||||
.map(|message| RuntimeInspectorMessage::from_renderer_message(message, target_id))
|
||||
.collect();
|
||||
let prepared = RuntimeInspectorMessageBatch {
|
||||
authority: RuntimeInspectorMessageAuthority::AuthorizedRetiredTerminal {
|
||||
browser_context_id: protocol_attachment
|
||||
.page_owner()
|
||||
.browser_context_id()
|
||||
.to_owned(),
|
||||
target_id: protocol_attachment
|
||||
.page_owner()
|
||||
.target_id()
|
||||
.map(str::to_owned),
|
||||
session_id,
|
||||
},
|
||||
messages,
|
||||
created_execution_context_ids: Vec::new(),
|
||||
};
|
||||
match batch.command_response_order() {
|
||||
RendererRuntimeInspectorMessageResponseOrder::BeforeCommandResponse => {
|
||||
outputs.inspector_message_batches.push(prepared);
|
||||
}
|
||||
RendererRuntimeInspectorMessageResponseOrder::AfterCommandResponse => outputs
|
||||
.post_response_inspector_message_batches
|
||||
.push(prepared),
|
||||
}
|
||||
}
|
||||
outputs
|
||||
}
|
||||
|
||||
pub(crate) fn extend(&mut self, other: Self) {
|
||||
self.binding_call_batches.extend(other.binding_call_batches);
|
||||
self.inspector_message_batches
|
||||
@@ -406,15 +531,15 @@ pub(in crate::domains) fn push_routed_renderer_runtime_inspector_message_batch_b
|
||||
.into_iter()
|
||||
.chain(prepared.post_response_inspector_message_batches)
|
||||
{
|
||||
if !conn.target_page_protocol_attachment_identity_is_current(&batch.attachment) {
|
||||
if !batch.authority.permits_projection(conn) {
|
||||
continue;
|
||||
}
|
||||
let effective_session_id = batch.attachment.session_id().map(str::to_owned);
|
||||
let session_id = batch.authority.session_id().map(str::to_owned);
|
||||
push_runtime_inspector_messages_for_session(
|
||||
conn,
|
||||
out,
|
||||
batch.messages,
|
||||
effective_session_id.as_deref(),
|
||||
session_id.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -422,9 +547,10 @@ pub(in crate::domains) fn push_routed_renderer_runtime_inspector_message_batch_b
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use moli_core::page::{
|
||||
DevToolsSessionKey, RendererDevToolsAgentToken, RendererRuntimeInspectorMessage,
|
||||
RendererRuntimeInspectorMessageBatch,
|
||||
DevToolsSessionKey, RendererAgentAttachmentId, RendererDevToolsAgentToken,
|
||||
RendererRuntimeInspectorMessage, RendererRuntimeInspectorMessageBatch,
|
||||
};
|
||||
use moli_page_types::RendererInspectorResponseDelivery;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -610,6 +736,103 @@ mod tests {
|
||||
command_context.take_protocol_events()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn retired_attachment_keeps_only_the_exact_terminal_session_response() {
|
||||
let mut ctx = TestContext::new();
|
||||
load_document(&mut ctx, "<!doctype html><main>replacement</main>").await;
|
||||
let current = ctx
|
||||
.conn
|
||||
.runtime_session_owner_slot(Some("SID-1"))
|
||||
.expect("loaded target runtime")
|
||||
.current_renderer_attachment()
|
||||
.expect("current renderer attachment");
|
||||
let retired_attachment = RendererAgentAttachmentId::allocate();
|
||||
assert_ne!(retired_attachment, current.id());
|
||||
|
||||
let frontend = crate::conn::ParsedCdpCommand::parse_str(
|
||||
r#"{"id":901001,"method":"Runtime.evaluate","sessionId":"SID-1","params":{"expression":"({ stale: true })"}}"#,
|
||||
)
|
||||
.expect("frontend Runtime command");
|
||||
let prepared = ctx
|
||||
.conn
|
||||
.try_register_renderer_call_for_session_owner(
|
||||
Some("SID-1"),
|
||||
901_001,
|
||||
Some(retired_attachment),
|
||||
crate::conn::RendererCommandDescriptor::from_frontend_policy(
|
||||
frontend.json().to_owned(),
|
||||
frontend.renderer_policy(),
|
||||
RendererInspectorResponseDelivery::DevToolsSession,
|
||||
),
|
||||
)
|
||||
.expect("retired renderer call correlation");
|
||||
let correlation = prepared.correlation();
|
||||
drop(prepared);
|
||||
|
||||
let mut batch = RendererRuntimeInspectorMessageBatch::new(
|
||||
current.agent_token(),
|
||||
DevToolsSessionKey::Primary,
|
||||
renderer_messages(vec![
|
||||
json!({
|
||||
"method": "Runtime.consoleAPICalled",
|
||||
"params": {"type": "log", "args": [], "executionContextId": 1},
|
||||
}),
|
||||
json!({
|
||||
"id": correlation.renderer_call_id().get(),
|
||||
"result": {
|
||||
"result": {"type": "object", "objectId": "retired-object"}
|
||||
},
|
||||
}),
|
||||
]),
|
||||
);
|
||||
batch.bind_renderer_agent_attachment(retired_attachment);
|
||||
|
||||
let outputs =
|
||||
RuntimePreparedOutputs::from_retired_renderer_runtime_inspector_session_responses(
|
||||
&mut ctx.conn,
|
||||
Some("SID-1"),
|
||||
&[batch],
|
||||
);
|
||||
let outputs_after_detach = outputs.clone();
|
||||
let events = drain_runtime_inspector_outputs(&mut ctx.conn, outputs, None)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(crate::conn::BackgroundProtocolEvent::into_protocol_message)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(events.len(), 1, "retired notifications must be discarded");
|
||||
assert_eq!(events[0]["id"], json!(901_001));
|
||||
assert_eq!(events[0]["sessionId"], json!("SID-1"));
|
||||
assert!(
|
||||
ctx.conn
|
||||
.renderer_runtime_command_cause_for_frontend(Some("SID-1"), 901_001)
|
||||
.is_none(),
|
||||
"the accepted terminal response must consume its exact correlation"
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.runtime_remote_object_group_for_session_owner(Some("SID-1"), "retired-object",),
|
||||
None,
|
||||
"a retired document response must not register objects on the replacement document"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.conn
|
||||
.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context")
|
||||
.detach_active_session()
|
||||
.as_deref(),
|
||||
Some("SID-1"),
|
||||
);
|
||||
assert!(
|
||||
drain_runtime_inspector_outputs(&mut ctx.conn, outputs_after_detach, None)
|
||||
.await
|
||||
.is_empty(),
|
||||
"terminal authority may outlive its Page, but not its protocol session binding",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn runtime_backlog_batch_ignores_prequeued_inspector_registry() {
|
||||
let mut ctx = TestContext::new();
|
||||
|
||||
@@ -1006,11 +1006,7 @@ fn start_main_runtime_inspector_command(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let session_owner_route = if await_promise {
|
||||
conn.runtime_await_owner_route_for_session(cmd.session_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let pre_registered_await = match pre_register_runtime_await_if_needed(
|
||||
conn,
|
||||
await_promise,
|
||||
@@ -1046,10 +1042,7 @@ fn start_main_runtime_inspector_command(
|
||||
RuntimeCommandTaskStep::Pending(Box::new(PendingRuntimeCommandDispatch {
|
||||
command_id: cmd.id,
|
||||
action: action_label,
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
cmd.session_id,
|
||||
session_owner_route,
|
||||
),
|
||||
owner_scope,
|
||||
object_group,
|
||||
release_object_ids,
|
||||
release_object_group,
|
||||
@@ -1632,11 +1625,7 @@ fn try_start_pending_runtime_enable_command(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
) -> Option<RuntimeCommandTaskStep> {
|
||||
let session_owner_route = if cmd.session_id.is_none() {
|
||||
conn.none_session_owner_route_override()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let has_loaded_page = match conn.runtime_session_owner_slot(cmd.session_id) {
|
||||
Ok(slot) => slot.has_loaded_page(),
|
||||
Err(_) if cmd.session_id.is_some() => {
|
||||
@@ -1684,7 +1673,7 @@ fn try_start_pending_runtime_enable_command(
|
||||
conn,
|
||||
cmd.id,
|
||||
cmd.session_id,
|
||||
session_owner_route,
|
||||
owner_scope,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1692,16 +1681,13 @@ fn start_pending_runtime_enable_events_phase(
|
||||
conn: &mut CdpConnection,
|
||||
command_id: Option<u64>,
|
||||
session_id: Option<&str>,
|
||||
session_owner_route: Option<CdpSessionRoute>,
|
||||
owner_scope: CommandOwnerScope,
|
||||
) -> RuntimeCommandTaskStep {
|
||||
match conn.start_runtime_enable_events_for_session_owner(session_id) {
|
||||
Ok(pending) => RuntimeCommandTaskStep::Pending(Box::new(PendingRuntimeCommandDispatch {
|
||||
command_id,
|
||||
action: "enable",
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
session_id,
|
||||
session_owner_route,
|
||||
),
|
||||
owner_scope,
|
||||
object_group: None,
|
||||
release_object_ids: Vec::new(),
|
||||
release_object_group: None,
|
||||
@@ -1729,11 +1715,7 @@ fn try_start_pending_runtime_binding_command(
|
||||
cmd: &Cmd<'_>,
|
||||
action: RuntimeBindingCommand,
|
||||
) -> Option<RuntimeCommandTaskStep> {
|
||||
let session_owner_route = if cmd.session_id.is_none() {
|
||||
conn.none_session_owner_route_override()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let (name, execution_context_name, execution_context_id) = match action {
|
||||
RuntimeBindingCommand::Add => {
|
||||
let params = match cmd.get_params::<AddBindingParams>() {
|
||||
@@ -1805,10 +1787,7 @@ fn try_start_pending_runtime_binding_command(
|
||||
let meta = RuntimeCommandCompletionMeta {
|
||||
command_id: cmd.id,
|
||||
action: task.action.label(),
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
cmd.session_id,
|
||||
session_owner_route.clone(),
|
||||
),
|
||||
owner_scope: owner_scope.clone(),
|
||||
object_group: None,
|
||||
release_object_ids: Vec::new(),
|
||||
release_object_group: None,
|
||||
@@ -1824,7 +1803,7 @@ fn try_start_pending_runtime_binding_command(
|
||||
cmd.session_id,
|
||||
task,
|
||||
execution_context_id,
|
||||
session_owner_route,
|
||||
owner_scope,
|
||||
);
|
||||
}
|
||||
if matches!(task.action, RuntimeBindingCommand::Add)
|
||||
@@ -1860,10 +1839,7 @@ fn try_start_pending_runtime_binding_command(
|
||||
PendingRuntimeCommandDispatch {
|
||||
command_id: cmd.id,
|
||||
action: action_label,
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
cmd.session_id,
|
||||
session_owner_route,
|
||||
),
|
||||
owner_scope,
|
||||
object_group: None,
|
||||
release_object_ids: Vec::new(),
|
||||
release_object_group: None,
|
||||
@@ -2122,11 +2098,7 @@ fn start_devtools_runtime_command(
|
||||
}
|
||||
};
|
||||
let object_group = runtime_object_group_for_command_result(conn, cmd, action);
|
||||
let session_owner_route = if await_promise {
|
||||
conn.runtime_await_owner_route_for_session(cmd.session_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let pre_registered_await = match pre_register_runtime_await_if_needed(
|
||||
conn,
|
||||
await_promise,
|
||||
@@ -2159,10 +2131,7 @@ fn start_devtools_runtime_command(
|
||||
RuntimeCommandTaskStep::Pending(Box::new(PendingRuntimeCommandDispatch {
|
||||
command_id: cmd.id,
|
||||
action: action_label,
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
cmd.session_id,
|
||||
session_owner_route,
|
||||
),
|
||||
owner_scope,
|
||||
object_group,
|
||||
release_object_ids: Vec::new(),
|
||||
release_object_group: None,
|
||||
@@ -7651,7 +7620,7 @@ fn start_pending_runtime_binding_context_lookup_phase(
|
||||
session_id: Option<&str>,
|
||||
task: RuntimeBindingCommandTask,
|
||||
execution_context_id: i64,
|
||||
session_owner_route: Option<CdpSessionRoute>,
|
||||
owner_scope: CommandOwnerScope,
|
||||
) -> Option<RuntimeCommandTaskStep> {
|
||||
let pending = conn
|
||||
.start_child_default_execution_context_lookup_for_session_owner(
|
||||
@@ -7663,10 +7632,7 @@ fn start_pending_runtime_binding_context_lookup_phase(
|
||||
PendingRuntimeCommandDispatch {
|
||||
command_id,
|
||||
action: task.action.label(),
|
||||
owner_scope: CommandOwnerScope::from_session_and_owner_route(
|
||||
session_id,
|
||||
session_owner_route,
|
||||
),
|
||||
owner_scope,
|
||||
object_group: None,
|
||||
release_object_ids: Vec::new(),
|
||||
release_object_group: None,
|
||||
@@ -8497,7 +8463,9 @@ async fn complete_pending_runtime_inspector_command(
|
||||
object_group,
|
||||
);
|
||||
}
|
||||
if completed.action == "runIfWaitingForDebugger" {
|
||||
if completed.action == "runIfWaitingForDebugger"
|
||||
&& conn.release_waiting_for_debugger_session(completed.session_id())
|
||||
{
|
||||
crate::domains::target::schedule_initial_document_target_url_navigation_after_debugger_resume(
|
||||
conn,
|
||||
completed.session_id(),
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(crate) use popup::{
|
||||
complete_popup_target_navigation_owner_action_async,
|
||||
create_popup_target_from_renderer_output_background_events_async,
|
||||
emit_target_info_changed_for_session_owner_background_event,
|
||||
schedule_initial_document_target_url_navigation_after_debugger_barrier_release_for_target,
|
||||
schedule_initial_document_target_url_navigation_after_debugger_resume,
|
||||
};
|
||||
pub(crate) fn popup_activation_creates_new_target(
|
||||
@@ -254,6 +255,7 @@ async fn clear_detached_target_owner_fetch_state_background_events_async(
|
||||
out,
|
||||
session_id,
|
||||
"Target detached",
|
||||
"Target detached",
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
|
||||
@@ -1026,6 +1026,7 @@ async fn detach_from_target_inner_async(
|
||||
out.background_events_mut(),
|
||||
Some(¤t_session_id),
|
||||
"Target detached",
|
||||
"Target detached",
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
|
||||
@@ -234,6 +234,7 @@ async fn fail_pending_navigations_for_disposed_target_async(
|
||||
out,
|
||||
session_id,
|
||||
DISPOSE_REASON,
|
||||
DISPOSE_REASON,
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
|
||||
@@ -298,6 +298,7 @@ pub(super) async fn fail_pending_fetch_state_for_target_background_events_async(
|
||||
out,
|
||||
session_id,
|
||||
reason,
|
||||
reason,
|
||||
pending_navigations,
|
||||
pending_auth_navigations,
|
||||
pending_response_navigations,
|
||||
|
||||
@@ -436,7 +436,7 @@ pub(crate) async fn start_initial_document_target_url_navigation_if_needed_backg
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
if !conn.runtime_session_owner_should_start_initial_document_navigation(session_id) {
|
||||
if !conn.runtime_session_owner_can_start_initial_document_navigation(session_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(target_url) = conn.runtime_session_owner_target_url(session_id) else {
|
||||
@@ -456,21 +456,51 @@ pub(crate) fn schedule_initial_document_target_url_navigation_after_debugger_res
|
||||
conn: &mut CdpConnection,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
if !conn.runtime_session_owner_should_start_initial_document_navigation(session_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(target_url) = conn.runtime_session_owner_target_url(session_id) else {
|
||||
let Some((_, Some(target_id))) = conn.target_owner_identity_for_session(session_id) else {
|
||||
return false;
|
||||
};
|
||||
let Some((browser_context_id, Some(target_id))) =
|
||||
conn.target_owner_identity_for_session(session_id)
|
||||
schedule_initial_document_target_url_navigation_after_debugger_barrier_release_for_target(
|
||||
conn, &target_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn schedule_initial_document_target_url_navigation_after_debugger_barrier_release_for_target(
|
||||
conn: &mut CdpConnection,
|
||||
target_id: &str,
|
||||
) -> bool {
|
||||
if conn.target_has_waiting_for_debugger_session(target_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(route) = conn.target_session_route_for_target_id(target_id) else {
|
||||
return false;
|
||||
};
|
||||
if !matches!(
|
||||
&route,
|
||||
crate::conn::CdpSessionRoute::ActiveTarget { .. }
|
||||
| crate::conn::CdpSessionRoute::AuxiliaryTarget { .. }
|
||||
| crate::conn::CdpSessionRoute::PageTargetHost { .. }
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let Some(browser_context_id) = route.browser_context_id().map(str::to_owned) else {
|
||||
return false;
|
||||
};
|
||||
let Some(browser_context) = conn.browser_context_by_id(&browser_context_id) else {
|
||||
return false;
|
||||
};
|
||||
if !browser_context.target_needs_initial_document_navigation(target_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(target_url) = browser_context
|
||||
.devtools_target_info(target_id)
|
||||
.map(|target_info| target_info.url)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(action) = PopupTargetNavigationOwnerAction::capture(
|
||||
conn,
|
||||
&browser_context_id,
|
||||
&target_id,
|
||||
target_id,
|
||||
target_url,
|
||||
PopupTargetNavigationKind::InitialDocumentAfterDebuggerResume,
|
||||
) else {
|
||||
@@ -527,7 +557,11 @@ pub(crate) async fn complete_popup_target_navigation_owner_action_async(
|
||||
match kind {
|
||||
PopupTargetNavigationKind::InitialDocument
|
||||
| PopupTargetNavigationKind::InitialDocumentAfterDebuggerResume => {
|
||||
if !conn.runtime_session_owner_should_start_initial_document_navigation(None) {
|
||||
// Revalidate the barrier when the queued owner action actually
|
||||
// runs. Another inspector session can attach after this action is
|
||||
// scheduled; that new session must be able to pause the initial
|
||||
// document before any target-URL request starts.
|
||||
if !conn.runtime_session_owner_can_start_initial_document_navigation(None) {
|
||||
return crate::conn::CdpTurnOutcome::new_with_protocol_events(
|
||||
Vec::new(),
|
||||
conn.take_scheduler_events(),
|
||||
|
||||
+46
-8
@@ -865,8 +865,46 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
ctx.expect_result(260_218, json!({}), Some(popup_session_id));
|
||||
ctx.sent.clear();
|
||||
|
||||
// Playwright sends this command in the same initialization burst as
|
||||
// Runtime.runIfWaitingForDebugger. It must operate on the materialized
|
||||
// initial document without independently starting the target URL: only
|
||||
// the debugger-resume response owns that transition.
|
||||
ctx.process_async(json!({
|
||||
"id": 260_219,
|
||||
"method": "Page.createIsolatedWorld",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
"frameId": popup_target_id,
|
||||
"worldName": "__playwright_pre_resume_utility_world",
|
||||
"grantUniveralAccess": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let initial_world = take_response_by_id(ctx, 260_219);
|
||||
assert!(
|
||||
initial_world["result"]["executionContextId"]
|
||||
.as_i64()
|
||||
.is_some(),
|
||||
"createIsolatedWorld should resolve against the paused initial document: {initial_world:?}"
|
||||
);
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.has_pending_document_navigation_for_session_owner(Some(popup_session_id)),
|
||||
"createIsolatedWorld must not claim the debugger-gated initial navigation"
|
||||
);
|
||||
assert!(
|
||||
!ctx.sent.iter().any(|message| {
|
||||
message["method"] == json!("Fetch.requestPaused")
|
||||
|| (message["method"] == json!("Network.requestWillBeSent")
|
||||
&& message["params"]["request"]["url"] == json!(popup_url))
|
||||
}),
|
||||
"createIsolatedWorld must leave the real popup URL gated: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_220,
|
||||
"method": "Runtime.runIfWaitingForDebugger",
|
||||
"sessionId": popup_session_id
|
||||
}))
|
||||
@@ -880,7 +918,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
let response_index = ctx
|
||||
.sent
|
||||
.iter()
|
||||
.position(|message| message["id"] == json!(260_219))
|
||||
.position(|message| message["id"] == json!(260_220))
|
||||
.expect("runIfWaitingForDebugger terminal response");
|
||||
let request_index = ctx
|
||||
.sent
|
||||
@@ -892,7 +930,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
"the debugger-resume response must cross the frontend before its initial navigation can replace about:blank: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
take_response_by_id(ctx, 260_219);
|
||||
take_response_by_id(ctx, 260_220);
|
||||
let paused = ctx
|
||||
.sent
|
||||
.iter()
|
||||
@@ -923,7 +961,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
ctx.sent.clear();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_220,
|
||||
"id": 260_221,
|
||||
"method": "Page.createIsolatedWorld",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
@@ -933,7 +971,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let isolated = take_response_by_id(ctx, 260_220);
|
||||
let isolated = take_response_by_id(ctx, 260_221);
|
||||
assert!(
|
||||
isolated["result"]["executionContextId"].as_i64().is_some(),
|
||||
"createIsolatedWorld should resolve while popup initial document is paused: {isolated:?}"
|
||||
@@ -947,7 +985,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_221,
|
||||
"id": 260_222,
|
||||
"method": "Fetch.fulfillRequest",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
@@ -960,7 +998,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(260_221, json!({}), Some(popup_session_id));
|
||||
ctx.expect_result(260_222, json!({}), Some(popup_session_id));
|
||||
crate::testing::wait_until_scheduler_message(ctx, "resumed popup load lifecycle", |message| {
|
||||
message["method"] == json!("Page.loadEventFired")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
@@ -977,7 +1015,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 260_222,
|
||||
"id": 260_223,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": popup_session_id,
|
||||
"params": {
|
||||
@@ -987,7 +1025,7 @@ async fn run_waiting_popup_initial_document_after_resume(
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
take_response_by_id(ctx, 260_222)["result"]["result"]["value"],
|
||||
take_response_by_id(ctx, 260_223)["result"]["result"]["value"],
|
||||
"routed-popup"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,25 @@ async fn arm_popup_route(
|
||||
base: u64,
|
||||
popup_target_id: &str,
|
||||
popup_session_id: &str,
|
||||
popup_url: &str,
|
||||
) {
|
||||
assert!(
|
||||
ctx.conn
|
||||
.target_has_waiting_for_debugger_session(popup_target_id),
|
||||
"the auto-attached popup session must own the debugger gate",
|
||||
);
|
||||
let initial_url = ctx
|
||||
.conn
|
||||
.browser_contexts()
|
||||
.find_map(|browser_context| {
|
||||
loaded_page_for_target(browser_context, popup_target_id)
|
||||
.map(|page| page.final_url().to_string())
|
||||
})
|
||||
.expect("popup initial document");
|
||||
assert_eq!(
|
||||
initial_url, "about:blank",
|
||||
"the popup target URL must remain gated until debugger resume",
|
||||
);
|
||||
ctx.process_async(json!({
|
||||
"id": base,
|
||||
"method": "Page.enable",
|
||||
@@ -114,7 +132,23 @@ async fn arm_popup_route(
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(base + 2, json!({}), Some(popup_session_id));
|
||||
|
||||
let fetch_snapshot = ctx
|
||||
.conn
|
||||
.target_fetch_subresource_interception_snapshot_for_session_owner(Some(popup_session_id))
|
||||
.expect("popup target Fetch configuration");
|
||||
let matching_sessions = fetch_snapshot.matching_request_stage_pause_sessions(
|
||||
Some(popup_session_id),
|
||||
crate::devtools_runtime::DevToolsNetworkResourceType::Document,
|
||||
&url::Url::parse(popup_url).expect("popup URL"),
|
||||
);
|
||||
assert_eq!(
|
||||
matching_sessions
|
||||
.iter()
|
||||
.map(|session| session.session_id.as_deref())
|
||||
.collect::<Vec<_>>(),
|
||||
[Some(popup_session_id)],
|
||||
"Fetch.enable must commit the document pattern to the popup target before resume",
|
||||
);
|
||||
ctx.process_async(json!({
|
||||
"id": base + 3,
|
||||
"method": "Runtime.runIfWaitingForDebugger",
|
||||
@@ -122,6 +156,11 @@ async fn arm_popup_route(
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(base + 3, json!({}), Some(popup_session_id));
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.target_has_waiting_for_debugger_session(popup_target_id),
|
||||
"runIfWaitingForDebugger must release the popup session's debugger barrier",
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": base + 4,
|
||||
@@ -149,6 +188,33 @@ async fn fulfill_popup_document_and_evaluate(
|
||||
popup_url: &str,
|
||||
expected_text: &str,
|
||||
) {
|
||||
let fetch_snapshot = ctx
|
||||
.conn
|
||||
.target_fetch_subresource_interception_snapshot_for_target(popup_target_id)
|
||||
.expect("popup target Fetch configuration after debugger resume");
|
||||
let matching_sessions = fetch_snapshot.matching_request_stage_pause_sessions(
|
||||
Some(popup_session_id),
|
||||
crate::devtools_runtime::DevToolsNetworkResourceType::Document,
|
||||
&url::Url::parse(popup_url).expect("popup URL"),
|
||||
);
|
||||
assert_eq!(
|
||||
matching_sessions
|
||||
.iter()
|
||||
.map(|session| session.session_id.as_deref())
|
||||
.collect::<Vec<_>>(),
|
||||
[Some(popup_session_id)],
|
||||
"popup activation and debugger resume must preserve target-owned Fetch configuration",
|
||||
);
|
||||
crate::testing::wait_until_scheduler_message(
|
||||
ctx,
|
||||
"debugger-resumed popup document request",
|
||||
|message| {
|
||||
message["method"] == json!("Fetch.requestPaused")
|
||||
&& message["sessionId"] == json!(popup_session_id)
|
||||
&& message["params"]["resourceType"] == json!("Document")
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let paused = ctx
|
||||
.sent
|
||||
.iter()
|
||||
@@ -524,6 +590,7 @@ async fn rust_cdp_playwright_auxiliary_session_network_event_contract() {
|
||||
async fn rust_cdp_playwright_multi_context_popup_route_and_evaluate_contract() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
let mut ctx = TestContext::new();
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
let first = attached_smoke_session(&mut ctx, 87_000).await;
|
||||
let second = attached_smoke_session(&mut ctx, 87_100).await;
|
||||
assert_ne!(first.browser_context_id, second.browser_context_id);
|
||||
@@ -540,6 +607,7 @@ async fn rust_cdp_playwright_multi_context_popup_route_and_evaluate_contract() {
|
||||
87_210,
|
||||
&first_popup_target_id,
|
||||
&first_popup_session_id,
|
||||
&first_popup_url,
|
||||
)
|
||||
.await;
|
||||
fulfill_popup_document_and_evaluate(
|
||||
@@ -562,6 +630,7 @@ async fn rust_cdp_playwright_multi_context_popup_route_and_evaluate_contract() {
|
||||
87_310,
|
||||
&second_popup_target_id,
|
||||
&second_popup_session_id,
|
||||
&second_popup_url,
|
||||
)
|
||||
.await;
|
||||
fulfill_popup_document_and_evaluate(
|
||||
@@ -588,6 +657,7 @@ async fn rust_cdp_playwright_multi_context_popup_route_and_evaluate_contract() {
|
||||
async fn rust_cdp_playwright_concurrent_popup_routes_keep_their_navigation_owners() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
let mut ctx = TestContext::new();
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
let opener = attached_smoke_session(&mut ctx, 88_000).await;
|
||||
|
||||
set_auto_attach_waiting_for_debugger(&mut ctx, 88_100).await;
|
||||
@@ -602,6 +672,7 @@ async fn rust_cdp_playwright_concurrent_popup_routes_keep_their_navigation_owner
|
||||
88_110,
|
||||
&first_popup_target_id,
|
||||
&first_popup_session_id,
|
||||
&first_popup_url,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -614,6 +685,7 @@ async fn rust_cdp_playwright_concurrent_popup_routes_keep_their_navigation_owner
|
||||
88_210,
|
||||
&second_popup_target_id,
|
||||
&second_popup_session_id,
|
||||
&second_popup_url,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -636,3 +708,213 @@ async fn rust_cdp_playwright_concurrent_popup_routes_keep_their_navigation_owner
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rust_cdp_popup_waits_for_every_debugger_barrier_and_detach_releases_the_last() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
let mut ctx = TestContext::new();
|
||||
ctx.enable_background_navigation_scheduler_for_test();
|
||||
let opener = attached_smoke_session(&mut ctx, 89_000).await;
|
||||
|
||||
set_auto_attach_waiting_for_debugger(&mut ctx, 89_100).await;
|
||||
ctx.process_async(json!({
|
||||
"id": 89_101,
|
||||
"method": "Target.attachToBrowserTarget"
|
||||
}))
|
||||
.await;
|
||||
let browser_attached = ctx.take_first_matching("browser target session", |message| {
|
||||
message["method"] == json!("Target.attachedToTarget")
|
||||
&& message["params"]["targetInfo"]["type"] == json!("browser")
|
||||
});
|
||||
let browser_session_id = browser_attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("browser target session id")
|
||||
.to_owned();
|
||||
ctx.expect_result(89_101, json!({ "sessionId": browser_session_id }), None);
|
||||
ctx.process_async(json!({
|
||||
"id": 89_102,
|
||||
"sessionId": browser_session_id,
|
||||
"method": "Target.setAutoAttach",
|
||||
"params": {
|
||||
"autoAttach": true,
|
||||
"waitForDebuggerOnStart": true,
|
||||
"flatten": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(89_102, json!({}), Some(&browser_session_id));
|
||||
ctx.take_all();
|
||||
|
||||
let popup_url = fixture.url("/plain?popup=two-debugger-barriers");
|
||||
ctx.process_async(json!({
|
||||
"id": 89_103,
|
||||
"method": "Runtime.evaluate",
|
||||
"sessionId": opener.session_id,
|
||||
"params": {
|
||||
"expression": format!("window.open('{popup_url}', '_blank') !== null"),
|
||||
"returnByValue": true
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let evaluated = take_response_by_id(&mut ctx, 89_103);
|
||||
assert_eq!(evaluated["result"]["result"]["value"], true);
|
||||
let created = ctx.take_first_matching("two-owner popup target", |message| {
|
||||
message["method"] == json!("Target.targetCreated")
|
||||
&& message["params"]["targetInfo"]["url"] == json!(popup_url)
|
||||
});
|
||||
let popup_target_id = created["params"]["targetInfo"]["targetId"]
|
||||
.as_str()
|
||||
.expect("popup target id")
|
||||
.to_owned();
|
||||
let root_attached = ctx.take_first_matching("root popup attachment", |message| {
|
||||
message.get("sessionId").is_none()
|
||||
&& message["method"] == json!("Target.attachedToTarget")
|
||||
&& message["params"]["targetInfo"]["targetId"] == json!(popup_target_id)
|
||||
});
|
||||
let browser_owned_attached =
|
||||
ctx.take_first_matching("browser-owned popup attachment", |message| {
|
||||
message["sessionId"] == json!(browser_session_id)
|
||||
&& message["method"] == json!("Target.attachedToTarget")
|
||||
&& message["params"]["targetInfo"]["targetId"] == json!(popup_target_id)
|
||||
});
|
||||
assert_eq!(root_attached["params"]["waitingForDebugger"], true);
|
||||
assert_eq!(browser_owned_attached["params"]["waitingForDebugger"], true);
|
||||
let root_popup_session_id = root_attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("root popup session id")
|
||||
.to_owned();
|
||||
let browser_popup_session_id = browser_owned_attached["params"]["sessionId"]
|
||||
.as_str()
|
||||
.expect("browser-owned popup session id")
|
||||
.to_owned();
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 89_104,
|
||||
"method": "Fetch.enable",
|
||||
"sessionId": root_popup_session_id,
|
||||
"params": {
|
||||
"patterns": [{
|
||||
"urlPattern": "*",
|
||||
"resourceType": "Document",
|
||||
"requestStage": "Request"
|
||||
}]
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(89_104, json!({}), Some(&root_popup_session_id));
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 89_105,
|
||||
"method": "Runtime.runIfWaitingForDebugger",
|
||||
"sessionId": root_popup_session_id
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(89_105, json!({}), Some(&root_popup_session_id));
|
||||
assert!(
|
||||
ctx.conn
|
||||
.target_has_waiting_for_debugger_session(&popup_target_id),
|
||||
"the second inspector session must keep the target behind its debugger barrier"
|
||||
);
|
||||
assert!(
|
||||
!ctx.sent
|
||||
.iter()
|
||||
.any(|message| message["method"] == json!("Fetch.requestPaused")),
|
||||
"one of two waiting sessions must not release the popup navigation: {:?}",
|
||||
ctx.sent
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
"id": 89_106,
|
||||
"method": "Target.detachFromTarget",
|
||||
"params": { "sessionId": browser_popup_session_id }
|
||||
}))
|
||||
.await;
|
||||
ctx.expect_result(89_106, json!({}), None);
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.target_has_waiting_for_debugger_session(&popup_target_id),
|
||||
"detaching the final waiting session must release the target barrier"
|
||||
);
|
||||
|
||||
fulfill_popup_document_and_evaluate(
|
||||
&mut ctx,
|
||||
89_110,
|
||||
&popup_target_id,
|
||||
&root_popup_session_id,
|
||||
&popup_url,
|
||||
"all-debugger-barriers-released",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn queued_popup_navigation_rechecks_a_late_debugger_barrier() {
|
||||
let fixture = SmokeFixtureServer::start().await;
|
||||
let mut ctx = TestContext::new();
|
||||
let opener = attached_smoke_session(&mut ctx, 90_000).await;
|
||||
set_auto_attach_waiting_for_debugger(&mut ctx, 90_100).await;
|
||||
|
||||
let popup_url = fixture.url("/plain?popup=late-debugger-barrier");
|
||||
let (popup_target_id, popup_session_id, browser_context_id) =
|
||||
open_popup_from_session(&mut ctx, 90_101, &opener.session_id, &popup_url).await;
|
||||
let action = crate::conn::PopupTargetNavigationOwnerAction::capture(
|
||||
&ctx.conn,
|
||||
&browser_context_id,
|
||||
&popup_target_id,
|
||||
popup_url,
|
||||
crate::conn::PopupTargetNavigationKind::InitialDocumentAfterDebuggerResume,
|
||||
)
|
||||
.expect("the paused popup should have an exact navigation owner action");
|
||||
|
||||
assert!(
|
||||
ctx.conn
|
||||
.release_waiting_for_debugger_session(Some(&popup_session_id))
|
||||
);
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.target_has_waiting_for_debugger_session(&popup_target_id)
|
||||
);
|
||||
|
||||
let late_session_id = "SID-late-debugger".to_owned();
|
||||
assert!(
|
||||
ctx.conn
|
||||
.prepare_auto_attached_page_session_binding(&popup_target_id, late_session_id.clone(),)
|
||||
);
|
||||
let prepared = ctx.conn.prepare_auto_attach_session_commit(
|
||||
late_session_id,
|
||||
Some(opener.session_id.clone()),
|
||||
true,
|
||||
);
|
||||
let target_info = ctx
|
||||
.conn
|
||||
.browser_context_by_id(&browser_context_id)
|
||||
.and_then(|browser_context| browser_context.devtools_target_info(&popup_target_id))
|
||||
.expect("popup target info");
|
||||
let _ = ctx
|
||||
.conn
|
||||
.commit_prepared_attach_event_plan(crate::conn::PreparedTargetAttach::new(
|
||||
&popup_target_id,
|
||||
target_info,
|
||||
[prepared],
|
||||
));
|
||||
assert!(
|
||||
ctx.conn
|
||||
.target_has_waiting_for_debugger_session(&popup_target_id),
|
||||
"the late session must install a new target barrier before queued work runs",
|
||||
);
|
||||
|
||||
let outcome = complete_popup_target_navigation_owner_action_async(&mut ctx.conn, action).await;
|
||||
assert!(outcome.into_parts().0.is_empty());
|
||||
assert!(
|
||||
!ctx.conn
|
||||
.has_pending_document_navigation_for_session_owner(Some(&popup_session_id)),
|
||||
"queued work must not start the target URL through a newly paused target",
|
||||
);
|
||||
let page_url = ctx
|
||||
.conn
|
||||
.browser_context_by_id(&browser_context_id)
|
||||
.and_then(|browser_context| loaded_page_for_target(browser_context, &popup_target_id))
|
||||
.map(|page| page.final_url().as_str())
|
||||
.expect("popup initial Page");
|
||||
assert_eq!(page_url, "about:blank");
|
||||
}
|
||||
|
||||
@@ -864,11 +864,17 @@ impl TestContext {
|
||||
return false;
|
||||
}
|
||||
CdpCommandTaskStep::Pending(pending) => {
|
||||
let completed = pending.wait().await;
|
||||
step = self
|
||||
.conn
|
||||
.complete_pending_command_dispatch_with_context(completed, command_context)
|
||||
.await;
|
||||
// Keep the test scheduler's pending-command boundary shaped
|
||||
// like production. Some domain completions carry a complete
|
||||
// renderer Page build, so composing both futures inline can
|
||||
// exceed Rust's default test-thread stack before the owner
|
||||
// turn gets a chance to yield.
|
||||
let completed = Box::pin(pending.wait()).await;
|
||||
step = Box::pin(self.conn.complete_pending_command_dispatch_with_context(
|
||||
completed,
|
||||
command_context,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,6 +749,30 @@ impl DocumentLifecycleRecord {
|
||||
true
|
||||
}
|
||||
|
||||
/// Abort the current Document's remaining loading algorithm without
|
||||
/// retiring the Document itself.
|
||||
///
|
||||
/// Blink's `Document::CancelParsing()` moves readiness to `complete` but
|
||||
/// marks the load event as completed instead of dispatching it. Keep the
|
||||
/// same distinction here: queued lifecycle work still names a current
|
||||
/// owner, but none of its old transition tokens remain admissible.
|
||||
pub(super) fn stop_loading_without_load_event(&mut self) -> Option<bool> {
|
||||
let previous_readiness = self.readiness?;
|
||||
let ready_state_changed = previous_readiness != DocumentReadinessState::Complete;
|
||||
|
||||
self.blockers.clear_for_retirement();
|
||||
self.incomplete_child_frames.clear();
|
||||
self.parsing_delay_token = None;
|
||||
self.interactive_transition_token = None;
|
||||
self.domcontentloaded_transition_token = None;
|
||||
self.complete_transition_token = None;
|
||||
self.readiness = Some(DocumentReadinessState::Complete);
|
||||
self.load = DocumentLoadEventProgress::Suppressed;
|
||||
self.child_load_delivery_admission = None;
|
||||
|
||||
Some(ready_state_changed)
|
||||
}
|
||||
|
||||
pub(super) fn complete_transition_is_pending(
|
||||
&self,
|
||||
transition_token: DocumentLoadDelayTokenId,
|
||||
|
||||
@@ -1481,6 +1481,25 @@ impl FrameOwnerStore {
|
||||
})
|
||||
}
|
||||
|
||||
/// Stop the exact current main Document while keeping its Window and
|
||||
/// Document identities live.
|
||||
///
|
||||
/// The returned boolean reports whether the observable ready state still
|
||||
/// needs to transition to `complete`. `None` means the supplied owner is
|
||||
/// stale or no longer owns lifecycle state.
|
||||
pub(crate) fn stop_current_main_document_loading(
|
||||
&mut self,
|
||||
owner: FrameDocumentTaskOwner,
|
||||
) -> Option<bool> {
|
||||
if !self.main_document_task_owner_is_current(owner) {
|
||||
return None;
|
||||
}
|
||||
self.documents
|
||||
.get_mut(&owner.document_id)?
|
||||
.lifecycle_progress
|
||||
.stop_loading_without_load_event()
|
||||
}
|
||||
|
||||
pub(crate) fn apply_current_main_document_complete_transition(
|
||||
&mut self,
|
||||
action: MainDocumentCompleteLifecycleAction,
|
||||
|
||||
@@ -336,6 +336,14 @@ impl JsContextHost {
|
||||
.current_main_document_complete_transition_is_ready(owner)
|
||||
}
|
||||
|
||||
pub(crate) fn stop_current_main_document_loading(
|
||||
&mut self,
|
||||
owner: FrameDocumentTaskOwner,
|
||||
) -> Option<bool> {
|
||||
self.frame_owner_store
|
||||
.stop_current_main_document_loading(owner)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_current_main_document_complete_transition(
|
||||
&mut self,
|
||||
action: MainDocumentCompleteLifecycleAction,
|
||||
|
||||
@@ -187,6 +187,10 @@ impl BrowserResourceRuntime {
|
||||
self.inner.client.cookie_store()
|
||||
}
|
||||
|
||||
pub fn browser_identity(&self) -> &moli_browser_profile::BrowserIdentityProfile {
|
||||
self.inner.client.browser_identity()
|
||||
}
|
||||
|
||||
pub fn matches_fetch_config(&self, config: &FetchConfig) -> bool {
|
||||
self.inner.client.matches_config(config)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,13 @@ impl ResourceRequestClient {
|
||||
self.resource_runtime.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn replace_browser_resource_runtime(
|
||||
&mut self,
|
||||
resource_runtime: BrowserResourceRuntime,
|
||||
) {
|
||||
self.resource_runtime = resource_runtime;
|
||||
}
|
||||
|
||||
pub fn shares_resource_runtime_with(&self, other: &Self) -> bool {
|
||||
self.resource_runtime
|
||||
.shares_state_with(&other.resource_runtime)
|
||||
|
||||
@@ -114,7 +114,11 @@ pub struct RendererPreparedDocumentCommitConfiguration {
|
||||
pub emulated_media: crate::protocol_types::EmulatedMediaOverrides,
|
||||
pub idle_override: Option<crate::protocol_types::EmulatedIdleOverride>,
|
||||
pub viewport_surface: Option<crate::protocol_types::ViewportSurface>,
|
||||
pub browser_resource_runtime: crate::network::BrowserResourceRuntime,
|
||||
pub navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
pub network_offline: bool,
|
||||
pub bypass_service_worker: bool,
|
||||
pub cache_disabled: bool,
|
||||
pub blocked_url_patterns: Vec<String>,
|
||||
pub fetch_subresource_interception_enabled: bool,
|
||||
pub fetch_subresource_interception_resource_type: Option<crate::SubresourceResourceType>,
|
||||
@@ -138,6 +142,7 @@ pub struct RendererCreateHtmlPageRequest {
|
||||
pub response_status: u16,
|
||||
pub response_headers: Vec<(String, String)>,
|
||||
pub loader: ResourceRequestClient,
|
||||
pub navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
pub web_storage: crate::RendererWebStorageHandles,
|
||||
pub final_url: Url,
|
||||
pub html: String,
|
||||
@@ -187,6 +192,7 @@ pub struct RendererCreateStreamingRawPageRequest {
|
||||
pub response_status: u16,
|
||||
pub response_headers: Vec<(String, String)>,
|
||||
pub loader: ResourceRequestClient,
|
||||
pub navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
pub web_storage: crate::RendererWebStorageHandles,
|
||||
pub raw_body: ExternalRawDocumentBodyStream,
|
||||
pub document_start_scripts: Vec<DocumentStartScript>,
|
||||
@@ -2134,6 +2140,7 @@ impl RendererOwnerHandle {
|
||||
response_status,
|
||||
response_headers,
|
||||
loader: loader.clone(),
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
web_storage,
|
||||
final_url,
|
||||
html,
|
||||
@@ -2210,6 +2217,7 @@ impl RendererOwnerHandle {
|
||||
response_status,
|
||||
response_headers,
|
||||
loader: loader.clone(),
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
web_storage,
|
||||
raw_body,
|
||||
document_start_scripts,
|
||||
@@ -6807,6 +6815,7 @@ impl RendererOwnerHandle {
|
||||
response_status,
|
||||
response_headers,
|
||||
loader,
|
||||
navigator_identity,
|
||||
web_storage,
|
||||
final_url,
|
||||
html,
|
||||
@@ -6902,6 +6911,7 @@ impl RendererOwnerHandle {
|
||||
runtime_isolated_worlds,
|
||||
permission_overrides,
|
||||
extra_http_headers,
|
||||
navigator_identity,
|
||||
document_policy_container,
|
||||
document_default_language,
|
||||
document_last_modified,
|
||||
@@ -7119,6 +7129,7 @@ impl RendererOwnerHandle {
|
||||
response_status,
|
||||
response_headers,
|
||||
loader,
|
||||
navigator_identity,
|
||||
web_storage,
|
||||
raw_body,
|
||||
document_start_scripts,
|
||||
@@ -7201,6 +7212,7 @@ impl RendererOwnerHandle {
|
||||
runtime_isolated_worlds,
|
||||
permission_overrides,
|
||||
extra_http_headers,
|
||||
navigator_identity,
|
||||
document_policy_container,
|
||||
document_default_language,
|
||||
document_last_modified,
|
||||
|
||||
@@ -919,7 +919,17 @@ impl RendererOwnerLocalStore {
|
||||
request.emulated_media = configuration.emulated_media;
|
||||
request.idle_override = configuration.idle_override;
|
||||
request.viewport_surface = configuration.viewport_surface;
|
||||
request
|
||||
.loader
|
||||
.replace_browser_resource_runtime(configuration.browser_resource_runtime);
|
||||
request.navigator_identity = configuration.navigator_identity;
|
||||
request.network_offline = configuration.network_offline;
|
||||
request
|
||||
.loader
|
||||
.set_bypass_service_worker(configuration.bypass_service_worker);
|
||||
request
|
||||
.loader
|
||||
.set_cache_disabled(configuration.cache_disabled);
|
||||
request.blocked_url_patterns = configuration.blocked_url_patterns;
|
||||
request.fetch_subresource_interception_enabled =
|
||||
configuration.fetch_subresource_interception_enabled;
|
||||
|
||||
@@ -411,7 +411,7 @@ impl PageVm {
|
||||
),
|
||||
),
|
||||
RendererPageCommand::StopDocumentLifecycle => {
|
||||
self.stop_document_lifecycle();
|
||||
self.stop_document_lifecycle()?;
|
||||
Ok(RendererPageReply::Unit)
|
||||
}
|
||||
RendererPageCommand::SearchTextByLines {
|
||||
@@ -1033,8 +1033,11 @@ impl PageVm {
|
||||
.set_javascript_dialog_handler_enabled(enabled);
|
||||
Ok(RendererPageReply::Unit)
|
||||
}
|
||||
RendererPageCommand::ReplaceBrowserResourceRuntime(resource_runtime) => {
|
||||
self.replace_browser_resource_runtime(&resource_runtime);
|
||||
RendererPageCommand::ReplaceBrowserResourceRuntime {
|
||||
resource_runtime,
|
||||
navigator_identity,
|
||||
} => {
|
||||
self.replace_browser_resource_runtime(&resource_runtime, &navigator_identity);
|
||||
Ok(RendererPageReply::Unit)
|
||||
}
|
||||
RendererPageCommand::RetireDocumentResourceAuthorities => {
|
||||
|
||||
@@ -219,6 +219,7 @@ impl PageVm {
|
||||
pub(crate) fn replace_browser_resource_runtime(
|
||||
&mut self,
|
||||
resource_runtime: &crate::network::BrowserResourceRuntime,
|
||||
navigator_identity: &moli_browser_profile::BrowserIdentityProfile,
|
||||
) {
|
||||
// Replacing a browser/network backend must not replace the live
|
||||
// target's Page policy. Pair the new backend with the exact policy
|
||||
@@ -230,8 +231,12 @@ impl PageVm {
|
||||
);
|
||||
let document_loader = self
|
||||
.vm_mut()
|
||||
.replace_document_resource_runtime(&page_loader);
|
||||
.replace_document_resource_runtime_with_navigator_identity(
|
||||
&page_loader,
|
||||
navigator_identity,
|
||||
);
|
||||
self.request_client = document_loader.request_client().clone();
|
||||
self.navigator_identity = navigator_identity.clone();
|
||||
}
|
||||
|
||||
pub(crate) fn retire_document_resource_authorities(&mut self) {
|
||||
|
||||
@@ -5201,7 +5201,10 @@ pub enum RendererPageCommand {
|
||||
resource_type: Option<crate::SubresourceResourceType>,
|
||||
},
|
||||
SetJavaScriptDialogHandlerEnabled(bool),
|
||||
ReplaceBrowserResourceRuntime(crate::network::BrowserResourceRuntime),
|
||||
ReplaceBrowserResourceRuntime {
|
||||
resource_runtime: crate::network::BrowserResourceRuntime,
|
||||
navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
},
|
||||
RetireDocumentResourceAuthorities,
|
||||
ApplyDocumentCookieFacadeOverrides(moli_cookie_jar::BrowserCookieFacadeOverrides),
|
||||
ClearDocumentCookieFacadeOverrides,
|
||||
|
||||
@@ -1182,6 +1182,7 @@ impl PageVm {
|
||||
runtime_isolated_worlds: self.runtime_isolated_worlds.clone(),
|
||||
permission_overrides: self.permission_overrides.clone(),
|
||||
extra_http_headers: self.extra_http_headers.clone(),
|
||||
navigator_identity: self.navigator_identity.clone(),
|
||||
document_policy_container: crate::document_runtime::DocumentPolicyContainer {
|
||||
document_content_security_policies: self.vm().document_content_security_policies(),
|
||||
..Default::default()
|
||||
|
||||
@@ -1073,6 +1073,7 @@ pub(crate) struct PageVmEnvConfig {
|
||||
pub(crate) runtime_isolated_worlds: Vec<crate::protocol_types::RuntimeIsolatedWorldDefinition>,
|
||||
pub(crate) permission_overrides: Vec<crate::protocol_types::PermissionOverrideRegistration>,
|
||||
pub(crate) extra_http_headers: Vec<(String, String)>,
|
||||
pub(crate) navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
pub(crate) document_policy_container: crate::document_runtime::DocumentPolicyContainer,
|
||||
pub(crate) document_default_language: Option<String>,
|
||||
pub(crate) document_last_modified: Option<f64>,
|
||||
@@ -1588,6 +1589,10 @@ pub(crate) struct PageVm {
|
||||
/// committed-Document loader lives only in `ScriptVm`'s exact-owner
|
||||
/// registry and may change across `document.open()` or navigation.
|
||||
pub(super) request_client: ResourceRequestClient,
|
||||
/// Identity exposed by the current Document's Navigator. This is distinct
|
||||
/// from the request transport identity when multiple DevTools sessions
|
||||
/// contribute Emulation overrides.
|
||||
pub(super) navigator_identity: moli_browser_profile::BrowserIdentityProfile,
|
||||
pub(super) runtime_isolated_worlds: Vec<crate::protocol_types::RuntimeIsolatedWorldDefinition>,
|
||||
pub(super) permission_overrides: Vec<crate::protocol_types::PermissionOverrideRegistration>,
|
||||
pub(super) document_start_scripts: Vec<DocumentStartScript>,
|
||||
@@ -1851,11 +1856,12 @@ impl PageVm {
|
||||
self.document_lifecycle.drain_live_events()
|
||||
}
|
||||
|
||||
pub(super) fn stop_document_lifecycle(&self) {
|
||||
pub(super) fn stop_document_lifecycle(&mut self) -> Result<()> {
|
||||
let _ = self.document_lifecycle.request_termination(
|
||||
self.document_lifecycle.identity(),
|
||||
RendererDocumentTerminationReason::Stopped,
|
||||
);
|
||||
self.vm_mut().stop_current_main_document_loading()
|
||||
}
|
||||
|
||||
pub(crate) fn document_lifecycle_wait_outcome(
|
||||
@@ -4262,6 +4268,7 @@ impl PageVm {
|
||||
env.reserved_service_worker_client_id,
|
||||
)?;
|
||||
let mut vm = vm_bootstrap.finish()?;
|
||||
vm.set_document_navigator_identity(&env.navigator_identity);
|
||||
vm.set_layout_policy(env.layout_policy);
|
||||
vm.install_page_task_capabilities(page_task_capabilities);
|
||||
vm.set_root_document_lifecycle(document_lifecycle.clone());
|
||||
@@ -4290,6 +4297,7 @@ impl PageVm {
|
||||
next_module_script_evaluation_reaction_id: 0,
|
||||
target_stage: PageVmInitStage::Load,
|
||||
request_client: document_loader.request_client().clone(),
|
||||
navigator_identity: env.navigator_identity.clone(),
|
||||
runtime_isolated_worlds: env.runtime_isolated_worlds.clone(),
|
||||
permission_overrides: env.permission_overrides.clone(),
|
||||
document_start_scripts: env.document_start_scripts.clone(),
|
||||
|
||||
@@ -662,6 +662,7 @@ fn minimal_test_page_vm_env_config() -> PageVmEnvConfig {
|
||||
runtime_isolated_worlds: Vec::new(),
|
||||
permission_overrides: Vec::new(),
|
||||
extra_http_headers: Vec::new(),
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
|
||||
@@ -9,7 +9,8 @@ use super::*;
|
||||
use crate::{
|
||||
RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity,
|
||||
RendererDocumentLifecycleMilestone, RendererDocumentLifecycleWaitOutcome,
|
||||
RendererDocumentTerminationReason, RendererLifecycleStartReason, RendererPageState,
|
||||
RendererDocumentTerminationReason, RendererLifecycleStartReason, RendererPageReply,
|
||||
RendererPageState,
|
||||
page_task_queue::PostParseLifecycleWork,
|
||||
runtime::document_lifecycle_turn::DocumentLifecycleNavigationTiming,
|
||||
script_vm::{
|
||||
@@ -1408,6 +1409,116 @@ fn main_document_lifecycle_coordinator_preserves_applied_callback_facts() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_loading_completes_readiness_without_dispatching_window_load() {
|
||||
run_page_vm_local_runtime_async_test("page-vm-stop-loading-lifecycle", || async move {
|
||||
let mut page_vm = test_page_vm();
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
local_executor
|
||||
.run(async move {
|
||||
let _initial = page_vm.take_page_creation_artifacts();
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__stopLoadingLifecycleEvents = [];
|
||||
document.addEventListener("readystatechange", () => {
|
||||
__stopLoadingLifecycleEvents.push(`readystatechange:${document.readyState}`);
|
||||
queueMicrotask(() => {
|
||||
__stopLoadingLifecycleEvents.push(`microtask:${document.readyState}`);
|
||||
});
|
||||
});
|
||||
window.addEventListener("load", () => {
|
||||
__stopLoadingLifecycleEvents.push("load");
|
||||
});
|
||||
window.addEventListener("pageshow", () => {
|
||||
__stopLoadingLifecycleEvents.push("pageshow");
|
||||
});
|
||||
"installed"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let owner = page_vm
|
||||
.vm()
|
||||
.current_main_document_task_owner()
|
||||
.expect("stop-loading fixture requires a current Document owner");
|
||||
let interactive = page_vm
|
||||
.vm_mut()
|
||||
.finish_current_main_document_parsing(owner)
|
||||
.expect("parser completion should prepare interactive work");
|
||||
execute_main_document_lifecycle_on_owner_local_task(
|
||||
&mut page_vm,
|
||||
MainDocumentLifecycleBody::Interactive(interactive),
|
||||
)
|
||||
.await?;
|
||||
execute_main_document_lifecycle_on_owner_local_task(
|
||||
&mut page_vm,
|
||||
MainDocumentLifecycleBody::DomContentLoaded { owner },
|
||||
)
|
||||
.await?;
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.eval("__stopLoadingLifecycleEvents.length = 0; 'cleared'")?;
|
||||
|
||||
let reply = page_vm
|
||||
.dispatch_renderer_page_command_async(
|
||||
RendererPageCommand::StopDocumentLifecycle,
|
||||
)
|
||||
.await?;
|
||||
assert!(matches!(reply, RendererPageReply::Unit));
|
||||
assert_eq!(
|
||||
page_vm.vm_mut().eval(
|
||||
"`${document.readyState}|${__stopLoadingLifecycleEvents.join(',')}`",
|
||||
)?,
|
||||
"complete|readystatechange:complete,microtask:complete",
|
||||
"stopLoading must synchronously complete readiness and its task checkpoint",
|
||||
);
|
||||
|
||||
let snapshot = page_vm.document_lifecycle.current_snapshot();
|
||||
assert!(snapshot.dom_content_loaded.is_some());
|
||||
assert!(snapshot.load.is_none());
|
||||
assert!(matches!(
|
||||
snapshot.terminated,
|
||||
Some(crate::RendererLifecycleTerminationStamp {
|
||||
reason: RendererDocumentTerminationReason::Stopped,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(
|
||||
page_vm.vm().current_main_document_task_owner(),
|
||||
Some(owner),
|
||||
"stopping loading must not replace the current Document owner",
|
||||
);
|
||||
|
||||
let run = execute_main_document_lifecycle_on_owner_local_task(
|
||||
&mut page_vm,
|
||||
MainDocumentLifecycleBody::WindowLoad { owner },
|
||||
)
|
||||
.await?;
|
||||
assert!(matches!(
|
||||
run.completion.target(),
|
||||
MainDocumentLifecycleTargetEffect::NotApplied {
|
||||
reason: MainDocumentLifecycleTargetRejection::TransitionRejected,
|
||||
current_owner: Some(current_owner),
|
||||
} if current_owner == owner
|
||||
));
|
||||
assert_eq!(
|
||||
run.completion.callback(),
|
||||
MainDocumentLifecycleCallbackEffect::NotEntered,
|
||||
);
|
||||
assert_eq!(
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.eval("__stopLoadingLifecycleEvents.join(',')")?,
|
||||
"readystatechange:complete,microtask:complete",
|
||||
"retired lifecycle work must not dispatch load or pageshow",
|
||||
);
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("stop-loading lifecycle should reconcile");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_main_lifecycle_body_leaves_listener_reaction_for_its_typed_checkpoint() {
|
||||
run_page_vm_local_runtime_async_test(
|
||||
|
||||
@@ -1795,6 +1795,7 @@ fn test_page_vm_with_loader_dom_host_hooks_and_response_referrer_policy(
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers,
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
document_policy_container: crate::document_runtime::DocumentPolicyContainer {
|
||||
referrer_policy: response_referrer_policy,
|
||||
..Default::default()
|
||||
@@ -3121,6 +3122,7 @@ fn default_runtime_hooks_reject_direct_no_owner_page_vm_construction() {
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: Vec::new(),
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
|
||||
@@ -2247,6 +2247,7 @@ document.body.setAttribute('data-error-state', [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -5979,6 +5980,7 @@ globalThis.__outerDocumentWriteScriptContinued = true;
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -14383,6 +14385,7 @@ document.body.setAttribute('data-result', [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -14563,6 +14566,7 @@ document.body.setAttribute('data-result', [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -14792,6 +14796,7 @@ document.body.setAttribute('data-result', [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -16790,6 +16795,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -16918,6 +16924,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -17056,6 +17063,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -17229,6 +17237,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -17552,6 +17561,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -17722,6 +17732,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
@@ -17810,6 +17821,7 @@ document.body.setAttribute("data-range", [
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
|
||||
@@ -940,6 +940,7 @@ mod tests {
|
||||
runtime_isolated_worlds: vec![],
|
||||
permission_overrides: vec![],
|
||||
extra_http_headers: vec![],
|
||||
navigator_identity: Default::default(),
|
||||
document_policy_container: Default::default(),
|
||||
document_default_language: None,
|
||||
document_last_modified: None,
|
||||
|
||||
@@ -1510,7 +1510,11 @@ async fn streaming_unstyled_xml_converts_live_document_before_domcontentloaded()
|
||||
emulated_media: Default::default(),
|
||||
idle_override: None,
|
||||
viewport_surface: None,
|
||||
browser_resource_runtime: loader.browser_resource_runtime(),
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
network_offline: false,
|
||||
bypass_service_worker: false,
|
||||
cache_disabled: false,
|
||||
blocked_url_patterns: Vec::new(),
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
@@ -1643,7 +1647,11 @@ async fn prepared_streaming_xml_document_waits_for_permit_and_uses_latest_config
|
||||
emulated_media: Default::default(),
|
||||
idle_override: None,
|
||||
viewport_surface: None,
|
||||
browser_resource_runtime: loader.browser_resource_runtime(),
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
network_offline: false,
|
||||
bypass_service_worker: false,
|
||||
cache_disabled: false,
|
||||
blocked_url_patterns: Vec::new(),
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
@@ -2616,7 +2624,11 @@ globalThis.__preparedCommitObserved = JSON.stringify([
|
||||
emulated_media: Default::default(),
|
||||
idle_override: None,
|
||||
viewport_surface: None,
|
||||
browser_resource_runtime: loader.browser_resource_runtime(),
|
||||
navigator_identity: loader.browser_identity().clone(),
|
||||
network_offline: false,
|
||||
bypass_service_worker: false,
|
||||
cache_disabled: false,
|
||||
blocked_url_patterns: Vec::new(),
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
|
||||
@@ -4730,9 +4730,22 @@ impl ScriptVm {
|
||||
/// The Document authority is installed before realm bootstrap and remains
|
||||
/// stable here. Existing leases keep their captured request client; only
|
||||
/// subsequently registered loads observe this replacement transport.
|
||||
#[cfg(test)]
|
||||
pub(super) fn replace_document_resource_runtime(
|
||||
&mut self,
|
||||
request_client: &ResourceRequestClient,
|
||||
) -> DocumentResourceLoader {
|
||||
let navigator_identity = request_client.browser_identity().clone();
|
||||
self.replace_document_resource_runtime_with_navigator_identity(
|
||||
request_client,
|
||||
&navigator_identity,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn replace_document_resource_runtime_with_navigator_identity(
|
||||
&mut self,
|
||||
request_client: &ResourceRequestClient,
|
||||
navigator_identity: &moli_browser_profile::BrowserIdentityProfile,
|
||||
) -> DocumentResourceLoader {
|
||||
let current = self
|
||||
.current_main_document_resource_loader()
|
||||
@@ -4743,12 +4756,21 @@ impl ScriptVm {
|
||||
.replace_main_document_resource_transport(&document_loader);
|
||||
self.document_runtime
|
||||
.set_cookie_store(document_loader.request_client().cookie_store());
|
||||
let identity = document_loader.request_client().browser_identity().clone();
|
||||
let _ = self
|
||||
.with_default_context_scope(|scope, _| set_window_navigator_identity(scope, &identity));
|
||||
let _ = self.with_default_context_scope(|scope, _| {
|
||||
set_window_navigator_identity(scope, navigator_identity)
|
||||
});
|
||||
document_loader
|
||||
}
|
||||
|
||||
pub(super) fn set_document_navigator_identity(
|
||||
&mut self,
|
||||
navigator_identity: &moli_browser_profile::BrowserIdentityProfile,
|
||||
) {
|
||||
let _ = self.with_default_context_scope(|scope, _| {
|
||||
set_window_navigator_identity(scope, navigator_identity)
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn set_web_storage_handles(&mut self, handles: &crate::RendererWebStorageHandles) {
|
||||
self._context_host
|
||||
.borrow_mut()
|
||||
|
||||
@@ -19,6 +19,31 @@ use crate::dom::native::DocumentReadyState;
|
||||
use crate::frame_owner_model::{FrameDocumentTaskOwner, MainDocumentInteractiveLifecycleAction};
|
||||
|
||||
impl ScriptVm {
|
||||
/// Apply Blink's `CancelParsing()` readiness boundary for Page.stopLoading.
|
||||
///
|
||||
/// This is deliberately separate from the ordinary Window-load body: the
|
||||
/// Document remains current and receives `readystatechange`, while the
|
||||
/// frame-owner lifecycle suppresses all later DCL/load delivery.
|
||||
pub(crate) fn stop_current_main_document_loading(&mut self) -> anyhow::Result<()> {
|
||||
let Some(owner) = self.current_main_document_task_owner() else {
|
||||
return Ok(());
|
||||
};
|
||||
let ready_state_changed = self
|
||||
._context_host
|
||||
.borrow_mut()
|
||||
.stop_current_main_document_loading(owner)
|
||||
.unwrap_or(false);
|
||||
|
||||
if ready_state_changed {
|
||||
self.set_document_ready_state(DocumentReadyState::Complete)?;
|
||||
let _dispatch =
|
||||
self.dispatch_document_lifecycle_event_body_best_effort("readystatechange");
|
||||
}
|
||||
|
||||
let checkpoint = self.finish_main_document_lifecycle_checkpoint();
|
||||
self.finish_main_document_lifecycle_turn(checkpoint)
|
||||
}
|
||||
|
||||
/// Start one lifecycle body without performing any task-end or internal
|
||||
/// lifecycle checkpoint.
|
||||
pub(crate) fn begin_main_document_lifecycle_body(
|
||||
|
||||
@@ -26,6 +26,41 @@ async fn initial_navigator_materializes_from_the_committed_document_authority_se
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_document_transport_keeps_wire_and_navigator_identities_distinct() {
|
||||
const WIRE_USER_AGENT: &str = "Moli-Wire-Identity/1.0";
|
||||
const NAVIGATOR_USER_AGENT: &str = "Moli-Navigator-Identity/1.0";
|
||||
|
||||
let mut vm = new_storage_test_vm("https://navigator-transport-split.test/");
|
||||
let mut fetch_config = moli_fetch::FetchConfig::default();
|
||||
fetch_config.set_user_agent(WIRE_USER_AGENT);
|
||||
let loader = ResourceRequestClient::new(&fetch_config).expect("loader");
|
||||
let navigator_identity =
|
||||
moli_browser_profile::BrowserIdentityProfile::new(NAVIGATOR_USER_AGENT, "fr-FR");
|
||||
|
||||
let document_loader =
|
||||
vm.replace_document_resource_runtime_with_navigator_identity(&loader, &navigator_identity);
|
||||
|
||||
assert_eq!(
|
||||
document_loader
|
||||
.request_client()
|
||||
.browser_identity()
|
||||
.user_agent(),
|
||||
WIRE_USER_AGENT,
|
||||
"the replacement transport must keep the browser-side request identity"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("navigator.userAgent")
|
||||
.expect("Navigator should expose the renderer identity"),
|
||||
NAVIGATOR_USER_AGENT
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("navigator.language")
|
||||
.expect("Navigator language should expose the renderer identity"),
|
||||
"fr-FR"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn navigator_identity_seed_keeps_window_metadata_and_network_profile_coherent() {
|
||||
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.1.2.3 Safari/537.36";
|
||||
|
||||
Reference in New Issue
Block a user