mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 00:00:55 +00:00
1480 lines
54 KiB
Rust
1480 lines
54 KiB
Rust
use moli_core::{
|
|
PageId, RendererDocumentLifecycleIdentity, RendererOutputCursor, RendererOutputFence,
|
|
RendererOutputItem, RendererOutputPublication, RendererOutputPublicationOrdering,
|
|
RendererOutputRecord, RendererOutputStreamControl, RendererOutputStreamIdentity,
|
|
RendererOutputTransportMessage, RendererProtocolObservation,
|
|
RendererRuntimeInspectorAsyncCompletion,
|
|
page::{
|
|
RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind,
|
|
RendererDocumentLifecycleMilestone, RendererDocumentToken, RendererFrameToken,
|
|
RendererLifecycleEpoch,
|
|
},
|
|
};
|
|
use moli_protocol::{
|
|
BackgroundProtocolEvent, CdpConnection, CdpSchedulerEvent,
|
|
DeferredMainDocumentLoadCompletionOutputAction,
|
|
DeferredMainDocumentLoadCompletionOutputInterest, DeferredMainDocumentLoadPredecessorCandidate,
|
|
ProtocolSchedulerWork,
|
|
conn::RuntimeInspectorResponseReady,
|
|
devtools_runtime::{
|
|
AutomationEvent, BrowserDownloadWillBeginEvent, DevToolsCommand, DevToolsCommandContext,
|
|
DevToolsCommandResult, DevToolsCreateTargetCommand, DevToolsFrameId, DevToolsLoaderId,
|
|
DevToolsNavigationWait, DevToolsNetworkResourceType, DevToolsProtocol, DevToolsRequestId,
|
|
DevToolsSessionId, DevToolsTargetId, NavigationFrameEvent, NavigationFrameEventKind,
|
|
NetworkRequestEvent,
|
|
},
|
|
test_support::{
|
|
arm_background_navigation_request, arm_background_navigation_request_for_target,
|
|
deferred_main_document_load_observation_id, deferred_main_document_load_output_interest,
|
|
root_frame_stopped_loading_work as make_root_frame_stopped_loading_work,
|
|
root_frame_stopped_loading_work_for_target, settle_background_navigation_request,
|
|
},
|
|
};
|
|
use serde_json::json;
|
|
|
|
use super::{
|
|
CdpScheduler, ForegroundNavigationNetworkBarrier, ProtocolOutputSequence,
|
|
ProtocolSchedulerResidence, ProtocolSchedulerStep, SchedulerQueues,
|
|
devtools_navigation_lifecycle_milestone, drain_pending_background_events,
|
|
next_page_screencast_deadline, page_screencast_interval,
|
|
};
|
|
|
|
#[test]
|
|
fn screencast_deadlines_are_one_hz_downsampled_without_catch_up() {
|
|
let started = tokio::time::Instant::now();
|
|
assert_eq!(
|
|
page_screencast_interval(1),
|
|
std::time::Duration::from_secs(1)
|
|
);
|
|
assert_eq!(
|
|
page_screencast_interval(3),
|
|
std::time::Duration::from_secs(3)
|
|
);
|
|
|
|
let completion = started + std::time::Duration::from_secs(10);
|
|
assert_eq!(
|
|
next_page_screencast_deadline(completion, page_screencast_interval(1)),
|
|
started + std::time::Duration::from_secs(11),
|
|
"an overdue capture schedules from completion instead of catching up"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn navigation_wait_maps_only_observable_document_milestones() {
|
|
use moli_core::page::RendererDocumentLifecycleMilestone;
|
|
|
|
assert_eq!(
|
|
devtools_navigation_lifecycle_milestone(Some(DevToolsNavigationWait::DomContentLoaded)),
|
|
Some(RendererDocumentLifecycleMilestone::DomContentLoaded)
|
|
);
|
|
assert_eq!(
|
|
devtools_navigation_lifecycle_milestone(Some(DevToolsNavigationWait::Load)),
|
|
Some(RendererDocumentLifecycleMilestone::Load)
|
|
);
|
|
assert_eq!(
|
|
devtools_navigation_lifecycle_milestone(Some(DevToolsNavigationWait::None)),
|
|
None
|
|
);
|
|
assert_eq!(devtools_navigation_lifecycle_milestone(None), None);
|
|
}
|
|
|
|
fn navigation_started_automation_event() -> AutomationEvent {
|
|
AutomationEvent::NavigationFrame(NavigationFrameEvent {
|
|
target_id: DevToolsTargetId::from("TID-output"),
|
|
frame_id: DevToolsFrameId::from("TID-output"),
|
|
parent_frame_id: None,
|
|
loader_id: Some(DevToolsLoaderId::from("LOADER-output")),
|
|
url: "https://example.test/output".to_owned(),
|
|
kind: NavigationFrameEventKind::StartedNavigating,
|
|
frame_name: None,
|
|
security_origin: None,
|
|
secure_context_type: None,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_output_sequence_preserves_background_event_sidecar() {
|
|
let message = json!({
|
|
"method": "Page.frameStartedNavigating",
|
|
"params": {
|
|
"frameId": "TID-output",
|
|
"loaderId": "LOADER-output",
|
|
"url": "https://example.test/output",
|
|
"navigationType": "differentDocument"
|
|
}
|
|
});
|
|
let automation_event = navigation_started_automation_event();
|
|
|
|
let output = ProtocolOutputSequence::from_background_event(
|
|
BackgroundProtocolEvent::immediate_automation_event(
|
|
message.clone(),
|
|
automation_event.clone(),
|
|
),
|
|
);
|
|
let mut events = output.into_background_events();
|
|
let (actual_message, actual_automation_event) = events
|
|
.pop()
|
|
.expect("protocol output should keep event")
|
|
.into_parts();
|
|
|
|
assert_eq!(actual_message, message);
|
|
assert_eq!(actual_automation_event, Some(automation_event));
|
|
}
|
|
|
|
#[test]
|
|
fn load_ordering_splits_network_observations_from_page_side_effects() {
|
|
let network = network_response_event(DevToolsNetworkResourceType::EventSource, "REQ-sse");
|
|
let page_effect = download_will_begin_event("FRAME-nav", "download-after-load");
|
|
let output =
|
|
ProtocolOutputSequence::from_background_events(vec![network.clone(), page_effect.clone()]);
|
|
|
|
let (load_independent, load_ordered) = output.split_network_observations();
|
|
|
|
assert_eq!(load_independent.into_background_events(), vec![network]);
|
|
assert_eq!(load_ordered.into_background_events(), vec![page_effect]);
|
|
}
|
|
|
|
fn download_will_begin_event(frame_id: &str, guid: &str) -> BackgroundProtocolEvent {
|
|
BackgroundProtocolEvent::immediate_automation_event(
|
|
json!({
|
|
"method": "Browser.downloadWillBegin",
|
|
"params": {
|
|
"frameId": frame_id,
|
|
"guid": guid,
|
|
"url": "https://example.test/download",
|
|
"suggestedFilename": "download.bin"
|
|
}
|
|
}),
|
|
AutomationEvent::BrowserDownloadWillBegin(BrowserDownloadWillBeginEvent {
|
|
frame_id: DevToolsFrameId::from(frame_id),
|
|
guid: guid.to_owned(),
|
|
url: "https://example.test/download".to_owned(),
|
|
suggested_filename: "download.bin".to_owned(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn navigation_download_observation_ignores_output_before_command_boundary() {
|
|
let mut output = ProtocolOutputSequence::from_background_event(download_will_begin_event(
|
|
"FRAME-nav",
|
|
"old-download",
|
|
));
|
|
let command_output_start = output.len();
|
|
|
|
assert!(!output.contains_download_start_for_frame_since("FRAME-nav", command_output_start,));
|
|
|
|
output.append(ProtocolOutputSequence::from_background_event(
|
|
download_will_begin_event("FRAME-nav", "current-download"),
|
|
));
|
|
assert!(output.contains_download_start_for_frame_since("FRAME-nav", command_output_start,));
|
|
}
|
|
|
|
#[test]
|
|
fn drain_pending_background_events_preserves_typed_sidecar() {
|
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
|
let message = json!({
|
|
"method": "Page.frameStartedNavigating",
|
|
"params": {
|
|
"frameId": "TID-output",
|
|
"loaderId": "LOADER-output",
|
|
"url": "https://example.test/output",
|
|
"navigationType": "differentDocument"
|
|
}
|
|
});
|
|
let automation_event = navigation_started_automation_event();
|
|
tx.send(BackgroundProtocolEvent::immediate_automation_event(
|
|
message.clone(),
|
|
automation_event.clone(),
|
|
))
|
|
.expect("background event receiver should be open");
|
|
|
|
let output = drain_pending_background_events(&mut rx);
|
|
let mut events = output.into_background_events();
|
|
let (actual_message, actual_automation_event) = events
|
|
.pop()
|
|
.expect("renderer publication ingress should preserve event")
|
|
.into_parts();
|
|
|
|
assert_eq!(actual_message, message);
|
|
assert_eq!(actual_automation_event, Some(automation_event));
|
|
}
|
|
|
|
fn runtime_response_ready_event(command_id: u64) -> BackgroundProtocolEvent {
|
|
BackgroundProtocolEvent::runtime_inspector_response_ready(RuntimeInspectorResponseReady::new(
|
|
command_id,
|
|
None,
|
|
Ok(
|
|
RendererRuntimeInspectorAsyncCompletion::from_protocol_message(
|
|
i32::try_from(command_id).expect("test command id should fit i32"),
|
|
json!({
|
|
"id": command_id,
|
|
"result": {}
|
|
}),
|
|
),
|
|
),
|
|
))
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_output_loose_response_take_ignores_typed_runtime_response() {
|
|
let mut output =
|
|
ProtocolOutputSequence::from_background_event(runtime_response_ready_event(42));
|
|
|
|
let events = output.take_protocol_events_with_id(42);
|
|
|
|
assert!(
|
|
events.is_empty(),
|
|
"typed runtime responses must not be flattened by loose protocol response scans"
|
|
);
|
|
let Some((prefix, response)) = output.split_next_runtime_response_ready() else {
|
|
panic!("typed runtime response should remain available for typed routing");
|
|
};
|
|
assert!(prefix.is_empty());
|
|
assert_eq!(response.command_id(), 42);
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_output_loose_response_split_ignores_typed_runtime_response() {
|
|
let mut output = ProtocolOutputSequence::from_background_events(vec![
|
|
runtime_response_ready_event(42),
|
|
BackgroundProtocolEvent::immediate(json!({
|
|
"id": 7,
|
|
"result": {}
|
|
})),
|
|
]);
|
|
|
|
assert!(
|
|
output
|
|
.split_next_protocol_message_with_any_id(&[42])
|
|
.is_none(),
|
|
"typed runtime responses must not satisfy loose protocol response lookup"
|
|
);
|
|
let Some((mut prefix, command_id, event)) =
|
|
output.split_next_protocol_message_with_any_id(&[7])
|
|
else {
|
|
panic!("ordinary protocol responses should still be found");
|
|
};
|
|
assert_eq!(command_id, 7);
|
|
let message = event.into_protocol_message();
|
|
assert_eq!(message["id"], json!(7));
|
|
let Some((typed_prefix, response)) = prefix.split_next_runtime_response_ready() else {
|
|
panic!("typed runtime response should stay in the prefix for typed routing");
|
|
};
|
|
assert!(typed_prefix.is_empty());
|
|
assert_eq!(response.command_id(), 42);
|
|
}
|
|
|
|
fn network_response_event(
|
|
resource_type: DevToolsNetworkResourceType,
|
|
request_id: &str,
|
|
) -> BackgroundProtocolEvent {
|
|
network_response_event_for_target(resource_type, request_id, "TID-nav")
|
|
}
|
|
|
|
fn network_response_event_for_target(
|
|
resource_type: DevToolsNetworkResourceType,
|
|
request_id: &str,
|
|
target_id: &str,
|
|
) -> BackgroundProtocolEvent {
|
|
let message = json!({
|
|
"method": "Network.responseReceived",
|
|
"params": {
|
|
"requestId": request_id,
|
|
"type": resource_type.as_cdp_type(),
|
|
"response": {
|
|
"url": "https://example.test/resource",
|
|
"status": 200,
|
|
"fromDiskCache": false
|
|
}
|
|
}
|
|
});
|
|
let automation_event = AutomationEvent::NetworkResponseStarted(NetworkRequestEvent {
|
|
target_id: DevToolsTargetId::from(target_id),
|
|
frame_id: Some(DevToolsFrameId::from("FRAME-nav")),
|
|
request_id: DevToolsRequestId::from(request_id),
|
|
loader_id: Some(DevToolsLoaderId::from("LOADER-nav")),
|
|
url: "https://example.test/resource".to_owned(),
|
|
document_url: None,
|
|
method: None,
|
|
request_headers: Vec::new(),
|
|
request_body: None,
|
|
request_initiator_type: None,
|
|
bidi_request_initiator_type: None,
|
|
redirect_response: None,
|
|
redirect_has_extra_info: false,
|
|
request_cookie_report: None,
|
|
resource_type: Some(resource_type),
|
|
timestamp: Some(1.0),
|
|
wall_time: None,
|
|
status: Some(200),
|
|
status_text: None,
|
|
response_headers: Vec::new(),
|
|
response_mime_type: None,
|
|
response_protocol: None,
|
|
has_extra_info: false,
|
|
encoded_data_length: Some(0),
|
|
from_cache: false,
|
|
fetch_request_id: None,
|
|
error_text: None,
|
|
loading_failed_canceled: false,
|
|
blocked_intercepts: Vec::new(),
|
|
network_id: None,
|
|
|
|
auth_challenge: None,
|
|
});
|
|
BackgroundProtocolEvent::immediate_automation_event(message, automation_event)
|
|
}
|
|
|
|
fn network_request_event_for_target(
|
|
resource_type: DevToolsNetworkResourceType,
|
|
request_id: &str,
|
|
target_id: &str,
|
|
) -> BackgroundProtocolEvent {
|
|
let (_, Some(AutomationEvent::NetworkResponseStarted(mut network_event))) =
|
|
network_response_event_for_target(resource_type, request_id, target_id).into_parts()
|
|
else {
|
|
unreachable!("network response fixture should retain its typed sidecar")
|
|
};
|
|
network_event.status = None;
|
|
network_event.encoded_data_length = None;
|
|
BackgroundProtocolEvent::immediate_automation_event(
|
|
json!({
|
|
"method": "Network.requestWillBeSent",
|
|
"params": {
|
|
"requestId": request_id,
|
|
"loaderId": "LOADER-nav",
|
|
"type": resource_type.as_cdp_type(),
|
|
"request": {
|
|
"url": "https://example.test/resource",
|
|
"method": "GET"
|
|
}
|
|
}
|
|
}),
|
|
AutomationEvent::NetworkBeforeRequestSent(network_event),
|
|
)
|
|
}
|
|
|
|
fn network_finished_event_for_target(
|
|
resource_type: DevToolsNetworkResourceType,
|
|
request_id: &str,
|
|
target_id: &str,
|
|
) -> BackgroundProtocolEvent {
|
|
let (_, Some(AutomationEvent::NetworkResponseStarted(network_event))) =
|
|
network_response_event_for_target(resource_type, request_id, target_id).into_parts()
|
|
else {
|
|
unreachable!("network response fixture should retain its typed sidecar")
|
|
};
|
|
BackgroundProtocolEvent::immediate_automation_event(
|
|
json!({
|
|
"method": "Network.loadingFinished",
|
|
"params": {
|
|
"requestId": request_id,
|
|
"timestamp": 2.0,
|
|
"encodedDataLength": 0
|
|
}
|
|
}),
|
|
AutomationEvent::NetworkResponseCompleted(network_event),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_owner_protocol_output_keeps_the_conservative_global_gate() {
|
|
let mut conn = CdpConnection::new();
|
|
let navigation = arm_background_navigation_request(&mut conn, "LOADER-known");
|
|
let target_id = navigation.target_id().to_owned();
|
|
let known = network_response_event_for_target(
|
|
DevToolsNetworkResourceType::Xhr,
|
|
"REQ-known",
|
|
&target_id,
|
|
);
|
|
assert_eq!(
|
|
ProtocolOutputSequence::from_background_event(known.clone())
|
|
.navigation_gate_target_ids(&conn),
|
|
[target_id]
|
|
);
|
|
|
|
let unresolved = BackgroundProtocolEvent::immediate(json!({
|
|
"method": "Runtime.consoleAPICalled",
|
|
"sessionId": "SID-missing",
|
|
"params": {}
|
|
}));
|
|
let mixed = ProtocolOutputSequence::from_background_events(vec![known, unresolved]);
|
|
assert!(
|
|
mixed.navigation_gate_target_ids(&conn).is_empty(),
|
|
"an atomic batch with any unresolved owner must remain connection-gated"
|
|
);
|
|
}
|
|
|
|
fn output_request_ids(output: ProtocolOutputSequence) -> Vec<String> {
|
|
output
|
|
.into_background_events()
|
|
.into_iter()
|
|
.map(|event| {
|
|
let (message, automation_event) = event.into_parts();
|
|
assert!(
|
|
automation_event.is_some(),
|
|
"deferred background events must preserve typed sidecars"
|
|
);
|
|
message["params"]["requestId"]
|
|
.as_str()
|
|
.expect("network event request id")
|
|
.to_owned()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn renderer_output_record(page_id: PageId, sequence: u64) -> RendererOutputRecord {
|
|
RendererOutputRecord::new_for_test(RendererOutputItem::Observation(
|
|
RendererProtocolObservation::DocumentLifecycle(RendererDocumentLifecycleEvent {
|
|
frame: RendererFrameToken { page_id },
|
|
document: RendererDocumentToken::new_for_testing(page_id, 1),
|
|
epoch: RendererLifecycleEpoch(1),
|
|
sequence,
|
|
timestamp_micros: sequence,
|
|
kind: RendererDocumentLifecycleEventKind::Milestone(
|
|
RendererDocumentLifecycleMilestone::DomContentLoaded,
|
|
),
|
|
}),
|
|
))
|
|
}
|
|
|
|
fn renderer_publication(
|
|
page_id: PageId,
|
|
sequence: u64,
|
|
ordering: RendererOutputPublicationOrdering,
|
|
) -> RendererOutputTransportMessage {
|
|
let stream = RendererOutputStreamIdentity::new_page_for_protocol_test(page_id);
|
|
RendererOutputPublication::new_for_test_with_ordering(
|
|
RendererOutputCursor::new_for_test(stream, sequence),
|
|
ordering,
|
|
vec![renderer_output_record(page_id, sequence)],
|
|
)
|
|
.into()
|
|
}
|
|
|
|
fn unconstrained_renderer_publication(page_id: PageId) -> RendererOutputTransportMessage {
|
|
renderer_publication(page_id, 1, RendererOutputPublicationOrdering::Unconstrained)
|
|
}
|
|
|
|
fn post_load_renderer_publication(
|
|
page_id: PageId,
|
|
lifecycle_document_id: u64,
|
|
) -> RendererOutputTransportMessage {
|
|
let source_document = root_document_lifecycle_identity(page_id, lifecycle_document_id);
|
|
renderer_publication(
|
|
page_id,
|
|
1,
|
|
RendererOutputPublicationOrdering::AfterPendingPageLoad { source_document },
|
|
)
|
|
}
|
|
|
|
fn publication_cursor(publication: &RendererOutputTransportMessage) -> RendererOutputCursor {
|
|
let RendererOutputTransportMessage::Publication(publication) = publication else {
|
|
panic!("test fixture must contain one concrete publication");
|
|
};
|
|
publication.cursor()
|
|
}
|
|
|
|
fn root_document_lifecycle_identity(
|
|
page_id: PageId,
|
|
identity_value: u64,
|
|
) -> RendererDocumentLifecycleIdentity {
|
|
RendererDocumentLifecycleIdentity {
|
|
frame: RendererFrameToken { page_id },
|
|
document: RendererDocumentToken::new_for_testing(page_id, identity_value),
|
|
epoch: RendererLifecycleEpoch(identity_value),
|
|
}
|
|
}
|
|
|
|
fn root_frame_stopped_loading_work(publish_sequence: u64, frame_id: &str) -> ProtocolSchedulerWork {
|
|
make_root_frame_stopped_loading_work(
|
|
publish_sequence,
|
|
vec![Some("SID-nav".to_owned())],
|
|
frame_id.to_owned(),
|
|
format!("LOADER-{frame_id}"),
|
|
)
|
|
}
|
|
|
|
fn future_load_candidate(
|
|
publication: &moli_core::RendererOutputTransportMessage,
|
|
) -> DeferredMainDocumentLoadPredecessorCandidate {
|
|
DeferredMainDocumentLoadPredecessorCandidate::from_renderer_publication(publication)
|
|
.expect("timer publication should be eligible for an exact future load predecessor")
|
|
}
|
|
|
|
fn load_interest_for_publication(
|
|
publication: &moli_core::RendererOutputTransportMessage,
|
|
) -> DeferredMainDocumentLoadCompletionOutputInterest {
|
|
let RendererOutputTransportMessage::Publication(publication) = publication else {
|
|
panic!("test fixture must contain one concrete publication");
|
|
};
|
|
let RendererOutputPublicationOrdering::AfterPendingPageLoad { source_document } =
|
|
publication.ordering()
|
|
else {
|
|
panic!("test fixture must carry exact post-load ordering");
|
|
};
|
|
deferred_main_document_load_output_interest(
|
|
publication.cursor().stream().residence(),
|
|
Some(source_document),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn only_post_load_renderer_output_can_become_a_future_load_predecessor_candidate() {
|
|
let page_id = PageId::new_for_testing(7);
|
|
let post_load = post_load_renderer_publication(page_id, 1);
|
|
assert!(
|
|
DeferredMainDocumentLoadPredecessorCandidate::from_renderer_publication(&post_load)
|
|
.is_some(),
|
|
"only a publication carrying typed post-load ordering may become a candidate"
|
|
);
|
|
|
|
let unconstrained = unconstrained_renderer_publication(page_id);
|
|
assert!(
|
|
DeferredMainDocumentLoadPredecessorCandidate::from_renderer_publication(&unconstrained)
|
|
.is_none(),
|
|
"ordinary concrete output is not inferred to be post-load from its payload"
|
|
);
|
|
|
|
let control: RendererOutputTransportMessage = RendererOutputStreamControl::Opened {
|
|
stream: RendererOutputStreamIdentity::new_page_for_protocol_test(page_id),
|
|
}
|
|
.into();
|
|
assert!(
|
|
DeferredMainDocumentLoadPredecessorCandidate::from_renderer_publication(&control).is_none(),
|
|
"stream lifetime control is never Page output"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn renderer_output_publication_waits_for_its_exact_load_predecessors() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let first = deferred_main_document_load_observation_id(1);
|
|
let second = deferred_main_document_load_observation_id(2);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]),
|
|
vec![first, second],
|
|
None,
|
|
);
|
|
|
|
queues.satisfy_front_client_turn_predecessor();
|
|
assert!(!queues.should_complete_next_residence());
|
|
queues.satisfy_load_predecessor(second);
|
|
assert!(
|
|
!queues.should_complete_next_residence(),
|
|
"an unrelated completed load must not release the frozen batch early"
|
|
);
|
|
queues.satisfy_load_predecessor(first);
|
|
assert!(queues.should_complete_next_residence());
|
|
}
|
|
|
|
#[test]
|
|
fn concrete_renderer_output_precedes_work_published_by_the_same_ingress() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Page.loadEventFired"})]),
|
|
vec![load],
|
|
None,
|
|
);
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
Vec::new(),
|
|
None,
|
|
);
|
|
|
|
assert!(matches!(
|
|
queues.protocol_residences.front(),
|
|
Some(ProtocolSchedulerResidence::RendererOutputPublication(work))
|
|
if work.renderer_output_cursor == cursor
|
|
));
|
|
assert!(matches!(
|
|
queues.protocol_residences.get(1),
|
|
Some(ProtocolSchedulerResidence::ProtocolWork { work, .. })
|
|
if work.publish_sequence().get() == 1
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_work_published_by_held_ingress_inherits_exact_load_predecessor() {
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.apply_scheduler_events_with_load_predecessors(
|
|
vec![CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
}],
|
|
&[load],
|
|
None,
|
|
);
|
|
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::SatisfyClientTurnPredecessor
|
|
);
|
|
scheduler.satisfy_front_protocol_residence_client_turn_predecessor();
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::Wait,
|
|
"a yielded client turn cannot satisfy the exact load predecessor"
|
|
);
|
|
scheduler.queues.satisfy_load_predecessor(load);
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::CompleteReadyResidence
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn newly_published_load_binds_every_residence_from_deferred_renderer_ingress() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate = future_load_candidate(&publication);
|
|
let interest = load_interest_for_publication(&publication);
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Page.loadEventFired"})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
assert_eq!(
|
|
queues.bind_main_document_load_predecessor(load, &interest),
|
|
0,
|
|
"the load owner action must be inserted before every residence produced by the ingress"
|
|
);
|
|
assert!(matches!(
|
|
queues.protocol_residences.front(),
|
|
Some(ProtocolSchedulerResidence::RendererOutputPublication(work))
|
|
if work.load_predecessors == [load]
|
|
));
|
|
assert!(matches!(
|
|
queues.protocol_residences.get(1),
|
|
Some(ProtocolSchedulerResidence::ProtocolWork {
|
|
load_predecessors,
|
|
..
|
|
}) if load_predecessors == &[load]
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn future_load_predecessor_candidate_cannot_bind_another_page_load() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate = future_load_candidate(&publication);
|
|
let unrelated_page_publication = post_load_renderer_publication(PageId::new_for_testing(8), 1);
|
|
let unrelated_interest = load_interest_for_publication(&unrelated_page_publication);
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
assert_eq!(
|
|
queues.bind_main_document_load_predecessor(load, &unrelated_interest),
|
|
queues.protocol_residence_len(),
|
|
"a load observation may bind only output from its exact Page"
|
|
);
|
|
assert!(matches!(
|
|
queues.protocol_residences.front(),
|
|
Some(ProtocolSchedulerResidence::RendererOutputPublication(work))
|
|
if work.load_predecessors.is_empty()
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn after_load_candidate_cannot_bind_another_document_on_the_same_page() {
|
|
let page_id = PageId::new_for_testing(7);
|
|
let replacement_document = root_document_lifecycle_identity(page_id, 2);
|
|
let publication = post_load_renderer_publication(page_id, 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate =
|
|
DeferredMainDocumentLoadPredecessorCandidate::from_renderer_publication(&publication)
|
|
.expect("after-load lifecycle output should expose an exact Document candidate");
|
|
let replacement_interest = deferred_main_document_load_output_interest(
|
|
publication.residence(),
|
|
Some(replacement_document),
|
|
);
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({
|
|
"method": "Page.frameStartedNavigating"
|
|
})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
assert_eq!(
|
|
queues.bind_main_document_load_predecessor(load, &replacement_interest),
|
|
queues.protocol_residence_len(),
|
|
"an after-load action may bind only the exact source Document's load observation"
|
|
);
|
|
assert!(matches!(
|
|
queues.protocol_residences.front(),
|
|
Some(ProtocolSchedulerResidence::RendererOutputPublication(work))
|
|
if work.load_predecessors.is_empty()
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn old_document_timer_output_does_not_wait_behind_replacement_document_load() {
|
|
let page_id = PageId::new_for_testing(7);
|
|
let publication = post_load_renderer_publication(page_id, 1);
|
|
let replacement_interest = deferred_main_document_load_output_interest(
|
|
publication.residence(),
|
|
Some(root_document_lifecycle_identity(page_id, 2)),
|
|
);
|
|
|
|
assert_eq!(
|
|
replacement_interest.route_output_while_waiting(&publication),
|
|
DeferredMainDocumentLoadCompletionOutputAction::ProcessNow,
|
|
"a timer turn must retain the Document that authorized it across replacement"
|
|
);
|
|
assert!(
|
|
!replacement_interest.observes_predecessor_candidate(future_load_candidate(&publication)),
|
|
"a replacement Document must not adopt the old timer publication as its load successor"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn client_turn_yield_closes_the_provisional_load_binding_window() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate = future_load_candidate(&publication);
|
|
let interest = load_interest_for_publication(&publication);
|
|
let unrelated_later_load = deferred_main_document_load_observation_id(1);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
queues.satisfy_front_client_turn_predecessor();
|
|
|
|
assert_eq!(
|
|
queues.bind_main_document_load_predecessor(unrelated_later_load, &interest),
|
|
queues.protocol_residence_len(),
|
|
"a later command must not retroactively bind work after its client-turn boundary"
|
|
);
|
|
assert!(matches!(
|
|
queues.protocol_residences.front(),
|
|
Some(ProtocolSchedulerResidence::RendererOutputPublication(work))
|
|
if work.load_predecessors.is_empty()
|
|
));
|
|
assert!(matches!(
|
|
queues.protocol_residences.get(1),
|
|
Some(ProtocolSchedulerResidence::ProtocolWork {
|
|
load_predecessors,
|
|
..
|
|
}) if load_predecessors.is_empty()
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn client_turn_yield_closes_candidate_behind_an_unrelated_front_residence() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate = future_load_candidate(&publication);
|
|
let interest = load_interest_for_publication(&publication);
|
|
let unrelated_later_load = deferred_main_document_load_observation_id(1);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
Vec::new(),
|
|
None,
|
|
);
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
queues.satisfy_front_client_turn_predecessor();
|
|
|
|
assert_eq!(
|
|
queues.bind_main_document_load_predecessor(unrelated_later_load, &interest),
|
|
queues.protocol_residence_len(),
|
|
"yielding for an older front residence must also close every candidate admitted before that client turn"
|
|
);
|
|
assert!(matches!(
|
|
queues.protocol_residences.get(1),
|
|
Some(ProtocolSchedulerResidence::RendererOutputPublication(work))
|
|
if work.load_predecessors.is_empty()
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_response_releases_its_frozen_renderer_publication_predecessor() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(1), 1);
|
|
let candidate = future_load_candidate(&publication);
|
|
let cursor = publication_cursor(&publication);
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({
|
|
"method": "Page.navigatedWithinDocument"
|
|
})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
let output = scheduler
|
|
.complete_renderer_output_predecessor_before_runtime_response(
|
|
&RendererOutputFence::new_for_test(cursor),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
output.into_messages(),
|
|
vec![json!({"method": "Page.navigatedWithinDocument"})],
|
|
"a provisional load-binding window must close before its later Runtime response"
|
|
);
|
|
assert_eq!(scheduler.queues.protocol_residence_len(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_response_cannot_release_renderer_output_with_an_exact_load_predecessor() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(1), 1);
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let cursor = publication_cursor(&publication);
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({
|
|
"method": "Runtime.consoleAPICalled"
|
|
})]),
|
|
vec![load],
|
|
None,
|
|
);
|
|
|
|
let output = scheduler
|
|
.complete_renderer_output_predecessor_before_runtime_response(
|
|
&RendererOutputFence::new_for_test(cursor),
|
|
)
|
|
.await;
|
|
|
|
assert!(output.is_empty());
|
|
assert_eq!(
|
|
scheduler.queues.protocol_residence_len(),
|
|
1,
|
|
"the exact load observation remains the only release authority"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn provisional_load_binding_prevents_specialized_wait_drain_from_overtaking_ingress() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate = future_load_candidate(&publication);
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Page.loadEventFired"})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
|
|
assert!(
|
|
queues.take_external_load_wait_snapshot().is_empty(),
|
|
"a specialized wait drain must not steal same-ingress work before command completion gets its load-binding opportunity"
|
|
);
|
|
assert_eq!(queues.protocol_residence_len(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn specialized_wait_yield_closes_candidate_retained_in_the_main_fifo() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let candidate = future_load_candidate(&publication);
|
|
let interest = load_interest_for_publication(&publication);
|
|
let unrelated_later_load = deferred_main_document_load_observation_id(1);
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]),
|
|
Vec::new(),
|
|
Some(candidate),
|
|
);
|
|
scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
}]);
|
|
|
|
let _ = scheduler
|
|
.complete_ready_protocol_residences_for_external_load_wait()
|
|
.await;
|
|
|
|
assert_eq!(
|
|
scheduler
|
|
.queues
|
|
.bind_main_document_load_predecessor(unrelated_later_load, &interest),
|
|
scheduler.queues.protocol_residence_len(),
|
|
"a specialized wait turn must close candidates retained outside its checked-out snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scheduler_holds_concrete_renderer_output_until_its_exact_load_predecessor_finishes() {
|
|
let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1);
|
|
let cursor = publication_cursor(&publication);
|
|
let load = deferred_main_document_load_observation_id(1);
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.queues.enqueue_renderer_output_publication(
|
|
cursor,
|
|
ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]),
|
|
vec![load],
|
|
None,
|
|
);
|
|
|
|
scheduler.satisfy_front_protocol_residence_client_turn_predecessor();
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::Wait,
|
|
"the exact load predecessor owns scheduler readiness"
|
|
);
|
|
scheduler.queues.satisfy_load_predecessor(load);
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::CompleteReadyResidence
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn renderer_stream_control_is_consumed_without_protocol_residence() {
|
|
let control: RendererOutputTransportMessage = RendererOutputStreamControl::Opened {
|
|
stream: RendererOutputStreamIdentity::new_page_for_protocol_test(PageId::new_for_testing(
|
|
7,
|
|
)),
|
|
}
|
|
.into();
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
let output = scheduler.ingest_renderer_publication_now(control).await;
|
|
|
|
assert!(output.is_empty());
|
|
assert_eq!(scheduler.queues.protocol_residence_len(), 0);
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::Wait
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn closed_renderer_transport_fails_an_unprojected_command_fence() {
|
|
let (_background_event_tx, background_event_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
let (_navigation_tx, background_navigation_completion_rx) =
|
|
tokio::sync::mpsc::unbounded_channel();
|
|
let (renderer_tx, renderer_publication_rx) = moli_core::renderer_output_transport_channel();
|
|
drop(renderer_tx);
|
|
let mut receivers = super::CdpSchedulerEventReceivers {
|
|
background_event_rx,
|
|
background_navigation_completion_rx,
|
|
renderer_publication_rx,
|
|
};
|
|
let stream =
|
|
RendererOutputStreamIdentity::new_page_for_protocol_test(PageId::new_for_testing(90));
|
|
let predecessor =
|
|
RendererOutputFence::new_for_test(RendererOutputCursor::new_for_test(stream, 1));
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
|
|
let failure = scheduler
|
|
.project_renderer_output_predecessor_before_devtools_result(&mut receivers, &predecessor)
|
|
.await
|
|
.expect_err("transport terminal must fail rather than wait forever");
|
|
let (output, error) = failure.into_parts();
|
|
assert!(output.is_empty());
|
|
assert_eq!(
|
|
error.kind,
|
|
moli_protocol::devtools_runtime::DevToolsErrorKind::Internal
|
|
);
|
|
assert!(error.message.contains("closed"));
|
|
}
|
|
|
|
#[test]
|
|
fn concrete_protocol_output_waits_for_its_client_turn_predecessor() {
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
}]);
|
|
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::SatisfyClientTurnPredecessor
|
|
);
|
|
scheduler.satisfy_front_protocol_residence_client_turn_predecessor();
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::CompleteReadyResidence
|
|
);
|
|
|
|
let Some(ProtocolSchedulerResidence::ProtocolWork { work, .. }) =
|
|
scheduler.queues.pop_next_protocol_residence()
|
|
else {
|
|
panic!("ready output must remain the same concrete work");
|
|
};
|
|
assert_eq!(work.publish_sequence().get(), 1);
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::Wait
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn concrete_protocol_output_preserves_protocol_publish_order() {
|
|
let mut scheduler = CdpScheduler::new(CdpConnection::new());
|
|
scheduler.apply_scheduler_events(vec![
|
|
CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
},
|
|
CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work(2, "FRAME-2"),
|
|
},
|
|
]);
|
|
|
|
let sequences = scheduler
|
|
.queues
|
|
.protocol_residences
|
|
.iter()
|
|
.map(|residence| match residence {
|
|
ProtocolSchedulerResidence::ProtocolWork { work, .. } => work.publish_sequence().get(),
|
|
_ => panic!("test enqueued only concrete protocol output"),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(sequences, [1, 2]);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(
|
|
expected = "protocol work must enter scheduler residence in exact publication order"
|
|
)]
|
|
fn concrete_protocol_output_rejects_duplicate_publication_sequence() {
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1"),
|
|
Vec::new(),
|
|
None,
|
|
);
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(1, "FRAME-1-DUPLICATE"),
|
|
Vec::new(),
|
|
None,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(
|
|
expected = "protocol work must enter scheduler residence in exact publication order"
|
|
)]
|
|
fn concrete_protocol_output_rejects_missing_earlier_publication() {
|
|
let mut queues = SchedulerQueues::default();
|
|
queues.enqueue_protocol_work(
|
|
root_frame_stopped_loading_work(2, "FRAME-2"),
|
|
Vec::new(),
|
|
None,
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn background_navigation_blocks_only_its_target_protocol_residences() {
|
|
let mut conn = CdpConnection::new();
|
|
let navigation = arm_background_navigation_request(&mut conn, "LOADER-nav");
|
|
let navigation_target_id = navigation.target_id().to_owned();
|
|
let browser_context_id = conn.default_browser_context_id().to_owned();
|
|
let mut scheduler = CdpScheduler::new(conn);
|
|
scheduler.apply_scheduler_events(vec![
|
|
CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work_for_target(
|
|
1,
|
|
vec![Some("SID-A".to_owned())],
|
|
browser_context_id.clone(),
|
|
navigation_target_id,
|
|
"FRAME-A".to_owned(),
|
|
"LOADER-A".to_owned(),
|
|
),
|
|
},
|
|
CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work_for_target(
|
|
2,
|
|
vec![Some("SID-B".to_owned())],
|
|
browser_context_id,
|
|
"TID-independent".to_owned(),
|
|
"FRAME-B".to_owned(),
|
|
"LOADER-B".to_owned(),
|
|
),
|
|
},
|
|
]);
|
|
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::SatisfyClientTurnPredecessor,
|
|
"the independent target must advance around target A's navigation"
|
|
);
|
|
scheduler.satisfy_front_protocol_residence_client_turn_predecessor();
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::CompleteReadyResidence
|
|
);
|
|
assert!(
|
|
scheduler
|
|
.complete_next_protocol_residence()
|
|
.await
|
|
.is_empty()
|
|
);
|
|
let remaining_sequences = scheduler
|
|
.queues
|
|
.protocol_residences
|
|
.iter()
|
|
.map(|residence| match residence {
|
|
ProtocolSchedulerResidence::ProtocolWork { work, .. } => work.publish_sequence().get(),
|
|
_ => panic!("test enqueued protocol work only"),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(remaining_sequences, [1]);
|
|
assert_eq!(
|
|
scheduler.next_protocol_scheduler_step(),
|
|
ProtocolSchedulerStep::Wait,
|
|
"target A's own residence must remain gated"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn protocol_residence_snapshot_skips_another_targets_navigation_gate() {
|
|
let mut conn = CdpConnection::new();
|
|
let navigation = arm_background_navigation_request(&mut conn, "LOADER-A");
|
|
let target_a = navigation.target_id().to_owned();
|
|
let browser_context_id = conn.default_browser_context_id().to_owned();
|
|
let mut scheduler = CdpScheduler::new(conn);
|
|
scheduler.apply_scheduler_events(vec![
|
|
CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work_for_target(
|
|
1,
|
|
vec![Some("SID-A".to_owned())],
|
|
browser_context_id.clone(),
|
|
target_a,
|
|
"FRAME-A".to_owned(),
|
|
"LOADER-A".to_owned(),
|
|
),
|
|
},
|
|
CdpSchedulerEvent::ProtocolWorkPublished {
|
|
work: root_frame_stopped_loading_work_for_target(
|
|
2,
|
|
vec![Some("SID-B".to_owned())],
|
|
browser_context_id,
|
|
"TID-B".to_owned(),
|
|
"FRAME-B".to_owned(),
|
|
"LOADER-B".to_owned(),
|
|
),
|
|
},
|
|
]);
|
|
let snapshot = std::mem::take(&mut scheduler.queues.protocol_residences);
|
|
|
|
assert!(
|
|
scheduler
|
|
.complete_protocol_residence_snapshot(snapshot)
|
|
.await
|
|
.is_empty()
|
|
);
|
|
let remaining_sequences = scheduler
|
|
.queues
|
|
.protocol_residences
|
|
.iter()
|
|
.map(|residence| match residence {
|
|
ProtocolSchedulerResidence::ProtocolWork { work, .. } => work.publish_sequence().get(),
|
|
_ => panic!("test enqueued protocol work only"),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(remaining_sequences, [1]);
|
|
}
|
|
|
|
#[test]
|
|
fn replacement_navigation_cancels_and_exactly_settles_the_target_owned_request() {
|
|
let mut conn = CdpConnection::new();
|
|
let source = arm_background_navigation_request(&mut conn, "LOADER-source");
|
|
let replacement = arm_background_navigation_request(&mut conn, "LOADER-replacement");
|
|
assert!(source.is_cancelled());
|
|
assert!(!replacement.is_cancelled());
|
|
|
|
assert!(!settle_background_navigation_request(&mut conn, &source));
|
|
assert!(
|
|
conn.has_inflight_background_navigation(),
|
|
"a stale completion must not clear the replacement request"
|
|
);
|
|
assert!(settle_background_navigation_request(
|
|
&mut conn,
|
|
&replacement
|
|
));
|
|
assert!(!conn.has_inflight_background_navigation());
|
|
}
|
|
|
|
#[test]
|
|
fn scheduler_defers_subresource_network_events_until_background_navigation_gate_clears() {
|
|
let mut conn = CdpConnection::new();
|
|
let navigation = arm_background_navigation_request(&mut conn, "LOADER-nav");
|
|
let target_id = navigation.target_id().to_owned();
|
|
let mut scheduler = CdpScheduler::new(conn);
|
|
|
|
let document_output = scheduler.route_background_event_around_inflight_navigation(
|
|
network_response_event_for_target(
|
|
DevToolsNetworkResourceType::Document,
|
|
"REQ-document",
|
|
&target_id,
|
|
),
|
|
);
|
|
assert_eq!(output_request_ids(document_output), ["REQ-document"]);
|
|
|
|
let script_output = scheduler.route_background_event_around_inflight_navigation(
|
|
network_response_event_for_target(
|
|
DevToolsNetworkResourceType::Script,
|
|
"REQ-script",
|
|
&target_id,
|
|
),
|
|
);
|
|
assert!(script_output.is_empty());
|
|
let script_terminal = scheduler.route_background_event_around_inflight_navigation(
|
|
network_finished_event_for_target(
|
|
DevToolsNetworkResourceType::Script,
|
|
"REQ-script",
|
|
&target_id,
|
|
),
|
|
);
|
|
assert!(script_terminal.is_empty());
|
|
assert_eq!(scheduler.pending_navigation_background_events.len(), 2);
|
|
|
|
assert!(settle_background_navigation_request(
|
|
&mut scheduler.conn,
|
|
&navigation
|
|
));
|
|
let released = scheduler.drain_pending_navigation_background_events();
|
|
let released = released
|
|
.into_background_events()
|
|
.into_iter()
|
|
.map(BackgroundProtocolEvent::into_protocol_message)
|
|
.map(|message| {
|
|
(
|
|
message["method"].as_str().unwrap().to_owned(),
|
|
message["params"]["requestId"].as_str().unwrap().to_owned(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
released,
|
|
[
|
|
(
|
|
"Network.responseReceived".to_owned(),
|
|
"REQ-script".to_owned()
|
|
),
|
|
(
|
|
"Network.loadingFinished".to_owned(),
|
|
"REQ-script".to_owned()
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn target_a_navigation_does_not_defer_target_b_network_events() {
|
|
let mut conn = CdpConnection::new();
|
|
conn.install_default_browser_target();
|
|
let context = DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverBidi,
|
|
session_id: Some(DevToolsSessionId::from("bidi-test")),
|
|
target_id: None,
|
|
browser_context_id: None,
|
|
};
|
|
let create_target = |context: DevToolsCommandContext| {
|
|
DevToolsCommand::CreateTarget(DevToolsCreateTargetCommand {
|
|
context,
|
|
url: "about:blank".to_owned(),
|
|
browser_context_id: None,
|
|
activate: false,
|
|
})
|
|
};
|
|
let first_create = conn
|
|
.execute_devtools_command(create_target(context.clone()))
|
|
.await;
|
|
let (first_result, _) = first_create.into_parts();
|
|
let DevToolsCommandResult::CreateTarget(first_result) =
|
|
first_result.expect("first target should be created")
|
|
else {
|
|
panic!("expected create-target result")
|
|
};
|
|
let first_target_id = first_result.target_id.into_string();
|
|
let second_create = conn.execute_devtools_command(create_target(context)).await;
|
|
let (second_result, _) = second_create.into_parts();
|
|
let DevToolsCommandResult::CreateTarget(second_result) =
|
|
second_result.expect("second target should be created")
|
|
else {
|
|
panic!("expected create-target result")
|
|
};
|
|
let second_target_id = second_result.target_id.into_string();
|
|
assert_ne!(first_target_id, second_target_id);
|
|
let navigation = arm_background_navigation_request(&mut conn, "LOADER-A");
|
|
let target_a = navigation.target_id().to_owned();
|
|
let target_b = if target_a == second_target_id {
|
|
first_target_id
|
|
} else {
|
|
second_target_id
|
|
};
|
|
assert!(conn.has_inflight_background_navigation_for_target(&target_a));
|
|
assert!(!conn.has_inflight_background_navigation_for_target(&target_b));
|
|
|
|
let mut scheduler = CdpScheduler::new(conn);
|
|
let target_b_output = scheduler.route_background_event_around_inflight_navigation(
|
|
network_response_event_for_target(DevToolsNetworkResourceType::Xhr, "REQ-B", &target_b),
|
|
);
|
|
assert_eq!(output_request_ids(target_b_output), ["REQ-B"]);
|
|
|
|
let navigation_b =
|
|
arm_background_navigation_request_for_target(&mut scheduler.conn, &target_b, "LOADER-B");
|
|
let target_b_held = scheduler.route_background_event_around_inflight_navigation(
|
|
network_response_event_for_target(
|
|
DevToolsNetworkResourceType::Xhr,
|
|
"REQ-B-held",
|
|
&target_b,
|
|
),
|
|
);
|
|
assert!(target_b_held.is_empty());
|
|
let target_a_output = scheduler.route_background_event_around_inflight_navigation(
|
|
network_response_event_for_target(DevToolsNetworkResourceType::Xhr, "REQ-A", &target_a),
|
|
);
|
|
assert!(target_a_output.is_empty());
|
|
assert_eq!(scheduler.pending_navigation_background_events.len(), 2);
|
|
|
|
assert!(settle_background_navigation_request(
|
|
&mut scheduler.conn,
|
|
&navigation
|
|
));
|
|
assert_eq!(
|
|
output_request_ids(scheduler.drain_pending_navigation_background_events()),
|
|
["REQ-A"],
|
|
"settling target A must not release target B's held event"
|
|
);
|
|
assert_eq!(scheduler.pending_navigation_background_events.len(), 1);
|
|
assert!(settle_background_navigation_request(
|
|
&mut scheduler.conn,
|
|
&navigation_b
|
|
));
|
|
assert_eq!(
|
|
output_request_ids(scheduler.drain_pending_navigation_background_events()),
|
|
["REQ-B-held"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn navigation_gate_release_precedes_later_renderer_boundary_network_output() {
|
|
let mut conn = CdpConnection::new();
|
|
let navigation = arm_background_navigation_request(&mut conn, "LOADER-nav");
|
|
let target_id = navigation.target_id().to_owned();
|
|
let mut scheduler = CdpScheduler::new(conn);
|
|
|
|
let request_id = "REQ-boundary-race";
|
|
assert!(
|
|
scheduler
|
|
.route_background_event_around_inflight_navigation(network_request_event_for_target(
|
|
DevToolsNetworkResourceType::Xhr,
|
|
request_id,
|
|
&target_id,
|
|
))
|
|
.is_empty()
|
|
);
|
|
|
|
assert!(settle_background_navigation_request(
|
|
&mut scheduler.conn,
|
|
&navigation
|
|
));
|
|
let mut output = ProtocolOutputSequence::empty();
|
|
scheduler.append_navigation_gate_release_before_renderer_boundary(&mut output);
|
|
output.append(ProtocolOutputSequence::from_background_event(
|
|
network_response_event_for_target(DevToolsNetworkResourceType::Xhr, request_id, &target_id),
|
|
));
|
|
output.append(ProtocolOutputSequence::from_background_event(
|
|
network_finished_event_for_target(DevToolsNetworkResourceType::Xhr, request_id, &target_id),
|
|
));
|
|
|
|
let methods = output
|
|
.into_background_events()
|
|
.into_iter()
|
|
.map(BackgroundProtocolEvent::into_protocol_message)
|
|
.map(|message| message["method"].as_str().unwrap().to_owned())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
methods,
|
|
[
|
|
"Network.requestWillBeSent",
|
|
"Network.responseReceived",
|
|
"Network.loadingFinished",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn foreground_load_wait_network_barrier_releases_document_response_before_subresources() {
|
|
let mut barrier =
|
|
ForegroundNavigationNetworkBarrier::for_navigation_wait(Some(DevToolsNavigationWait::Load));
|
|
|
|
let script_output = barrier.route_event(network_response_event(
|
|
DevToolsNetworkResourceType::Script,
|
|
"REQ-script",
|
|
));
|
|
assert!(script_output.is_empty());
|
|
|
|
let document_output = barrier.route_event(network_response_event(
|
|
DevToolsNetworkResourceType::Document,
|
|
"REQ-document",
|
|
));
|
|
assert_eq!(
|
|
output_request_ids(document_output),
|
|
["REQ-document", "REQ-script"]
|
|
);
|
|
|
|
let image_output = barrier.route_event(network_response_event(
|
|
DevToolsNetworkResourceType::Image,
|
|
"REQ-image",
|
|
));
|
|
assert_eq!(output_request_ids(image_output), ["REQ-image"]);
|
|
assert!(barrier.finish().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn foreground_load_wait_network_barrier_drains_subresources_if_document_response_never_arrives() {
|
|
let mut barrier =
|
|
ForegroundNavigationNetworkBarrier::for_navigation_wait(Some(DevToolsNavigationWait::Load));
|
|
|
|
let script_output = barrier.route_event(network_response_event(
|
|
DevToolsNetworkResourceType::Script,
|
|
"REQ-script",
|
|
));
|
|
assert!(script_output.is_empty());
|
|
|
|
assert_eq!(output_request_ids(barrier.finish()), ["REQ-script"]);
|
|
}
|