///|
/// WebDriver BiDi Protocol (minimal)
///
/// Focuses on a BiDi-first automation surface while reusing CDP sessions.
/// This is intentionally small and grows as coverage expands.
///|
/// BiDi request message
priv struct BidiRequest {
id : Int
method_name : String
params : Json?
}
///|
/// BiDi response message
priv struct BidiResponse {
id : Int
result : Json?
error : BidiError?
}
///|
/// BiDi error payload
priv struct BidiError {
error : String
message : String
stacktrace : String
}
///|
/// BiDi event message
priv struct BidiEvent {
event_method : String
params : Json
}
///|
/// Stored script.addPreloadScript registration.
priv struct PreloadScriptEntry {
script_id : String
function_declaration : String
arguments : Array[Json]
sandbox : String?
contexts : Array[String]
user_contexts : Array[String]
}
///|
/// Parsed script.addPreloadScript parameters.
priv struct AddPreloadScriptParams {
function_declaration : String
arguments : Array[Json]
sandbox : String?
contexts : Array[String]
user_contexts : Array[String]
}
///|
priv struct NavigationCompletion {
result : Json
navigation_id : String
deferred_response : Bool
}
///|
/// Outgoing BiDi message
priv enum BidiOutMessage {
Response(BidiResponse)
Event(BidiEvent)
Raw(String)
}
///|
/// BiDi protocol handler
priv struct BidiProtocol {
manager : CdpSessionManager
mut outbox : Array[BidiOutMessage]
mut default_context_id : String?
mut active_context_id : String?
mut next_navigation_id : Int
mut next_realm_id : Int
mut next_preload_script_id : Int
subscriptions : Map[String, Bool] // module-level subscriptions (legacy)
context_subscriptions : Map[String, Array[String]] // context -> events
user_context_subscriptions : Map[String, Array[String]] // user context -> events
mut global_subscriptions : Array[String] // events subscribed globally
mut global_unsubscribed_events : Array[String] // event-level overrides for module subscriptions
subscription_events : Map[String, Array[String]] // subscription id -> events
subscription_contexts : Map[String, Array[String]] // subscription id -> contexts
subscription_user_contexts : Map[String, Array[String]] // subscription id -> user contexts
mut next_subscription_id : Int
pending_log_entries : Map[String, Array[Json]] // context -> buffered log.entryAdded params
user_contexts : Map[String, Bool]
user_context_prompt_behavior : Map[String, Json] // user context -> unhandledPromptBehavior object
mut global_download_behavior : Json?
user_context_download_behavior : Map[String, Json] // user context -> downloadBehavior object
installed_web_extensions : Map[String, String] // extension id -> source kind
mut next_web_extension_id : Int
permission_state_by_scope : Map[String, String] // "|||" -> state
mut emulation_user_agent_global : String?
emulation_user_agent_by_context : Map[String, String] // top-level context -> userAgent override
emulation_user_agent_by_user_context : Map[String, String] // user context -> userAgent override
mut emulation_locale_global : String?
emulation_locale_by_context : Map[String, String] // top-level context -> locale override
emulation_locale_by_user_context : Map[String, String] // user context -> locale override
mut emulation_timezone_global : String?
emulation_timezone_by_context : Map[String, String] // top-level context -> timezone override
emulation_timezone_by_user_context : Map[String, String] // user context -> timezone override
mut emulation_geolocation_global : Json?
emulation_geolocation_by_context : Map[String, Json] // top-level context -> geolocation override
emulation_geolocation_by_user_context : Map[String, Json] // user context -> geolocation override
mut emulation_network_conditions_global : Json?
emulation_network_conditions_by_context : Map[String, Json] // top-level context -> network conditions override
emulation_network_conditions_by_user_context : Map[String, Json] // user context -> network conditions override
mut emulation_screen_orientation_global : Json?
emulation_screen_orientation_by_context : Map[String, Json] // top-level context -> screen orientation override
emulation_screen_orientation_by_user_context : Map[String, Json] // user context -> screen orientation override
mut emulation_screen_area_global : Json?
emulation_screen_area_by_context : Map[String, Json] // top-level context -> screen area override
emulation_screen_area_by_user_context : Map[String, Json] // user context -> screen area override
geolocation_watch_channel_by_scope : Map[String, String] // "|" -> script.message channel
geolocation_watch_realm_by_scope : Map[String, String] // "|" -> originating realm
geolocation_watch_notify_success_by_scope : Map[String, Bool] // "|" -> emit coords updates
geolocation_watch_notify_error_by_scope : Map[String, Bool] // "|" -> emit error updates
mut next_geolocation_watch_id : Int
bluetooth_adapter_state_by_context : Map[String, String] // context -> absent|powered-off|powered-on
bluetooth_simulation_disabled_by_context : Map[String, Bool] // context -> simulation disabled
bluetooth_preconnected_devices_by_context : Map[String, Array[Json]] // context -> synthetic device descriptors
bluetooth_granted_devices_by_context : Map[String, Array[Json]] // context -> granted synthetic device descriptors
bluetooth_pending_prompt_request : Map[String, Int] // context -> deferred requestDevice request id
bluetooth_pending_prompt_realm : Map[String, String] // context -> deferred requestDevice realm id
bluetooth_pending_prompt_unwrap_result : Map[String, Bool] // context -> deferred requestDevice unwrap mode
bluetooth_pending_prompt_id : Map[String, String] // context -> active bluetooth prompt id
bluetooth_pending_prompt_device_address : Map[String, String] // context -> prompt-selected device address
bluetooth_pending_prompt_device_name : Map[String, String] // context -> prompt-selected device name
bluetooth_gatt_connected_by_scope : Map[String, Bool] // "|" -> connected flag
bluetooth_services_by_scope : Map[String, Array[String]] // "|" -> primary service uuids
bluetooth_characteristics_by_scope : Map[String, Json] // "|||" -> properties object
bluetooth_descriptors_by_scope : Map[String, Bool] // "||||" -> descriptor exists
bluetooth_pending_gatt_request : Map[String, Int] // context -> deferred gatt.connect request id
bluetooth_pending_gatt_realm : Map[String, String] // context -> deferred gatt.connect realm id
bluetooth_pending_gatt_unwrap_result : Map[String, Bool] // context -> deferred gatt.connect unwrap mode
bluetooth_pending_gatt_address : Map[String, String] // context -> deferred gatt.connect device address
bluetooth_pending_gatt_response_kind : Map[String, String] // context -> deferred response mode
bluetooth_pending_characteristic_request : Map[String, Int] // context -> deferred characteristic request id
bluetooth_pending_characteristic_realm : Map[String, String] // context -> deferred characteristic realm id
bluetooth_pending_characteristic_unwrap_result : Map[String, Bool] // context -> deferred characteristic unwrap mode
bluetooth_pending_characteristic_address : Map[String, String] // context -> deferred characteristic device address
bluetooth_pending_characteristic_service_uuid : Map[String, String] // context -> deferred characteristic service uuid
bluetooth_pending_characteristic_uuid : Map[String, String] // context -> deferred characteristic uuid
bluetooth_pending_characteristic_type : Map[String, String] // context -> deferred characteristic response type
bluetooth_pending_descriptor_request : Map[String, Int] // context -> deferred descriptor request id
bluetooth_pending_descriptor_realm : Map[String, String] // context -> deferred descriptor realm id
bluetooth_pending_descriptor_unwrap_result : Map[String, Bool] // context -> deferred descriptor unwrap mode
bluetooth_pending_descriptor_address : Map[String, String] // context -> deferred descriptor device address
bluetooth_pending_descriptor_service_uuid : Map[String, String] // context -> deferred descriptor service uuid
bluetooth_pending_descriptor_characteristic_uuid : Map[String, String] // context -> deferred descriptor characteristic uuid
bluetooth_pending_descriptor_uuid : Map[String, String] // context -> deferred descriptor uuid
bluetooth_pending_descriptor_type : Map[String, String] // context -> deferred descriptor response type
mut next_bluetooth_prompt_id : Int
user_context_viewport_width : Map[String, Int] // user context -> default viewport width override
user_context_viewport_height : Map[String, Int] // user context -> default viewport height override
user_context_device_pixel_ratio : Map[String, Double] // user context -> default devicePixelRatio override
context_user_context : Map[String, String]
context_has_beforeunload : Map[String, Bool]
context_blocks_cross_origin_iframe_navigation : Map[String, Bool] // context -> active CSP/COEP frame policy
pending_prompt_type : Map[String, String] // context -> prompt type
pending_close_request : Map[String, Int] // context -> browsingContext.close request id
pending_close_wait_for_destroyed : Map[String, Bool] // context -> closeWithState wait flag
pending_close_response_mode : Map[String, String] // context -> close response kind
pending_prompt_request : Map[String, Int] // context -> deferred script.evaluate request id
pending_prompt_realm : Map[String, String] // context -> deferred script.evaluate realm id
pending_prompt_unwrap_result : Map[String, Bool] // context -> deferred script.evaluate unwrap mode
pending_beforeunload_request : Map[String, Int] // context -> deferred navigate request id
pending_beforeunload_url : Map[String, String] // context -> deferred navigate destination
pending_beforeunload_requested_url : Map[String, String] // context -> deferred original requested URL
pending_beforeunload_wait : Map[String, String] // context -> deferred navigate wait mode
pending_beforeunload_navigation : Map[String, String] // context -> deferred navigation id
pending_beforeunload_finalize_mode : Map[String, String] // context -> deferred internal navigate/reload mode
pending_navigation_request : Map[String, Int] // context -> deferred navigate/reload request id
pending_navigation_response_mode : Map[String, String] // context -> deferred navigation response kind
in_flight_navigation_url : Map[String, String] // context -> interruptible navigation URL
in_flight_navigation_id : Map[String, String] // context -> interruptible navigation id
in_flight_navigation_started_at : Map[String, Double] // context -> interruptible navigation start timestamp
pending_prompt_result_var : Map[String, String] // context -> window property to store prompt result
context_local_storage : Map[String, Map[String, String]] // context -> localStorage key/value
storage_cookies_by_scope : Map[String, Map[String, StoredCookie]] // scope -> stored cookies
synthetic_location_href_by_context : Map[String, String] // context -> synthetic window.location.href override
context_last_started_navigation : Map[String, String] // context -> last emitted navigationStarted id
context_requested_navigation_url : Map[String, String] // context -> last requested navigation URL
context_original_opener : Map[String, String]
context_realm : Map[String, String]
context_sandbox_realm : Map[String, String] // "::" -> realm
realm_context : Map[String, String] // realm -> context
context_worker_realms : Map[String, Array[String]] // context -> worker realms
realm_sandbox : Map[String, String] // sandbox realm -> sandbox name
context_parent : Map[String, String]
context_children : Map[String, Array[String]]
context_had_synthetic_child : Map[String, Bool] // top-level context -> has synthetic child helper
context_client_window : Map[String, String]
context_viewport_width : Map[String, Int] // context -> viewport width override
context_viewport_height : Map[String, Int] // context -> viewport height override
context_device_pixel_ratio : Map[String, Double] // context -> devicePixelRatio override
input_pressed_keys : Map[String, Array[String]] // context -> pressed key stack
input_pointer_x : Map[String, Double] // context -> current pointer x
input_pointer_y : Map[String, Double] // context -> current pointer y
input_pointer_buttons : Map[String, Int] // context -> mouse buttons bitfield
input_pointer_type : Map[String, String] // context -> last pointer type
input_pointer_target_shared : Map[String, String] // context -> last element-origin sharedId
input_pointer_down_x : Map[String, Double] // context -> primary pointerDown x
input_pointer_down_y : Map[String, Double] // context -> primary pointerDown y
input_pointer_down_target_shared : Map[String, String] // context -> primary pointerDown target
input_pointer_dragged : Map[String, Bool] // context -> whether pointer moved while primary button pressed
input_html_drag_source_shared : Map[String, String] // context -> active HTML draggable source sharedId
input_html_drag_active : Map[String, Bool] // context -> whether dragstart has been emitted
input_html_drag_over_shared : Map[String, String] // context -> current dragover target sharedId
input_html_drag_accept_shared : Map[String, String] // context -> dragover target that accepted drop
input_html_drag_transfer_id : Map[String, String] // context -> shared synthetic DataTransfer key
input_last_click_time : Map[String, Double] // context -> timestamp of last primary click release
input_last_click_x : Map[String, Double] // context -> x of last primary click release
input_last_click_y : Map[String, Double] // context -> y of last primary click release
input_last_click_target_shared : Map[String, String] // context -> target of last primary click release
input_pending_double_click : Map[String, Bool] // context -> whether current click sequence should emit dblclick
input_action_clock_ms : Map[String, Double] // context -> virtual input clock advanced by pause durations
input_synthetic_events_by_context : Map[String, Array[Json]] // context -> synthetic window.allEvents.events entries
context_synthetic_scrolled : Map[String, Bool] // context -> synthetic scroll fallback applied
speculation_prefetch_target_by_context : Map[String, String] // context -> last synthetic prefetch target
network_intercepts : Map[String, Json] // intercept id -> network.addIntercept options
mut next_network_intercept_id : Int
mut next_network_request_id : Int
mut network_cache_behavior_global : String
network_extra_headers_global : Map[String, String]
network_extra_headers_by_context : Map[String, Map[String, String]]
network_extra_headers_by_user_context : Map[String, Map[String, String]]
network_cache_behavior_by_context : Map[String, String]
network_state_channel_by_context : Map[String, String] // context -> script.message channel for offline/online notifications
network_cached_requests_by_context : Map[String, Map[String, Bool]]
network_blocked_requests : Map[String, Json] // request id -> synthetic blocked request payload
network_data_collectors : Map[String, Json] // collector id -> network.addDataCollector options
network_collected_data : Map[String, Map[String, Map[String, Json]]] // request -> collector -> dataType -> bytes value
mut next_network_data_collector_id : Int
mut socket : @core.Any? // WebSocket for async responses
mut socket_epoch : Int // Monotonic epoch for active WebSocket
use_quickjs : Bool // Use QuickJS sandboxed runtime for script execution
mut dom_initialized : Bool // Deno-side mock DOM initialized
mut preload_scripts : Array[PreloadScriptEntry]
}
///|
/// Create new BiDi protocol instance
fn BidiProtocol::new() -> BidiProtocol {
let user_contexts : Map[String, Bool] = {}
user_contexts["default"] = true
{
manager: CdpSessionManager::new(),
outbox: [],
default_context_id: None,
active_context_id: None,
next_navigation_id: 1,
next_realm_id: 1,
next_preload_script_id: 1,
subscriptions: {},
context_subscriptions: {},
user_context_subscriptions: {},
global_subscriptions: [],
global_unsubscribed_events: [],
subscription_events: {},
subscription_contexts: {},
subscription_user_contexts: {},
next_subscription_id: 1,
pending_log_entries: {},
user_contexts,
user_context_prompt_behavior: {},
global_download_behavior: None,
user_context_download_behavior: {},
installed_web_extensions: {},
next_web_extension_id: 1,
permission_state_by_scope: {},
emulation_user_agent_global: None,
emulation_user_agent_by_context: {},
emulation_user_agent_by_user_context: {},
emulation_locale_global: None,
emulation_locale_by_context: {},
emulation_locale_by_user_context: {},
emulation_timezone_global: None,
emulation_timezone_by_context: {},
emulation_timezone_by_user_context: {},
emulation_geolocation_global: None,
emulation_geolocation_by_context: {},
emulation_geolocation_by_user_context: {},
emulation_network_conditions_global: None,
emulation_network_conditions_by_context: {},
emulation_network_conditions_by_user_context: {},
emulation_screen_orientation_global: None,
emulation_screen_orientation_by_context: {},
emulation_screen_orientation_by_user_context: {},
emulation_screen_area_global: None,
emulation_screen_area_by_context: {},
emulation_screen_area_by_user_context: {},
geolocation_watch_channel_by_scope: {},
geolocation_watch_realm_by_scope: {},
geolocation_watch_notify_success_by_scope: {},
geolocation_watch_notify_error_by_scope: {},
next_geolocation_watch_id: 1,
bluetooth_adapter_state_by_context: {},
bluetooth_simulation_disabled_by_context: {},
bluetooth_preconnected_devices_by_context: {},
bluetooth_granted_devices_by_context: {},
bluetooth_pending_prompt_request: {},
bluetooth_pending_prompt_realm: {},
bluetooth_pending_prompt_unwrap_result: {},
bluetooth_pending_prompt_id: {},
bluetooth_pending_prompt_device_address: {},
bluetooth_pending_prompt_device_name: {},
bluetooth_gatt_connected_by_scope: {},
bluetooth_services_by_scope: {},
bluetooth_characteristics_by_scope: {},
bluetooth_descriptors_by_scope: {},
bluetooth_pending_gatt_request: {},
bluetooth_pending_gatt_realm: {},
bluetooth_pending_gatt_unwrap_result: {},
bluetooth_pending_gatt_address: {},
bluetooth_pending_gatt_response_kind: {},
bluetooth_pending_characteristic_request: {},
bluetooth_pending_characteristic_realm: {},
bluetooth_pending_characteristic_unwrap_result: {},
bluetooth_pending_characteristic_address: {},
bluetooth_pending_characteristic_service_uuid: {},
bluetooth_pending_characteristic_uuid: {},
bluetooth_pending_characteristic_type: {},
bluetooth_pending_descriptor_request: {},
bluetooth_pending_descriptor_realm: {},
bluetooth_pending_descriptor_unwrap_result: {},
bluetooth_pending_descriptor_address: {},
bluetooth_pending_descriptor_service_uuid: {},
bluetooth_pending_descriptor_characteristic_uuid: {},
bluetooth_pending_descriptor_uuid: {},
bluetooth_pending_descriptor_type: {},
next_bluetooth_prompt_id: 1,
user_context_viewport_width: {},
user_context_viewport_height: {},
user_context_device_pixel_ratio: {},
context_user_context: {},
context_has_beforeunload: {},
context_blocks_cross_origin_iframe_navigation: {},
pending_prompt_type: {},
pending_close_request: {},
pending_close_wait_for_destroyed: {},
pending_close_response_mode: {},
pending_prompt_request: {},
pending_prompt_realm: {},
pending_prompt_unwrap_result: {},
pending_beforeunload_request: {},
pending_beforeunload_url: {},
pending_beforeunload_requested_url: {},
pending_beforeunload_wait: {},
pending_beforeunload_navigation: {},
pending_beforeunload_finalize_mode: {},
pending_navigation_request: {},
pending_navigation_response_mode: {},
in_flight_navigation_url: {},
in_flight_navigation_id: {},
in_flight_navigation_started_at: {},
pending_prompt_result_var: {},
context_local_storage: {},
storage_cookies_by_scope: {},
synthetic_location_href_by_context: {},
context_last_started_navigation: {},
context_requested_navigation_url: {},
context_original_opener: {},
context_realm: {},
context_sandbox_realm: {},
realm_context: {},
context_worker_realms: {},
realm_sandbox: {},
context_parent: {},
context_children: {},
context_had_synthetic_child: {},
context_client_window: {},
context_viewport_width: {},
context_viewport_height: {},
context_device_pixel_ratio: {},
input_pressed_keys: {},
input_pointer_x: {},
input_pointer_y: {},
input_pointer_buttons: {},
input_pointer_type: {},
input_pointer_target_shared: {},
input_pointer_down_x: {},
input_pointer_down_y: {},
input_pointer_down_target_shared: {},
input_pointer_dragged: {},
input_html_drag_source_shared: {},
input_html_drag_active: {},
input_html_drag_over_shared: {},
input_html_drag_accept_shared: {},
input_html_drag_transfer_id: {},
input_last_click_time: {},
input_last_click_x: {},
input_last_click_y: {},
input_last_click_target_shared: {},
input_pending_double_click: {},
input_action_clock_ms: {},
input_synthetic_events_by_context: {},
context_synthetic_scrolled: {},
speculation_prefetch_target_by_context: {},
network_intercepts: {},
next_network_intercept_id: 1,
next_network_request_id: 1,
network_cache_behavior_global: "default",
network_extra_headers_global: {},
network_extra_headers_by_context: {},
network_extra_headers_by_user_context: {},
network_cache_behavior_by_context: {},
network_state_channel_by_context: {},
network_cached_requests_by_context: {},
network_blocked_requests: {},
network_data_collectors: {},
network_collected_data: {},
next_network_data_collector_id: 1,
socket: None,
socket_epoch: 0,
use_quickjs: false,
dom_initialized: false,
preload_scripts: [],
}
}
///|
/// Set WebSocket for async responses and return epoch token.
fn BidiProtocol::set_socket(self : BidiProtocol, socket : @core.Any) -> Int {
self.socket_epoch += 1
self.socket = Some(socket)
self.socket_epoch
}
///|
/// Clear WebSocket reference
fn BidiProtocol::clear_socket(self : BidiProtocol) -> Unit {
self.socket = None
}
///|
/// Check whether a captured WebSocket epoch is still active.
fn BidiProtocol::is_current_socket_epoch(
self : BidiProtocol,
epoch : Int,
) -> Bool {
self.socket_epoch == epoch
}
///|
/// Reset per-connection subscription state.
fn BidiProtocol::reset_subscription_state(self : BidiProtocol) -> Unit {
let legacy_keys : Array[String] = []
for key, _ in self.subscriptions {
legacy_keys.push(key)
}
for key in legacy_keys {
self.subscriptions.remove(key)
}
let context_keys : Array[String] = []
for key, _ in self.context_subscriptions {
context_keys.push(key)
}
for key in context_keys {
self.context_subscriptions.remove(key)
}
let user_context_keys : Array[String] = []
for key, _ in self.user_context_subscriptions {
user_context_keys.push(key)
}
for key in user_context_keys {
self.user_context_subscriptions.remove(key)
}
let subscription_keys : Array[String] = []
for key, _ in self.subscription_events {
subscription_keys.push(key)
}
for key in subscription_keys {
self.subscription_events.remove(key)
self.subscription_contexts.remove(key)
self.subscription_user_contexts.remove(key)
}
let pending_log_keys : Array[String] = []
for key, _ in self.pending_log_entries {
pending_log_keys.push(key)
}
for key in pending_log_keys {
self.pending_log_entries.remove(key)
}
self.global_subscriptions = []
self.global_unsubscribed_events = []
self.next_subscription_id = 1
}
///|
/// Reset per-session browsing-context and realm state.
fn BidiProtocol::reset_session_runtime_state(self : BidiProtocol) -> Unit {
self.reset_subscription_state()
for ctx_id in self.manager.list_sessions() {
self.manager.close_session(ctx_id) |> ignore
}
self.default_context_id = None
self.active_context_id = None
let user_context_keys : Array[String] = []
for key, _ in self.user_contexts {
user_context_keys.push(key)
}
for key in user_context_keys {
self.user_contexts.remove(key)
}
self.user_contexts["default"] = true
let prompt_behavior_keys : Array[String] = []
for key, _ in self.user_context_prompt_behavior {
prompt_behavior_keys.push(key)
}
for key in prompt_behavior_keys {
self.user_context_prompt_behavior.remove(key)
}
self.global_download_behavior = None
let download_behavior_keys : Array[String] = []
for key, _ in self.user_context_download_behavior {
download_behavior_keys.push(key)
}
for key in download_behavior_keys {
self.user_context_download_behavior.remove(key)
}
let permission_scope_keys : Array[String] = []
for key, _ in self.permission_state_by_scope {
permission_scope_keys.push(key)
}
for key in permission_scope_keys {
self.permission_state_by_scope.remove(key)
}
self.emulation_user_agent_global = None
let emulation_user_agent_context_keys : Array[String] = []
for key, _ in self.emulation_user_agent_by_context {
emulation_user_agent_context_keys.push(key)
}
for key in emulation_user_agent_context_keys {
self.emulation_user_agent_by_context.remove(key)
}
let emulation_user_agent_user_context_keys : Array[String] = []
for key, _ in self.emulation_user_agent_by_user_context {
emulation_user_agent_user_context_keys.push(key)
}
for key in emulation_user_agent_user_context_keys {
self.emulation_user_agent_by_user_context.remove(key)
}
self.emulation_locale_global = None
let emulation_locale_context_keys : Array[String] = []
for key, _ in self.emulation_locale_by_context {
emulation_locale_context_keys.push(key)
}
for key in emulation_locale_context_keys {
self.emulation_locale_by_context.remove(key)
}
let emulation_locale_user_context_keys : Array[String] = []
for key, _ in self.emulation_locale_by_user_context {
emulation_locale_user_context_keys.push(key)
}
for key in emulation_locale_user_context_keys {
self.emulation_locale_by_user_context.remove(key)
}
self.emulation_timezone_global = None
let emulation_timezone_context_keys : Array[String] = []
for key, _ in self.emulation_timezone_by_context {
emulation_timezone_context_keys.push(key)
}
for key in emulation_timezone_context_keys {
self.emulation_timezone_by_context.remove(key)
}
let emulation_timezone_user_context_keys : Array[String] = []
for key, _ in self.emulation_timezone_by_user_context {
emulation_timezone_user_context_keys.push(key)
}
for key in emulation_timezone_user_context_keys {
self.emulation_timezone_by_user_context.remove(key)
}
self.emulation_geolocation_global = None
let emulation_geolocation_context_keys : Array[String] = []
for key, _ in self.emulation_geolocation_by_context {
emulation_geolocation_context_keys.push(key)
}
for key in emulation_geolocation_context_keys {
self.emulation_geolocation_by_context.remove(key)
}
let emulation_geolocation_user_context_keys : Array[String] = []
for key, _ in self.emulation_geolocation_by_user_context {
emulation_geolocation_user_context_keys.push(key)
}
for key in emulation_geolocation_user_context_keys {
self.emulation_geolocation_by_user_context.remove(key)
}
self.emulation_network_conditions_global = None
let emulation_network_conditions_context_keys : Array[String] = []
for key, _ in self.emulation_network_conditions_by_context {
emulation_network_conditions_context_keys.push(key)
}
for key in emulation_network_conditions_context_keys {
self.emulation_network_conditions_by_context.remove(key)
}
let emulation_network_conditions_user_context_keys : Array[String] = []
for key, _ in self.emulation_network_conditions_by_user_context {
emulation_network_conditions_user_context_keys.push(key)
}
for key in emulation_network_conditions_user_context_keys {
self.emulation_network_conditions_by_user_context.remove(key)
}
self.emulation_screen_orientation_global = None
let emulation_screen_orientation_context_keys : Array[String] = []
for key, _ in self.emulation_screen_orientation_by_context {
emulation_screen_orientation_context_keys.push(key)
}
for key in emulation_screen_orientation_context_keys {
self.emulation_screen_orientation_by_context.remove(key)
}
let emulation_screen_orientation_user_context_keys : Array[String] = []
for key, _ in self.emulation_screen_orientation_by_user_context {
emulation_screen_orientation_user_context_keys.push(key)
}
for key in emulation_screen_orientation_user_context_keys {
self.emulation_screen_orientation_by_user_context.remove(key)
}
self.emulation_screen_area_global = None
let emulation_screen_area_context_keys : Array[String] = []
for key, _ in self.emulation_screen_area_by_context {
emulation_screen_area_context_keys.push(key)
}
for key in emulation_screen_area_context_keys {
self.emulation_screen_area_by_context.remove(key)
}
let emulation_screen_area_user_context_keys : Array[String] = []
for key, _ in self.emulation_screen_area_by_user_context {
emulation_screen_area_user_context_keys.push(key)
}
for key in emulation_screen_area_user_context_keys {
self.emulation_screen_area_by_user_context.remove(key)
}
let geolocation_watch_channel_keys : Array[String] = []
for key, _ in self.geolocation_watch_channel_by_scope {
geolocation_watch_channel_keys.push(key)
}
for key in geolocation_watch_channel_keys {
self.geolocation_watch_channel_by_scope.remove(key)
self.geolocation_watch_realm_by_scope.remove(key)
self.geolocation_watch_notify_success_by_scope.remove(key)
self.geolocation_watch_notify_error_by_scope.remove(key)
}
self.next_geolocation_watch_id = 1
let bluetooth_adapter_state_keys : Array[String] = []
for key, _ in self.bluetooth_adapter_state_by_context {
bluetooth_adapter_state_keys.push(key)
}
for key in bluetooth_adapter_state_keys {
self.bluetooth_adapter_state_by_context.remove(key)
}
let bluetooth_disabled_keys : Array[String] = []
for key, _ in self.bluetooth_simulation_disabled_by_context {
bluetooth_disabled_keys.push(key)
}
for key in bluetooth_disabled_keys {
self.bluetooth_simulation_disabled_by_context.remove(key)
}
let bluetooth_preconnected_keys : Array[String] = []
for key, _ in self.bluetooth_preconnected_devices_by_context {
bluetooth_preconnected_keys.push(key)
}
for key in bluetooth_preconnected_keys {
self.bluetooth_preconnected_devices_by_context.remove(key)
}
let bluetooth_granted_keys : Array[String] = []
for key, _ in self.bluetooth_granted_devices_by_context {
bluetooth_granted_keys.push(key)
}
for key in bluetooth_granted_keys {
self.bluetooth_granted_devices_by_context.remove(key)
}
let bluetooth_pending_request_keys : Array[String] = []
for key, _ in self.bluetooth_pending_prompt_request {
bluetooth_pending_request_keys.push(key)
}
for key in bluetooth_pending_request_keys {
self.bluetooth_pending_prompt_request.remove(key)
}
let bluetooth_pending_realm_keys : Array[String] = []
for key, _ in self.bluetooth_pending_prompt_realm {
bluetooth_pending_realm_keys.push(key)
}
for key in bluetooth_pending_realm_keys {
self.bluetooth_pending_prompt_realm.remove(key)
}
let bluetooth_pending_unwrap_keys : Array[String] = []
for key, _ in self.bluetooth_pending_prompt_unwrap_result {
bluetooth_pending_unwrap_keys.push(key)
}
for key in bluetooth_pending_unwrap_keys {
self.bluetooth_pending_prompt_unwrap_result.remove(key)
}
let bluetooth_pending_prompt_id_keys : Array[String] = []
for key, _ in self.bluetooth_pending_prompt_id {
bluetooth_pending_prompt_id_keys.push(key)
}
for key in bluetooth_pending_prompt_id_keys {
self.bluetooth_pending_prompt_id.remove(key)
}
let bluetooth_pending_address_keys : Array[String] = []
for key, _ in self.bluetooth_pending_prompt_device_address {
bluetooth_pending_address_keys.push(key)
}
for key in bluetooth_pending_address_keys {
self.bluetooth_pending_prompt_device_address.remove(key)
}
let bluetooth_pending_name_keys : Array[String] = []
for key, _ in self.bluetooth_pending_prompt_device_name {
bluetooth_pending_name_keys.push(key)
}
for key in bluetooth_pending_name_keys {
self.bluetooth_pending_prompt_device_name.remove(key)
}
let bluetooth_gatt_connected_keys : Array[String] = []
for key, _ in self.bluetooth_gatt_connected_by_scope {
bluetooth_gatt_connected_keys.push(key)
}
for key in bluetooth_gatt_connected_keys {
self.bluetooth_gatt_connected_by_scope.remove(key)
}
let bluetooth_service_scope_keys : Array[String] = []
for key, _ in self.bluetooth_services_by_scope {
bluetooth_service_scope_keys.push(key)
}
for key in bluetooth_service_scope_keys {
self.bluetooth_services_by_scope.remove(key)
}
let bluetooth_characteristic_scope_keys : Array[String] = []
for key, _ in self.bluetooth_characteristics_by_scope {
bluetooth_characteristic_scope_keys.push(key)
}
for key in bluetooth_characteristic_scope_keys {
self.bluetooth_characteristics_by_scope.remove(key)
}
let bluetooth_descriptor_scope_keys : Array[String] = []
for key, _ in self.bluetooth_descriptors_by_scope {
bluetooth_descriptor_scope_keys.push(key)
}
for key in bluetooth_descriptor_scope_keys {
self.bluetooth_descriptors_by_scope.remove(key)
}
let bluetooth_pending_gatt_request_keys : Array[String] = []
for key, _ in self.bluetooth_pending_gatt_request {
bluetooth_pending_gatt_request_keys.push(key)
}
for key in bluetooth_pending_gatt_request_keys {
self.bluetooth_pending_gatt_request.remove(key)
}
let bluetooth_pending_gatt_realm_keys : Array[String] = []
for key, _ in self.bluetooth_pending_gatt_realm {
bluetooth_pending_gatt_realm_keys.push(key)
}
for key in bluetooth_pending_gatt_realm_keys {
self.bluetooth_pending_gatt_realm.remove(key)
}
let bluetooth_pending_gatt_unwrap_keys : Array[String] = []
for key, _ in self.bluetooth_pending_gatt_unwrap_result {
bluetooth_pending_gatt_unwrap_keys.push(key)
}
for key in bluetooth_pending_gatt_unwrap_keys {
self.bluetooth_pending_gatt_unwrap_result.remove(key)
}
let bluetooth_pending_gatt_address_keys : Array[String] = []
for key, _ in self.bluetooth_pending_gatt_address {
bluetooth_pending_gatt_address_keys.push(key)
}
for key in bluetooth_pending_gatt_address_keys {
self.bluetooth_pending_gatt_address.remove(key)
}
let bluetooth_pending_gatt_kind_keys : Array[String] = []
for key, _ in self.bluetooth_pending_gatt_response_kind {
bluetooth_pending_gatt_kind_keys.push(key)
}
for key in bluetooth_pending_gatt_kind_keys {
self.bluetooth_pending_gatt_response_kind.remove(key)
}
let bluetooth_pending_characteristic_request_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_request {
bluetooth_pending_characteristic_request_keys.push(key)
}
for key in bluetooth_pending_characteristic_request_keys {
self.bluetooth_pending_characteristic_request.remove(key)
}
let bluetooth_pending_characteristic_realm_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_realm {
bluetooth_pending_characteristic_realm_keys.push(key)
}
for key in bluetooth_pending_characteristic_realm_keys {
self.bluetooth_pending_characteristic_realm.remove(key)
}
let bluetooth_pending_characteristic_unwrap_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_unwrap_result {
bluetooth_pending_characteristic_unwrap_keys.push(key)
}
for key in bluetooth_pending_characteristic_unwrap_keys {
self.bluetooth_pending_characteristic_unwrap_result.remove(key)
}
let bluetooth_pending_characteristic_address_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_address {
bluetooth_pending_characteristic_address_keys.push(key)
}
for key in bluetooth_pending_characteristic_address_keys {
self.bluetooth_pending_characteristic_address.remove(key)
}
let bluetooth_pending_characteristic_service_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_service_uuid {
bluetooth_pending_characteristic_service_keys.push(key)
}
for key in bluetooth_pending_characteristic_service_keys {
self.bluetooth_pending_characteristic_service_uuid.remove(key)
}
let bluetooth_pending_characteristic_uuid_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_uuid {
bluetooth_pending_characteristic_uuid_keys.push(key)
}
for key in bluetooth_pending_characteristic_uuid_keys {
self.bluetooth_pending_characteristic_uuid.remove(key)
}
let bluetooth_pending_characteristic_type_keys : Array[String] = []
for key, _ in self.bluetooth_pending_characteristic_type {
bluetooth_pending_characteristic_type_keys.push(key)
}
for key in bluetooth_pending_characteristic_type_keys {
self.bluetooth_pending_characteristic_type.remove(key)
}
let bluetooth_pending_descriptor_request_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_request {
bluetooth_pending_descriptor_request_keys.push(key)
}
for key in bluetooth_pending_descriptor_request_keys {
self.bluetooth_pending_descriptor_request.remove(key)
}
let bluetooth_pending_descriptor_realm_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_realm {
bluetooth_pending_descriptor_realm_keys.push(key)
}
for key in bluetooth_pending_descriptor_realm_keys {
self.bluetooth_pending_descriptor_realm.remove(key)
}
let bluetooth_pending_descriptor_unwrap_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_unwrap_result {
bluetooth_pending_descriptor_unwrap_keys.push(key)
}
for key in bluetooth_pending_descriptor_unwrap_keys {
self.bluetooth_pending_descriptor_unwrap_result.remove(key)
}
let bluetooth_pending_descriptor_address_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_address {
bluetooth_pending_descriptor_address_keys.push(key)
}
for key in bluetooth_pending_descriptor_address_keys {
self.bluetooth_pending_descriptor_address.remove(key)
}
let bluetooth_pending_descriptor_service_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_service_uuid {
bluetooth_pending_descriptor_service_keys.push(key)
}
for key in bluetooth_pending_descriptor_service_keys {
self.bluetooth_pending_descriptor_service_uuid.remove(key)
}
let bluetooth_pending_descriptor_characteristic_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_characteristic_uuid {
bluetooth_pending_descriptor_characteristic_keys.push(key)
}
for key in bluetooth_pending_descriptor_characteristic_keys {
self.bluetooth_pending_descriptor_characteristic_uuid.remove(key)
}
let bluetooth_pending_descriptor_uuid_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_uuid {
bluetooth_pending_descriptor_uuid_keys.push(key)
}
for key in bluetooth_pending_descriptor_uuid_keys {
self.bluetooth_pending_descriptor_uuid.remove(key)
}
let bluetooth_pending_descriptor_type_keys : Array[String] = []
for key, _ in self.bluetooth_pending_descriptor_type {
bluetooth_pending_descriptor_type_keys.push(key)
}
for key in bluetooth_pending_descriptor_type_keys {
self.bluetooth_pending_descriptor_type.remove(key)
}
self.next_bluetooth_prompt_id = 1
let user_context_viewport_width_keys : Array[String] = []
for key, _ in self.user_context_viewport_width {
user_context_viewport_width_keys.push(key)
}
for key in user_context_viewport_width_keys {
self.user_context_viewport_width.remove(key)
}
let user_context_viewport_height_keys : Array[String] = []
for key, _ in self.user_context_viewport_height {
user_context_viewport_height_keys.push(key)
}
for key in user_context_viewport_height_keys {
self.user_context_viewport_height.remove(key)
}
let user_context_device_pixel_ratio_keys : Array[String] = []
for key, _ in self.user_context_device_pixel_ratio {
user_context_device_pixel_ratio_keys.push(key)
}
for key in user_context_device_pixel_ratio_keys {
self.user_context_device_pixel_ratio.remove(key)
}
let context_user_keys : Array[String] = []
for key, _ in self.context_user_context {
context_user_keys.push(key)
}
for key in context_user_keys {
self.context_user_context.remove(key)
}
let context_beforeunload_keys : Array[String] = []
for key, _ in self.context_has_beforeunload {
context_beforeunload_keys.push(key)
}
for key in context_beforeunload_keys {
self.context_has_beforeunload.remove(key)
}
let context_policy_keys : Array[String] = []
for key, _ in self.context_blocks_cross_origin_iframe_navigation {
context_policy_keys.push(key)
}
for key in context_policy_keys {
self.context_blocks_cross_origin_iframe_navigation.remove(key)
}
let pending_prompt_keys : Array[String] = []
for key, _ in self.pending_prompt_type {
pending_prompt_keys.push(key)
}
for key in pending_prompt_keys {
self.pending_prompt_type.remove(key)
self.pending_close_request.remove(key)
self.pending_prompt_request.remove(key)
self.pending_prompt_realm.remove(key)
self.pending_beforeunload_request.remove(key)
self.pending_beforeunload_url.remove(key)
self.pending_beforeunload_requested_url.remove(key)
self.pending_beforeunload_wait.remove(key)
self.pending_beforeunload_navigation.remove(key)
self.pending_prompt_result_var.remove(key)
}
let in_flight_navigation_keys : Array[String] = []
for key, _ in self.in_flight_navigation_id {
in_flight_navigation_keys.push(key)
}
for key in in_flight_navigation_keys {
self.clear_pending_navigation_state(key)
}
let context_local_storage_keys : Array[String] = []
for key, _ in self.context_local_storage {
context_local_storage_keys.push(key)
}
for key in context_local_storage_keys {
self.context_local_storage.remove(key)
}
let storage_cookie_scope_keys : Array[String] = []
for key, _ in self.storage_cookies_by_scope {
storage_cookie_scope_keys.push(key)
}
for key in storage_cookie_scope_keys {
self.storage_cookies_by_scope.remove(key)
}
self.next_network_request_id = 1
let synthetic_location_keys : Array[String] = []
for key, _ in self.synthetic_location_href_by_context {
synthetic_location_keys.push(key)
}
for key in synthetic_location_keys {
self.synthetic_location_href_by_context.remove(key)
}
let last_navigation_keys : Array[String] = []
for key, _ in self.context_last_started_navigation {
last_navigation_keys.push(key)
}
for key in last_navigation_keys {
self.context_last_started_navigation.remove(key)
}
let requested_navigation_keys : Array[String] = []
for key, _ in self.context_requested_navigation_url {
requested_navigation_keys.push(key)
}
for key in requested_navigation_keys {
self.context_requested_navigation_url.remove(key)
}
let context_original_opener_keys : Array[String] = []
for key, _ in self.context_original_opener {
context_original_opener_keys.push(key)
}
for key in context_original_opener_keys {
self.context_original_opener.remove(key)
}
let context_realm_keys : Array[String] = []
for key, _ in self.context_realm {
context_realm_keys.push(key)
}
for key in context_realm_keys {
self.context_realm.remove(key)
}
let context_sandbox_realm_keys : Array[String] = []
for key, _ in self.context_sandbox_realm {
context_sandbox_realm_keys.push(key)
}
for key in context_sandbox_realm_keys {
self.context_sandbox_realm.remove(key)
}
let realm_context_keys : Array[String] = []
for key, _ in self.realm_context {
realm_context_keys.push(key)
}
for key in realm_context_keys {
self.realm_context.remove(key)
}
let context_worker_realm_keys : Array[String] = []
for key, _ in self.context_worker_realms {
context_worker_realm_keys.push(key)
}
for key in context_worker_realm_keys {
self.context_worker_realms.remove(key)
}
let realm_sandbox_keys : Array[String] = []
for key, _ in self.realm_sandbox {
realm_sandbox_keys.push(key)
}
for key in realm_sandbox_keys {
self.realm_sandbox.remove(key)
}
let context_parent_keys : Array[String] = []
for key, _ in self.context_parent {
context_parent_keys.push(key)
}
for key in context_parent_keys {
self.context_parent.remove(key)
}
let context_children_keys : Array[String] = []
for key, _ in self.context_children {
context_children_keys.push(key)
}
for key in context_children_keys {
self.context_children.remove(key)
}
let synthetic_child_context_keys : Array[String] = []
for key, _ in self.context_had_synthetic_child {
synthetic_child_context_keys.push(key)
}
for key in synthetic_child_context_keys {
self.context_had_synthetic_child.remove(key)
}
let context_client_window_keys : Array[String] = []
for key, _ in self.context_client_window {
context_client_window_keys.push(key)
}
for key in context_client_window_keys {
self.context_client_window.remove(key)
}
let network_state_channel_keys : Array[String] = []
for key, _ in self.network_state_channel_by_context {
network_state_channel_keys.push(key)
}
for key in network_state_channel_keys {
self.network_state_channel_by_context.remove(key)
}
let context_viewport_width_keys : Array[String] = []
for key, _ in self.context_viewport_width {
context_viewport_width_keys.push(key)
}
for key in context_viewport_width_keys {
self.context_viewport_width.remove(key)
}
let context_viewport_height_keys : Array[String] = []
for key, _ in self.context_viewport_height {
context_viewport_height_keys.push(key)
}
for key in context_viewport_height_keys {
self.context_viewport_height.remove(key)
}
let context_device_pixel_ratio_keys : Array[String] = []
for key, _ in self.context_device_pixel_ratio {
context_device_pixel_ratio_keys.push(key)
}
for key in context_device_pixel_ratio_keys {
self.context_device_pixel_ratio.remove(key)
}
let input_pressed_keys : Array[String] = []
for key, _ in self.input_pressed_keys {
input_pressed_keys.push(key)
}
for key in input_pressed_keys {
self.input_pressed_keys.remove(key)
}
let input_pointer_x_keys : Array[String] = []
for key, _ in self.input_pointer_x {
input_pointer_x_keys.push(key)
}
for key in input_pointer_x_keys {
self.input_pointer_x.remove(key)
}
let input_pointer_y_keys : Array[String] = []
for key, _ in self.input_pointer_y {
input_pointer_y_keys.push(key)
}
for key in input_pointer_y_keys {
self.input_pointer_y.remove(key)
}
let input_pointer_button_keys : Array[String] = []
for key, _ in self.input_pointer_buttons {
input_pointer_button_keys.push(key)
}
for key in input_pointer_button_keys {
self.input_pointer_buttons.remove(key)
}
let input_pointer_type_keys : Array[String] = []
for key, _ in self.input_pointer_type {
input_pointer_type_keys.push(key)
}
for key in input_pointer_type_keys {
self.input_pointer_type.remove(key)
}
let input_pointer_target_keys : Array[String] = []
for key, _ in self.input_pointer_target_shared {
input_pointer_target_keys.push(key)
}
for key in input_pointer_target_keys {
self.input_pointer_target_shared.remove(key)
}
let input_pointer_down_x_keys : Array[String] = []
for key, _ in self.input_pointer_down_x {
input_pointer_down_x_keys.push(key)
}
for key in input_pointer_down_x_keys {
self.input_pointer_down_x.remove(key)
}
let input_pointer_down_y_keys : Array[String] = []
for key, _ in self.input_pointer_down_y {
input_pointer_down_y_keys.push(key)
}
for key in input_pointer_down_y_keys {
self.input_pointer_down_y.remove(key)
}
let input_pointer_down_target_keys : Array[String] = []
for key, _ in self.input_pointer_down_target_shared {
input_pointer_down_target_keys.push(key)
}
for key in input_pointer_down_target_keys {
self.input_pointer_down_target_shared.remove(key)
}
let input_pointer_dragged_keys : Array[String] = []
for key, _ in self.input_pointer_dragged {
input_pointer_dragged_keys.push(key)
}
for key in input_pointer_dragged_keys {
self.input_pointer_dragged.remove(key)
}
let input_html_drag_source_keys : Array[String] = []
for key, _ in self.input_html_drag_source_shared {
input_html_drag_source_keys.push(key)
}
for key in input_html_drag_source_keys {
self.input_html_drag_source_shared.remove(key)
}
let input_html_drag_active_keys : Array[String] = []
for key, _ in self.input_html_drag_active {
input_html_drag_active_keys.push(key)
}
for key in input_html_drag_active_keys {
self.input_html_drag_active.remove(key)
}
let input_html_drag_over_keys : Array[String] = []
for key, _ in self.input_html_drag_over_shared {
input_html_drag_over_keys.push(key)
}
for key in input_html_drag_over_keys {
self.input_html_drag_over_shared.remove(key)
}
let input_html_drag_accept_keys : Array[String] = []
for key, _ in self.input_html_drag_accept_shared {
input_html_drag_accept_keys.push(key)
}
for key in input_html_drag_accept_keys {
self.input_html_drag_accept_shared.remove(key)
}
let input_html_drag_transfer_keys : Array[String] = []
for key, _ in self.input_html_drag_transfer_id {
input_html_drag_transfer_keys.push(key)
}
for key in input_html_drag_transfer_keys {
let transfer_id = self.input_html_drag_transfer_id.get(key).unwrap_or("")
if transfer_id != "" {
input_clear_drag_transfer(transfer_id)
}
self.input_html_drag_transfer_id.remove(key)
}
let input_last_click_time_keys : Array[String] = []
for key, _ in self.input_last_click_time {
input_last_click_time_keys.push(key)
}
for key in input_last_click_time_keys {
self.input_last_click_time.remove(key)
}
let input_last_click_x_keys : Array[String] = []
for key, _ in self.input_last_click_x {
input_last_click_x_keys.push(key)
}
for key in input_last_click_x_keys {
self.input_last_click_x.remove(key)
}
let input_last_click_y_keys : Array[String] = []
for key, _ in self.input_last_click_y {
input_last_click_y_keys.push(key)
}
for key in input_last_click_y_keys {
self.input_last_click_y.remove(key)
}
let input_last_click_target_keys : Array[String] = []
for key, _ in self.input_last_click_target_shared {
input_last_click_target_keys.push(key)
}
for key in input_last_click_target_keys {
self.input_last_click_target_shared.remove(key)
}
let input_pending_double_click_keys : Array[String] = []
for key, _ in self.input_pending_double_click {
input_pending_double_click_keys.push(key)
}
for key in input_pending_double_click_keys {
self.input_pending_double_click.remove(key)
}
let input_action_clock_keys : Array[String] = []
for key, _ in self.input_action_clock_ms {
input_action_clock_keys.push(key)
}
for key in input_action_clock_keys {
self.input_action_clock_ms.remove(key)
}
let input_synthetic_event_keys : Array[String] = []
for key, _ in self.input_synthetic_events_by_context {
input_synthetic_event_keys.push(key)
}
for key in input_synthetic_event_keys {
self.input_synthetic_events_by_context.remove(key)
}
let speculation_prefetch_target_keys : Array[String] = []
for key, _ in self.speculation_prefetch_target_by_context {
speculation_prefetch_target_keys.push(key)
}
for key in speculation_prefetch_target_keys {
self.speculation_prefetch_target_by_context.remove(key)
}
let intercept_keys : Array[String] = []
for key, _ in self.network_intercepts {
intercept_keys.push(key)
}
for key in intercept_keys {
self.network_intercepts.remove(key)
}
self.next_network_intercept_id = 1
self.clear_all_network_state()
let collector_keys : Array[String] = []
for key, _ in self.network_data_collectors {
collector_keys.push(key)
}
for key in collector_keys {
self.network_data_collectors.remove(key)
}
let collected_request_keys : Array[String] = []
for key, _ in self.network_collected_data {
collected_request_keys.push(key)
}
for key in collected_request_keys {
self.network_collected_data.remove(key)
}
self.next_network_data_collector_id = 1
self.next_preload_script_id = 1
let installed_web_extension_keys : Array[String] = []
for key, _ in self.installed_web_extensions {
installed_web_extension_keys.push(key)
}
for key in installed_web_extension_keys {
self.installed_web_extensions.remove(key)
}
self.next_web_extension_id = 1
self.preload_scripts = []
}
///|
/// Process incoming JSON message
fn BidiProtocol::process_message(
self : BidiProtocol,
json_str : String,
) -> Result[Unit, String] {
let json = @json.parse(json_str) catch { _ => return Err("Invalid JSON") }
let id = match get_int_field(json, "id") {
Some(i) => i
None => return Err("Missing id field")
}
let method_name = match get_string_field(json, "method") {
Some(m) => m
None => return Err("Missing method field")
}
let params = get_field(json, "params")
let request : BidiRequest = { id, method_name, params }
self.dispatch(request)
}
///|
/// Take and clear outgoing messages
fn BidiProtocol::take_messages(self : BidiProtocol) -> Array[BidiOutMessage] {
let out = self.outbox
self.outbox = []
out
}
///|
/// Convert outgoing message to JSON string
fn BidiOutMessage::to_json(self : BidiOutMessage) -> String {
match self {
Response(resp) => response_to_json(resp)
Event(evt) => event_to_json(evt)
Raw(raw) => raw
}
}
// =============================================================================
// Dispatch
// =============================================================================
///|
/// Dispatch request to method handler
fn BidiProtocol::dispatch(
self : BidiProtocol,
request : BidiRequest,
) -> Result[Unit, String] {
self.flush_expired_pending_navigation_requests()
let parts = split_method(request.method_name)
let (domain, action) = match parts {
Some(p) => p
None => return Err("Invalid method format")
}
match domain {
"session" => self.dispatch_session(request, action)
"browser" => self.dispatch_browser(request, action)
"browsingContext" => self.dispatch_browsing_context(request, action)
"script" => self.dispatch_script(request, action)
"input" => self.dispatch_input(request, action)
"network" => self.dispatch_network(request, action)
"emulation" => self.dispatch_emulation(request, action)
"bluetooth" => self.dispatch_bluetooth(request, action)
"permissions" => self.dispatch_permissions(request, action)
"storage" => self.dispatch_storage(request, action)
"webExtension" => self.dispatch_web_extension(request, action)
"log" => self.dispatch_log(request, action)
_ => {
self.send_error(
request.id,
"unknown command",
"Unknown method: " + request.method_name,
)
Ok(())
}
}
}
///|
/// Dispatch session domain
fn BidiProtocol::dispatch_session(
self : BidiProtocol,
request : BidiRequest,
action : String,
) -> Result[Unit, String] {
match action {
"status" => {
self.send_success(
request.id,
Some(
make_object({
"ready": Json::boolean(true),
"message": Json::string("Crater WebDriver BiDi is ready"),
}),
),
)
Ok(())
}
"new" => {
let mut default_prompt_behavior : Json? = None
match get_param_raw(request.params, "capabilities") {
Some(Object(capabilities)) =>
match capabilities.get("alwaysMatch") {
Some(Object(always_match)) =>
match always_match.get("unhandledPromptBehavior") {
Some(String(handler)) =>
match normalize_unhandled_prompt_handler_name(handler) {
Some(normalized) => {
let behavior : Map[String, Json] = {}
behavior["default"] = Json::string(normalized)
default_prompt_behavior = Some(make_object(behavior))
}
None => {
self.send_error(
request.id,
"invalid argument",
"unhandledPromptBehavior handler is invalid",
)
return Ok(())
}
}
Some(Object(behavior)) =>
match
self.validate_browser_unhandled_prompt_behavior(
request.id,
behavior,
) {
Some(_) => return Ok(())
None =>
default_prompt_behavior = Some(make_object(behavior))
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"unhandledPromptBehavior must be a string or object",
)
return Ok(())
}
None => ()
}
Some(_) => ()
None => ()
}
Some(_) => ()
None => ()
}
self.reset_session_runtime_state()
match default_prompt_behavior {
Some(behavior) =>
self.user_context_prompt_behavior["default"] = behavior
None => ()
}
let session_id = "bidi-" + request.id.to_string()
let ctx_id = self.ensure_default_context()
let caps = self.build_capabilities()
let result = make_object({
"sessionId": Json::string(session_id),
"capabilities": caps,
})
// Send response BEFORE events (BiDi protocol requirement)
self.send_success(request.id, Some(result))
// Emit initial context + realm events so Puppeteer can bind immediately.
self.emit_context_created(ctx_id, "tab", "about:blank", None, None)
self.emit_default_realm(ctx_id)
Ok(())
}
"subscribe" => {
match self.validate_session_subscribe_params(request.id, request.params) {
Some(_) => return Ok(())
None => ()
}
let subscription_id = self.handle_subscribe(request.params)
self.send_success(
request.id,
Some(make_object({ "subscription": Json::string(subscription_id) })),
)
Ok(())
}
"subscribeId" => {
match self.validate_session_subscribe_params(request.id, request.params) {
Some(_) => return Ok(())
None => ()
}
let subscription_id = self.handle_subscribe(request.params)
self.send_success(request.id, Some(Json::string(subscription_id)))
Ok(())
}
"unsubscribe" => {
match
self.validate_session_unsubscribe_params(request.id, request.params) {
Some(_) => return Ok(())
None => ()
}
self.handle_unsubscribe(request.params)
self.send_success(request.id, Some(make_object({})))
Ok(())
}
"isSubscribedForContext" => {
self.handle_session_is_subscribed_for_context(request.id, request.params)
Ok(())
}
"prepareBaselineContextForTest" => {
self.handle_session_prepare_baseline_context_for_test(
request.id,
request.params,
)
Ok(())
}
"getBaselineContextInfoForTest" => {
self.handle_session_get_baseline_context_info_for_test(
request.id,
request.params,
)
Ok(())
}
"getBaselineContextInfoValueForTest" => {
self.handle_session_get_baseline_context_info_value_for_test(
request.id,
request.params,
)
Ok(())
}
"resetForTest" => {
self.reset_session_runtime_state()
let ctx_id = self.ensure_default_context()
let realm_id = self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
self.send_success(
request.id,
Some(
make_object({
"context": Json::string(ctx_id),
"realm": Json::string(realm_id),
}),
),
)
Ok(())
}
"end" => {
self.reset_session_runtime_state()
self.send_success(request.id, Some(make_object({})))
Ok(())
}
_ => {
self.send_error(
request.id,
"unknown command",
"Unknown session method: " + action,
)
Ok(())
}
}
}
///|
/// Dispatch browser domain
fn BidiProtocol::dispatch_browser(
self : BidiProtocol,
request : BidiRequest,
action : String,
) -> Result[Unit, String] {
match action {
"getUserContexts" => {
let contexts = self.resolve_browser_user_contexts_list()
let result = make_object({ "userContexts": Json::array(contexts) })
self.send_success(request.id, Some(result))
Ok(())
}
"getUserContextsList" => {
let contexts = self.resolve_browser_user_contexts_list()
self.send_success(request.id, Some(Json::array(contexts)))
Ok(())
}
"hasUserContext" => {
self.handle_browser_has_user_context(request.id, request.params)
Ok(())
}
"hasUserContextValue" => {
self.handle_browser_has_user_context_value(request.id, request.params)
Ok(())
}
"getClientWindows" => {
let windows = self.resolve_browser_client_windows_list()
let result = make_object({ "clientWindows": Json::array(windows) })
self.send_success(request.id, Some(result))
Ok(())
}
"getClientWindowsList" => {
let windows = self.resolve_browser_client_windows_list()
self.send_success(request.id, Some(Json::array(windows)))
Ok(())
}
"createUserContext" =>
match
self.resolve_browser_create_user_context(request.id, request.params) {
Some(id) => {
let result = make_object({ "userContext": Json::string(id) })
self.send_success(request.id, Some(result))
Ok(())
}
None => Ok(())
}
"createUserContextId" => {
match
self.resolve_browser_create_user_context(request.id, request.params) {
Some(id) => self.send_success(request.id, Some(Json::string(id)))
None => ()
}
Ok(())
}
"removeUserContext" =>
match get_param_raw(request.params, "userContext") {
Some(String(id)) =>
if id == "default" {
self.send_error(
request.id,
"invalid argument",
"default user context cannot be removed",
)
Ok(())
} else if !self.user_contexts.contains(id) {
self.send_error(
request.id,
"no such user context",
"Unknown user context: " + id,
)
Ok(())
} else {
let contexts_to_close : Array[String] = []
for ctx_id, user_ctx in self.context_user_context {
if user_ctx == id && !self.context_parent.contains(ctx_id) {
contexts_to_close.push(ctx_id)
}
}
for ctx_id in contexts_to_close {
self.force_close_context_from_input(ctx_id) |> ignore
}
self.remove_storage_cookies_for_user_context(id)
self.remove_permission_state_for_user_context(id)
self.network_extra_headers_by_user_context.remove(id)
self.user_contexts.remove(id)
self.user_context_prompt_behavior.remove(id)
self.user_context_download_behavior.remove(id)
self.user_context_viewport_width.remove(id)
self.user_context_viewport_height.remove(id)
self.user_context_device_pixel_ratio.remove(id)
self.send_success(request.id, Some(make_object({})))
Ok(())
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"userContext must be a string",
)
Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing userContext")
Ok(())
}
}
"setDownloadBehavior" => {
match
self.validate_browser_set_download_behavior(request.id, request.params) {
Some(_) => return Ok(())
None => ()
}
let download_behavior : Json? = match
get_param_raw(request.params, "downloadBehavior") {
Some(Null) => None
Some(Object(behavior)) => Some(make_object(behavior))
_ => None
}
let target_user_contexts : Array[String] = []
match
get_param_raw_with_alias(
request.params,
"userContexts",
"user_contexts",
) {
Some(Array(user_contexts)) =>
for user_context in user_contexts {
match user_context {
String(id) => target_user_contexts.push(id)
_ => ()
}
}
_ => ()
}
if target_user_contexts.length() > 0 {
for user_context_id in target_user_contexts {
match download_behavior {
Some(behavior) =>
self.user_context_download_behavior[user_context_id] = behavior
None => self.user_context_download_behavior.remove(user_context_id)
}
}
} else {
self.global_download_behavior = download_behavior
}
self.send_success(request.id, Some(make_object({})))
Ok(())
}
"close" => {
self.send_success(request.id, Some(make_object({})))
Ok(())
}
_ => {
self.send_error(
request.id,
"unknown command",
"Unknown browser method: " + action,
)
Ok(())
}
}
}
///|
/// Dispatch browsingContext domain
fn BidiProtocol::dispatch_browsing_context(
self : BidiProtocol,
request : BidiRequest,
action : String,
) -> Result[Unit, String] {
match action {
"createSyntheticChildContext" => {
self.handle_browsing_context_create_synthetic_child_context(
request.id,
request.params,
)
Ok(())
}
"createAndGetInfo" => {
self.handle_browsing_context_create_and_get_info(
request.id,
request.params,
)
Ok(())
}
"createAndGetInfoValue" => {
self.handle_browsing_context_create_and_get_info_value(
request.id,
request.params,
)
Ok(())
}
"createContextId" => {
self.handle_browsing_context_create_context_id(request.id, request.params)
Ok(())
}
"getContextScopeInfo" => {
self.handle_browsing_context_get_context_scope_info(
request.id,
request.params,
)
Ok(())
}
"isKnownContext" => {
self.handle_browsing_context_is_known_context(request.id, request.params)
Ok(())
}
"getRequestedNavigationUrl" => {
self.handle_browsing_context_get_requested_navigation_url(
request.id,
request.params,
)
Ok(())
}
"getContextInfo" => {
self.handle_browsing_context_get_context_info(request.id, request.params)
Ok(())
}
"getTreeContexts" => {
self.handle_browsing_context_get_tree_contexts(request.id, request.params)
Ok(())
}
"getCurrentUrl" => {
self.handle_browsing_context_get_current_url(request.id, request.params)
Ok(())
}
"getCurrentUrlValue" => {
self.handle_browsing_context_get_current_url_value(
request.id,
request.params,
)
Ok(())
}
"closeWithState" => {
self.handle_browsing_context_close_with_state(request.id, request.params)
Ok(())
}
"closeResult" => {
self.handle_browsing_context_close_result(request.id, request.params)
Ok(())
}
"prepareNavigate" => {
self.handle_browsing_context_prepare_navigate(request.id, request.params)
Ok(())
}
"navigateWithState" => {
self.handle_browsing_context_navigate_with_state(
request.id,
request.params,
)
Ok(())
}
"finalizeNavigate" => {
self.handle_browsing_context_finalize_navigate(request.id, request.params)
Ok(())
}
"finalizeReload" => {
self.handle_browsing_context_finalize_reload(request.id, request.params)
Ok(())
}
"reloadWithState" => {
self.handle_browsing_context_reload_with_state(request.id, request.params)
Ok(())
}
"getCloseMetadata" => {
self.handle_browsing_context_get_close_metadata(
request.id,
request.params,
)
Ok(())
}
"getTree" =>
match self.resolve_get_tree_contexts(request.id, request.params) {
Some(contexts) => {
let result = make_object({ "contexts": Json::array(contexts) })
self.send_success(request.id, Some(result))
Ok(())
}
None => Ok(())
}
"create" => {
match
self.create_top_level_context_from_params(request.id, request.params) {
Some(ctx_id) =>
self.send_success(
request.id,
Some(make_object({ "context": Json::string(ctx_id) })),
)
None => ()
}
Ok(())
}
"navigate" => {
let ctx_id = match get_param_raw(request.params, "context") {
Some(String(value)) => value
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing context/url")
return Ok(())
}
}
let url = match get_param_raw(request.params, "url") {
Some(String(value)) => value
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"url must be a string",
)
return Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing context/url")
return Ok(())
}
}
let wait_mode = match get_param_raw(request.params, "wait") {
Some(String(value)) =>
if value == "none" || value == "interactive" || value == "complete" {
value
} else {
self.send_error(
request.id,
"invalid argument",
"wait must be one of none, interactive, or complete",
)
return Ok(())
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"wait must be a string",
)
return Ok(())
}
None => "none"
}
if !self.manager.has_session(ctx_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
return Ok(())
}
let resolved_url = self.resolve_effective_navigation_target_url(
ctx_id, url,
)
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
let _ = self.fail_in_flight_navigation(
ctx_id, "aborted", "Navigation interrupted by another navigation",
)
if !is_valid_navigation_target_url(resolved_url) {
self.send_error(request.id, "invalid argument", "url is invalid")
return Ok(())
}
if self.navigation_blocked_by_network_conditions(ctx_id, resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request.id,
"unknown error",
"Navigation failed: network offline",
)
return Ok(())
}
if should_fail_navigation_with_unknown_error(resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request.id,
"unknown error",
"Navigation failed: navigation target is unreachable",
)
return Ok(())
}
if self.is_iframe_navigation_blocked(ctx_id, resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.emit_navigation_failed_event(ctx_id, resolved_url, nav_id)
self.send_error(
request.id,
"unknown error",
"Navigation failed: blocked by embedding policy",
)
return Ok(())
}
match self.manager.get_session(ctx_id) {
Some(session) =>
if self.has_beforeunload_prompt_in_scope(ctx_id) {
let handler = self.resolve_unhandled_prompt_handler(
ctx_id, "beforeunload",
)
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.clear_pending_prompt_state(ctx_id)
self.pending_prompt_type[ctx_id] = "beforeunload"
self.pending_beforeunload_request[ctx_id] = request.id
self.pending_beforeunload_url[ctx_id] = resolved_url
self.pending_beforeunload_requested_url[ctx_id] = url
self.pending_beforeunload_wait[ctx_id] = wait_mode
self.pending_beforeunload_navigation[ctx_id] = nav_id
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.emit_user_prompt_opened(
ctx_id,
"beforeunload",
handler,
"beforeunload",
None,
)
Ok(())
} else {
let previous_url = session.get_url()
match session.navigate_to(resolved_url) {
Ok(_) => {
self.remember_requested_navigation_url(ctx_id, url)
if is_fragment_navigation(previous_url, resolved_url) {
self.complete_fragment_navigation_request(
request.id,
ctx_id,
resolved_url,
)
} else {
self.complete_navigation_request_with_wait(
request.id,
ctx_id,
resolved_url,
wait_mode,
)
}
Ok(())
}
Err(e) => {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request.id,
"unknown error",
"Navigation failed: " + e.message,
)
Ok(())
}
}
}
None => {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
Ok(())
}
}
}
"reload" => {
let ctx_id = match get_param_raw(request.params, "context") {
Some(String(value)) => value
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing context")
return Ok(())
}
}
let ignore_cache_raw = match
get_param_raw(request.params, "ignoreCache") {
Some(value) => Some(value)
None => get_param_raw(request.params, "ignore_cache")
}
let _ignore_cache = match ignore_cache_raw {
Some(True) => true
Some(False) => false
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"ignoreCache must be a boolean",
)
return Ok(())
}
None => false
}
let wait_mode = match get_param_raw(request.params, "wait") {
Some(String(value)) =>
if value == "none" || value == "interactive" || value == "complete" {
value
} else {
self.send_error(
request.id,
"invalid argument",
"wait must be one of none, interactive, or complete",
)
return Ok(())
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"wait must be a string",
)
return Ok(())
}
None => "none"
}
if !self.manager.has_session(ctx_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
return Ok(())
}
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
let _ = self.fail_in_flight_navigation(
ctx_id, "aborted", "Navigation interrupted by reload",
)
match self.manager.get_session(ctx_id) {
Some(session) => {
let url = session.get_url()
if self.has_beforeunload_prompt_in_scope(ctx_id) {
let handler = self.resolve_unhandled_prompt_handler(
ctx_id, "beforeunload",
)
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.clear_pending_prompt_state(ctx_id)
self.pending_prompt_type[ctx_id] = "beforeunload"
self.pending_beforeunload_request[ctx_id] = request.id
self.pending_beforeunload_url[ctx_id] = url
self.pending_beforeunload_requested_url[ctx_id] = self.requested_navigation_url(
ctx_id,
)
self.pending_beforeunload_wait[ctx_id] = wait_mode
self.pending_beforeunload_navigation[ctx_id] = nav_id
self.emit_navigation_started_event(ctx_id, url, nav_id)
self.emit_user_prompt_opened(
ctx_id,
"beforeunload",
handler,
"beforeunload",
None,
)
return Ok(())
}
self.complete_navigation_request_with_wait(
request.id,
ctx_id,
url,
wait_mode,
)
Ok(())
}
None => {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
Ok(())
}
}
}
"activate" =>
match get_string_param(request.params, "context") {
Some(ctx_id) =>
if self.context_parent.contains(ctx_id) {
self.send_error(
request.id,
"invalid argument",
"context must be top-level",
)
Ok(())
} else if self.manager.has_session(ctx_id) {
self.active_context_id = Some(ctx_id)
self.send_success(request.id, Some(make_object({})))
Ok(())
} else {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing context")
Ok(())
}
}
"close" =>
match get_param_raw(request.params, "context") {
Some(String(ctx_id)) => {
let prompt_unload = match
get_param_raw(request.params, "promptUnload") {
Some(True) => true
Some(False) => false
Some(Null) => false
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"promptUnload must be a boolean",
)
return Ok(())
}
None => false
}
if !self.manager.has_session(ctx_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
return Ok(())
}
if self.context_parent.contains(ctx_id) {
self.send_error(
request.id,
"invalid argument",
"context must be top-level",
)
return Ok(())
}
if prompt_unload && self.has_beforeunload_prompt_in_scope(ctx_id) {
let handler = self.resolve_unhandled_prompt_handler(
ctx_id, "beforeunload",
)
self.clear_pending_prompt_state(ctx_id)
self.pending_prompt_type[ctx_id] = "beforeunload"
self.pending_close_request[ctx_id] = request.id
self.emit_user_prompt_opened(
ctx_id,
"beforeunload",
handler,
"beforeunload",
None,
)
return Ok(())
}
// Send response BEFORE events (BiDi protocol requirement)
self.send_success(request.id, Some(make_object({})))
self.close_context_and_cleanup(ctx_id, true) |> ignore
Ok(())
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing context")
Ok(())
}
}
"handleUserPrompt" =>
match get_param_raw(request.params, "context") {
Some(String(ctx_id)) => {
if !self.manager.has_session(ctx_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
return Ok(())
}
let accepted = match get_param_raw(request.params, "accept") {
Some(True) => true
Some(False) => false
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"accept must be a boolean",
)
return Ok(())
}
None => true
}
let user_text = match
get_param_raw_with_alias(request.params, "userText", "user_text") {
Some(String(v)) => Some(v)
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"userText must be a string",
)
return Ok(())
}
None => None
}
match self.resolve_pending_prompt_context(ctx_id) {
Some(prompt_ctx_id) => {
let prompt_type = self.pending_prompt_type
.get(prompt_ctx_id)
.unwrap_or("alert")
let pending_close = self.pending_close_request.get(prompt_ctx_id)
let pending_script_request = self.pending_prompt_request.get(
prompt_ctx_id,
)
let pending_script_realm = self.pending_prompt_realm.get(
prompt_ctx_id,
)
let pending_prompt_unwrap_result = self.pending_prompt_unwrap_result
.get(prompt_ctx_id)
.unwrap_or(false)
let pending_beforeunload_request = self.pending_beforeunload_request.get(
prompt_ctx_id,
)
let pending_beforeunload_url = self.pending_beforeunload_url.get(
prompt_ctx_id,
)
let pending_beforeunload_requested_url = self.pending_beforeunload_requested_url.get(
prompt_ctx_id,
)
let pending_beforeunload_wait = self.pending_beforeunload_wait
.get(prompt_ctx_id)
.unwrap_or("none")
let pending_beforeunload_navigation = self.pending_beforeunload_navigation.get(
prompt_ctx_id,
)
let pending_beforeunload_finalize_mode = self.pending_beforeunload_finalize_mode.get(
prompt_ctx_id,
)
let pending_result_var = self.pending_prompt_result_var.get(
prompt_ctx_id,
)
self.emit_user_prompt_closed(
prompt_ctx_id, prompt_type, accepted, user_text,
)
match pending_close {
Some(close_request_id) => {
let close_response_mode = self.pending_close_response_mode
.get(prompt_ctx_id)
.unwrap_or("wait")
if close_response_mode == "empty" {
self.close_context_and_cleanup(prompt_ctx_id, true)
|> ignore
self.send_success(close_request_id, Some(make_object({})))
} else {
let close_result = match
self.pending_close_wait_for_destroyed.get(prompt_ctx_id) {
Some(wait_for_destroyed) =>
make_object({
"waitForDestroyed": Json::boolean(wait_for_destroyed),
})
None => make_object({})
}
self.send_success(close_request_id, Some(close_result))
self.close_context_and_cleanup(prompt_ctx_id, true)
|> ignore
}
}
None => {
match pending_result_var {
Some(var_name) =>
self.apply_pending_prompt_result_var(
prompt_ctx_id, var_name, prompt_type, accepted, user_text,
)
None => ()
}
match
(
pending_beforeunload_request, pending_beforeunload_url, pending_beforeunload_navigation,
) {
(Some(nav_request_id), Some(nav_url), pending_navigation_id) =>
if accepted {
match self.manager.get_session(prompt_ctx_id) {
Some(session) =>
match session.navigate_to(nav_url) {
Ok(_) => {
let requested_url = pending_beforeunload_requested_url.unwrap_or(
nav_url,
)
self.remember_requested_navigation_url(
prompt_ctx_id, requested_url,
)
match pending_beforeunload_finalize_mode {
Some("reloadWithState") => {
let completion = self.build_navigation_completion(
nav_request_id, prompt_ctx_id, nav_url, pending_beforeunload_wait,
pending_navigation_id,
)
let _ = self.emit_synthetic_navigation_request_sequence(
prompt_ctx_id,
requested_url,
Some(completion.navigation_id),
)
let blocked = self.has_network_blocked_navigation_request(
prompt_ctx_id,
Some(completion.navigation_id),
)
if blocked {
self.strip_pending_navigation_lifecycle_events(
prompt_ctx_id,
nav_url,
completion.navigation_id,
)
self.clear_pending_navigation_state(
prompt_ctx_id,
)
} else if !completion.deferred_response {
self.send_success(
nav_request_id,
Some(completion.result),
)
}
}
Some("navigateWithState") =>
self.complete_navigate_with_state(
nav_request_id, prompt_ctx_id, requested_url,
nav_url, "", pending_beforeunload_wait, pending_navigation_id,
)
_ =>
self.complete_navigation_request_with_wait_and_navigation(
nav_request_id, prompt_ctx_id, nav_url, pending_beforeunload_wait,
pending_navigation_id,
)
}
}
Err(e) => {
match pending_navigation_id {
Some(nav_id) =>
self.emit_navigation_failed_event(
prompt_ctx_id, nav_url, nav_id,
)
None => ()
}
self.send_error(
nav_request_id,
"unknown error",
"Navigation failed: " + e.message,
)
}
}
None =>
self.send_error(
nav_request_id,
"no such frame",
"Unknown context: " + prompt_ctx_id,
)
}
} else {
match pending_navigation_id {
Some(nav_id) =>
self.emit_navigation_failed_event(
prompt_ctx_id, nav_url, nav_id,
)
None => ()
}
self.send_error(
nav_request_id, "unknown error", "Navigation canceled by beforeunload prompt",
)
}
_ => ()
}
match pending_script_request {
Some(eval_request_id) => {
let realm_id = match pending_script_realm {
Some(realm) => realm
None =>
self.context_realm
.get(prompt_ctx_id)
.unwrap_or("realm-" + prompt_ctx_id)
}
self.send_script_prompt_result(
eval_request_id, realm_id, prompt_type, accepted, user_text,
pending_prompt_unwrap_result,
)
}
None => ()
}
self.clear_pending_prompt_state(prompt_ctx_id)
}
}
self.send_success(request.id, Some(make_object({})))
Ok(())
}
None => {
self.send_error(
request.id,
"no such alert",
"No dialog is showing",
)
Ok(())
}
}
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
Ok(())
}
None => {
self.send_error(request.id, "invalid argument", "Missing context")
Ok(())
}
}
"captureScreenshot" => {
self.handle_capture_screenshot(request)
Ok(())
}
"captureScreenshotData" => {
self.handle_capture_screenshot_data(request.id, request.params)
Ok(())
}
"capturePaintData" => {
self.handle_capture_paint_data(request.id, request.params)
Ok(())
}
"capturePaintTree" => {
self.handle_capture_paint_tree(request.id, request.params)
Ok(())
}
"getResponsiveBreakpoints" => {
self.handle_get_responsive_breakpoints(request.id, request.params)
Ok(())
}
"getCssRuleViewportMap" => {
self.handle_get_css_rule_viewport_map(request.id, request.params)
Ok(())
}
"getCssRuleUsage" => {
self.handle_get_css_rule_usage(request.id, request.params)
Ok(())
}
"getRequiredTestViewports" => {
self.handle_get_required_test_viewports(request.id, request.params)
Ok(())
}
"batchRender" => {
self.handle_batch_render(request.id, request.params)
Ok(())
}
"getComputedStyles" => {
self.handle_get_computed_styles(request.id, request.params)
Ok(())
}
"getAllComputedStyles" => {
self.handle_get_all_computed_styles(request.id, request.params)
Ok(())
}
"getComputedStylesWithState" => {
self.handle_get_computed_styles_with_state(request.id, request.params)
Ok(())
}
"print" => {
self.handle_print(request)
Ok(())
}
"printData" => {
self.handle_print_data(request.id, request.params)
Ok(())
}
"locateNodes" => {
self.handle_locate_nodes(request)
Ok(())
}
"setViewport" => {
self.handle_set_viewport(request)
Ok(())
}
"traverseHistory" => {
self.handle_traverse_history(request)
Ok(())
}
_ => {
self.send_error(
request.id,
"unknown command",
"Unknown browsingContext method: " + action,
)
Ok(())
}
}
}
///|
fn BidiProtocol::handle_browsing_context_create_synthetic_child_context(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
None =>
match get_map_field_with_alias(map, "parentContext", "parent_context") {
Some(String(context_id)) => context_id
_ => ""
}
_ => {
self.send_error(
request_id, "invalid argument", "parentContext must be a string",
)
return
}
}
if context_id == "" {
self.send_error(
request_id, "invalid argument", "parentContext must be a string",
)
return
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown parent context: " + context_id,
)
return
}
let user_context_id = match
get_map_field_with_alias(map, "userContext", "user_context") {
Some(String(user_context_id)) =>
if user_context_id == "" || !self.user_contexts.contains(user_context_id) {
self.send_error(
request_id,
"no such user context",
"Unknown user context: " + user_context_id,
)
return
} else {
user_context_id
}
Some(Null) | None =>
self.context_user_context.get(context_id).unwrap_or("default")
Some(_) => {
self.send_error(
request_id, "invalid argument", "userContext must be a string",
)
return
}
}
let requested_url = match map.get("url") {
Some(String(url)) => url
Some(Null) | None => "about:blank"
Some(_) => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
let result = self.create_synthetic_child_context_result(
context_id, requested_url, user_context_id,
)
self.send_success(request_id, Some(result))
}
///|
fn BidiProtocol::handle_browsing_context_get_context_scope_info(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
self.send_success(
request_id,
Some(self.build_context_scope_info_json(context_id)),
)
}
///|
fn BidiProtocol::handle_browsing_context_is_known_context(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
self.send_success(
request_id,
Some(Json::boolean(self.manager.has_session(context_id))),
)
}
///|
fn BidiProtocol::handle_browsing_context_get_requested_navigation_url(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
self.send_success(
request_id,
Some(
make_object({
"url": Json::string(self.requested_navigation_url(context_id)),
}),
),
)
}
///|
fn BidiProtocol::handle_browsing_context_get_context_info(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
match self.build_context_entry(context_id, true, 0, Some(0)) {
Some(entry) => self.send_success(request_id, Some(entry))
None =>
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
}
}
///|
fn BidiProtocol::handle_browsing_context_get_tree_contexts(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_get_tree_contexts(request_id, params) {
Some(contexts) => self.send_success(request_id, Some(Json::array(contexts)))
None => ()
}
}
///|
fn BidiProtocol::handle_browsing_context_get_current_url(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_current_context_url(request_id, params) {
Some((context_id, url)) =>
self.send_success(
request_id,
Some(
make_object({
"context": Json::string(context_id),
"url": Json::string(url),
}),
),
)
None => ()
}
}
///|
fn BidiProtocol::handle_browsing_context_get_current_url_value(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_current_context_url(request_id, params) {
Some((_, url)) => self.send_success(request_id, Some(Json::string(url)))
None => ()
}
}
///|
fn BidiProtocol::resolve_current_context_url(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> (String, String)? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return None
}
Some((context_id, self.current_context_url(context_id)))
}
///|
fn BidiProtocol::handle_browsing_context_create_and_get_info(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.create_top_level_context_from_params(request_id, params) {
Some(ctx_id) =>
match self.build_context_entry(ctx_id, true, 0, Some(0)) {
Some(entry) => self.send_success(request_id, Some(entry))
None =>
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
}
None => ()
}
}
///|
fn BidiProtocol::handle_browsing_context_create_and_get_info_value(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.create_top_level_context_from_params(request_id, params) {
Some(ctx_id) =>
match self.build_context_entry(ctx_id, true, 0, Some(0)) {
Some(entry) => self.send_success(request_id, Some(entry))
None =>
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
}
None => ()
}
}
///|
fn BidiProtocol::handle_browsing_context_create_context_id(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.create_top_level_context_from_params(request_id, params) {
Some(ctx_id) => self.send_success(request_id, Some(Json::string(ctx_id)))
None => ()
}
}
///|
fn BidiProtocol::create_top_level_context_from_params(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> String? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let type_hint = match get_map_field_with_alias(map, "type", "type_hint") {
Some(String(kind)) => kind
Some(_) => {
self.send_error(request_id, "invalid argument", "type must be a string")
return None
}
None => "tab"
}
if type_hint != "tab" && type_hint != "window" {
self.send_error(
request_id, "invalid argument", "type must be 'tab' or 'window'",
)
return None
}
let background = match map.get("background") {
Some(True) => true
Some(False) => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "background must be a boolean",
)
return None
}
None => false
}
let reference_context = match
get_map_field_with_alias(map, "referenceContext", "reference_context") {
Some(String(reference_ctx_id)) => {
if !self.manager.has_session(reference_ctx_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + reference_ctx_id,
)
return None
}
if self.context_parent.contains(reference_ctx_id) {
self.send_error(
request_id, "invalid argument", "referenceContext must be top-level",
)
return None
}
Some(reference_ctx_id)
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "referenceContext must be a string",
)
return None
}
None => None
}
let requested_user_context = match
get_map_field_with_alias(map, "userContext", "user_context") {
Some(String(user_ctx_id)) =>
if user_ctx_id == "" || !self.user_contexts.contains(user_ctx_id) {
self.send_error(
request_id,
"no such user context",
"Unknown user context: " + user_ctx_id,
)
return None
} else {
Some(user_ctx_id)
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "userContext must be a string",
)
return None
}
None => None
}
let user_ctx = match requested_user_context {
Some(user_ctx_id) => user_ctx_id
None =>
match reference_context {
Some(reference_ctx_id) =>
self.context_user_context.get(reference_ctx_id).unwrap_or("default")
None => "default"
}
}
let previous_active_context = self.active_context_id
let ctx_id = self.create_context(user_ctx, type_hint)
if type_hint == "window" {
match reference_context {
Some(reference_ctx_id) =>
match self.context_client_window.get(reference_ctx_id) {
Some(reference_client_window) =>
self.context_client_window[ctx_id] = reference_client_window
None => ()
}
None => ()
}
}
if background {
self.active_context_id = previous_active_context
}
self.emit_context_created(ctx_id, type_hint, "about:blank", None, None)
self.emit_default_realm(ctx_id)
self.apply_preload_scripts_for_context(ctx_id)
Some(ctx_id)
}
///|
fn BidiProtocol::handle_browsing_context_prepare_navigate(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
let url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
self.clear_synthetic_location_href(context_id)
match self.prepare_navigation_request_internal(request_id, context_id, url) {
Some(result) => self.send_success(request_id, Some(result))
None => ()
}
}
///|
fn BidiProtocol::handle_browsing_context_finalize_navigate(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
let url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
let navigation_id : String? = match map.get("navigation") {
Some(String(navigation_id)) => Some(navigation_id)
Some(Null) | None => None
Some(_) => {
self.send_error(
request_id, "invalid argument", "navigation must be a string",
)
return
}
}
let emitted = self.emit_synthetic_navigation_request_sequence(
context_id, url, navigation_id,
)
self.send_success(
request_id,
Some(
make_object({
"emitted": Json::number(emitted.to_double()),
"blocked": Json::boolean(
self.has_network_blocked_navigation_request(context_id, navigation_id),
),
}),
),
)
}
///|
fn BidiProtocol::send_navigation_state_result(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
requested_url : String,
result_url : String,
navigation_id : String,
result : Json,
) -> Unit {
self.send_navigation_result_with_mode(
request_id, ctx_id, requested_url, result_url, navigation_id, result, "state",
)
}
///|
fn BidiProtocol::send_navigation_url_result(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
requested_url : String,
result_url : String,
navigation_id : String,
) -> Unit {
self.send_navigation_result_with_mode(
request_id,
ctx_id,
requested_url,
result_url,
navigation_id,
Json::string(result_url),
"url",
)
}
///|
fn BidiProtocol::send_navigation_result_with_mode(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
requested_url : String,
result_url : String,
navigation_id : String,
result : Json,
response_mode : String,
) -> Unit {
let _ = self.emit_synthetic_navigation_request_sequence(
ctx_id,
requested_url,
Some(navigation_id),
)
if self.has_network_blocked_navigation_request(ctx_id, Some(navigation_id)) {
self.strip_pending_navigation_lifecycle_events(
ctx_id, result_url, navigation_id,
)
if response_mode == "url" {
self.pending_navigation_response_mode[ctx_id] = "url"
} else {
self.pending_navigation_response_mode.remove(ctx_id)
}
self.register_in_flight_navigation(
ctx_id,
result_url,
navigation_id,
Some(request_id),
)
} else {
self.pending_navigation_response_mode.remove(ctx_id)
self.send_success(request_id, Some(result))
}
}
///|
fn BidiProtocol::complete_navigate_with_state(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
requested_url : String,
resolved_url : String,
previous_url : String,
wait_mode : String,
navigation_id : String?,
) -> Unit {
self.remember_requested_navigation_url(ctx_id, requested_url)
if is_fragment_navigation(previous_url, resolved_url) {
self.clear_pending_navigation_state(ctx_id)
let nav_id = match navigation_id {
Some(id) => id
None => {
let generated_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
generated_id
}
}
self.emit_fragment_navigated_event(ctx_id, resolved_url, Some(nav_id))
self.emit_history_updated_event(ctx_id, resolved_url)
let result = make_object({
"navigation": Json::string(nav_id),
"url": Json::string(resolved_url),
})
self.send_navigation_state_result(
request_id, ctx_id, requested_url, resolved_url, nav_id, result,
)
return
}
let completion = self.build_navigation_completion(
request_id, ctx_id, resolved_url, wait_mode, navigation_id,
)
if completion.deferred_response {
return
}
self.send_navigation_state_result(
request_id,
ctx_id,
requested_url,
resolved_url,
completion.navigation_id,
completion.result,
)
}
///|
fn BidiProtocol::handle_browsing_context_navigate_with_state(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let ctx_id = match get_param_raw(params, "context") {
Some(String(value)) => value
Some(_) => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
None => {
self.send_error(request_id, "invalid argument", "Missing context")
return
}
}
let url = match get_param_raw(params, "url") {
Some(String(value)) => value
Some(_) => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
None => {
self.send_error(request_id, "invalid argument", "Missing url")
return
}
}
let wait_mode = match get_param_raw(params, "wait") {
Some(String(value)) =>
if value == "none" || value == "interactive" || value == "complete" {
value
} else {
self.send_error(
request_id, "invalid argument", "wait must be one of none, interactive, or complete",
)
return
}
Some(_) => {
self.send_error(request_id, "invalid argument", "wait must be a string")
return
}
None => "none"
}
if !self.manager.has_session(ctx_id) {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return
}
match self.prepare_navigation_request_internal(request_id, ctx_id, url) {
Some(prepared) =>
match get_field(prepared, "blocked") {
Some(True) => return
_ => ()
}
None => return
}
let resolved_url = self.resolve_effective_navigation_target_url(ctx_id, url)
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
let _ = self.fail_in_flight_navigation(
ctx_id, "aborted", "Navigation interrupted by another navigation",
)
if !is_valid_navigation_target_url(resolved_url) {
self.send_error(request_id, "invalid argument", "url is invalid")
return
}
if self.navigation_blocked_by_network_conditions(ctx_id, resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id, "unknown error", "Navigation failed: network offline",
)
return
}
if should_fail_navigation_with_unknown_error(resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id, "unknown error", "Navigation failed: navigation target is unreachable",
)
return
}
if self.is_iframe_navigation_blocked(ctx_id, resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.emit_navigation_failed_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id, "unknown error", "Navigation failed: blocked by embedding policy",
)
return
}
match self.manager.get_session(ctx_id) {
Some(session) =>
if self.has_beforeunload_prompt_in_scope(ctx_id) {
let handler = self.resolve_unhandled_prompt_handler(
ctx_id, "beforeunload",
)
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.clear_pending_prompt_state(ctx_id)
self.pending_prompt_type[ctx_id] = "beforeunload"
self.pending_beforeunload_request[ctx_id] = request_id
self.pending_beforeunload_url[ctx_id] = resolved_url
self.pending_beforeunload_requested_url[ctx_id] = url
self.pending_beforeunload_wait[ctx_id] = wait_mode
self.pending_beforeunload_navigation[ctx_id] = nav_id
self.pending_beforeunload_finalize_mode[ctx_id] = "navigateWithState"
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.emit_user_prompt_opened(
ctx_id,
"beforeunload",
handler,
"beforeunload",
None,
)
return
} else {
let previous_url = session.get_url()
match session.navigate_to(resolved_url) {
Ok(_) =>
self.complete_navigate_with_state(
request_id,
ctx_id,
url,
resolved_url,
previous_url,
wait_mode,
None,
)
Err(e) => {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id,
"unknown error",
"Navigation failed: " + e.message,
)
}
}
}
None => {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return
}
}
}
///|
fn BidiProtocol::handle_browsing_context_finalize_reload(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
let navigation_id : String? = match map.get("navigation") {
Some(String(navigation_id)) => Some(navigation_id)
Some(Null) | None => None
Some(_) => {
self.send_error(
request_id, "invalid argument", "navigation must be a string",
)
return
}
}
let url = self.requested_navigation_url(context_id)
let emitted = self.emit_synthetic_navigation_request_sequence(
context_id, url, navigation_id,
)
self.send_success(
request_id,
Some(
make_object({
"url": Json::string(url),
"emitted": Json::number(emitted.to_double()),
"blocked": Json::boolean(
self.has_network_blocked_navigation_request(context_id, navigation_id),
),
}),
),
)
}
///|
fn BidiProtocol::handle_browsing_context_reload_with_state(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let ctx_id = match get_param_raw(params, "context") {
Some(String(value)) => value
Some(_) => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
None => {
self.send_error(request_id, "invalid argument", "Missing context")
return
}
}
let ignore_cache_raw = match get_param_raw(params, "ignoreCache") {
Some(value) => Some(value)
None => get_param_raw(params, "ignore_cache")
}
let _ignore_cache = match ignore_cache_raw {
Some(True) => true
Some(False) => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "ignoreCache must be a boolean",
)
return
}
None => false
}
let wait_mode = match get_param_raw(params, "wait") {
Some(String(value)) =>
if value == "none" || value == "interactive" || value == "complete" {
value
} else {
self.send_error(
request_id, "invalid argument", "wait must be one of none, interactive, or complete",
)
return
}
Some(_) => {
self.send_error(request_id, "invalid argument", "wait must be a string")
return
}
None => "none"
}
if !self.manager.has_session(ctx_id) {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return
}
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
let _ = self.fail_in_flight_navigation(
ctx_id, "aborted", "Navigation interrupted by reload",
)
match self.manager.get_session(ctx_id) {
Some(session) => {
let current_url = session.get_url()
let requested_url = self.requested_navigation_url(ctx_id)
let url = if requested_url != "" { requested_url } else { current_url }
if self.has_beforeunload_prompt_in_scope(ctx_id) {
let handler = self.resolve_unhandled_prompt_handler(
ctx_id, "beforeunload",
)
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.clear_pending_prompt_state(ctx_id)
self.pending_prompt_type[ctx_id] = "beforeunload"
self.pending_beforeunload_request[ctx_id] = request_id
self.pending_beforeunload_url[ctx_id] = url
self.pending_beforeunload_requested_url[ctx_id] = url
self.pending_beforeunload_wait[ctx_id] = wait_mode
self.pending_beforeunload_navigation[ctx_id] = nav_id
self.pending_beforeunload_finalize_mode[ctx_id] = "reloadWithState"
self.emit_navigation_started_event(ctx_id, url, nav_id)
self.emit_user_prompt_opened(
ctx_id,
"beforeunload",
handler,
"beforeunload",
None,
)
return
}
let completion = self.build_navigation_completion(
request_id,
ctx_id,
url,
wait_mode,
None,
)
let _ = self.emit_synthetic_navigation_request_sequence(
ctx_id,
requested_url,
Some(completion.navigation_id),
)
let blocked = self.has_network_blocked_navigation_request(
ctx_id,
Some(completion.navigation_id),
)
if blocked {
self.strip_pending_navigation_lifecycle_events(
ctx_id,
url,
completion.navigation_id,
)
self.register_in_flight_navigation(
ctx_id,
url,
completion.navigation_id,
Some(request_id),
)
return
}
if !completion.deferred_response {
self.send_success(request_id, Some(completion.result))
}
}
None => {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return
}
}
}
///|
fn BidiProtocol::close_wait_for_destroyed(
self : BidiProtocol,
context_id : String,
) -> Bool {
self.context_children.get(context_id).unwrap_or([]).length() > 0 ||
self.context_had_synthetic_child.get(context_id).unwrap_or(false)
}
///|
fn BidiProtocol::handle_browsing_context_get_close_metadata(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
if self.context_parent.contains(context_id) {
self.send_error(request_id, "invalid argument", "context must be top-level")
return
}
let wait_for_destroyed = self.close_wait_for_destroyed(context_id)
self.send_success(
request_id,
Some(make_object({ "waitForDestroyed": Json::boolean(wait_for_destroyed) })),
)
}
///|
fn BidiProtocol::handle_browsing_context_close_with_state(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
self.handle_browsing_context_close_mode(request_id, params, true)
}
///|
fn BidiProtocol::handle_browsing_context_close_result(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
self.handle_browsing_context_close_mode(request_id, params, false)
}
///|
fn BidiProtocol::handle_browsing_context_close_mode(
self : BidiProtocol,
request_id : Int,
params : Json?,
include_wait_for_destroyed : Bool,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
let prompt_unload = match
get_map_field_with_alias(map, "promptUnload", "prompt_unload") {
Some(True) => true
Some(False) | Some(Null) | None => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "promptUnload must be a boolean",
)
return
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return
}
if self.context_parent.contains(context_id) {
self.send_error(request_id, "invalid argument", "context must be top-level")
return
}
let wait_for_destroyed = self.close_wait_for_destroyed(context_id)
if prompt_unload && self.has_beforeunload_prompt_in_scope(context_id) {
let handler = self.resolve_unhandled_prompt_handler(
context_id, "beforeunload",
)
self.clear_pending_prompt_state(context_id)
self.pending_prompt_type[context_id] = "beforeunload"
self.pending_close_request[context_id] = request_id
self.pending_close_wait_for_destroyed[context_id] = wait_for_destroyed
self.pending_close_response_mode[context_id] = if include_wait_for_destroyed {
"wait"
} else {
"empty"
}
self.emit_user_prompt_opened(
context_id,
"beforeunload",
handler,
"beforeunload",
None,
)
return
}
if include_wait_for_destroyed {
self.send_success(
request_id,
Some(
make_object({ "waitForDestroyed": Json::boolean(wait_for_destroyed) }),
),
)
self.close_context_and_cleanup(context_id, true) |> ignore
} else {
self.close_context_and_cleanup(context_id, true) |> ignore
self.send_success(request_id, Some(make_object({})))
}
}
///|
fn BidiProtocol::handle_browser_has_user_context(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_browser_has_user_context_known(request_id, params) {
Some(known) =>
self.send_success(
request_id,
Some(make_object({ "known": Json::boolean(known) })),
)
None => ()
}
}
///|
fn BidiProtocol::handle_browser_has_user_context_value(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_browser_has_user_context_known(request_id, params) {
Some(known) => self.send_success(request_id, Some(Json::boolean(known)))
None => ()
}
}
///|
fn BidiProtocol::resolve_browser_has_user_context_known(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Bool? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let user_context = match map.get("userContext") {
Some(String(user_context)) => user_context
_ => {
self.send_error(
request_id, "invalid argument", "userContext must be a string",
)
return None
}
}
Some(self.user_contexts.contains(user_context))
}
///|
fn BidiProtocol::resolve_browser_user_contexts_list(
self : BidiProtocol,
) -> Array[Json] {
let contexts : Array[Json] = []
for id, _ in self.user_contexts {
contexts.push(make_object({ "userContext": Json::string(id) }))
}
contexts
}
///|
fn BidiProtocol::resolve_browser_client_windows_list(
self : BidiProtocol,
) -> Array[Json] {
let top_level_contexts : Array[String] = []
for ctx_id in self.manager.list_sessions() {
if !self.context_parent.contains(ctx_id) {
top_level_contexts.push(ctx_id)
}
}
let mut active_client_window : String? = None
match self.active_context_id {
Some(active_ctx) =>
if !self.context_parent.contains(active_ctx) {
active_client_window = self.context_client_window.get(active_ctx)
}
None => ()
}
match active_client_window {
Some(_) => ()
None =>
match self.default_context_id {
Some(default_ctx) =>
if !self.context_parent.contains(default_ctx) {
active_client_window = self.context_client_window.get(default_ctx)
}
None => ()
}
}
let windows : Array[Json] = []
let seen_windows : Map[String, Bool] = {}
for ctx_id in top_level_contexts {
let client_window = self.context_client_window
.get(ctx_id)
.unwrap_or("client-window-" + ctx_id)
if !seen_windows.contains(client_window) {
seen_windows[client_window] = true
let is_active = match active_client_window {
Some(active_window) => active_window == client_window
None => windows.length() == 0
}
windows.push(
make_object({
"clientWindow": Json::string(client_window),
"active": Json::boolean(is_active),
}),
)
}
}
windows
}
///|
fn BidiProtocol::resolve_browser_create_user_context(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> String? {
match self.validate_browser_create_user_context(request_id, params) {
Some(_) => return None
None => ()
}
let id = "user-" + self.user_contexts.length().to_string()
self.user_contexts[id] = true
match
get_param_raw_with_alias(
params, "unhandledPromptBehavior", "unhandled_prompt_behavior",
) {
Some(Object(behavior)) =>
self.user_context_prompt_behavior[id] = make_object(behavior)
_ => ()
}
Some(id)
}
///|
fn BidiProtocol::handle_session_is_subscribed_for_context(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let event_name = match map.get("event") {
Some(String(event_name)) => event_name
_ => {
self.send_error(request_id, "invalid argument", "event must be a string")
return
}
}
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
let subscribed = self.manager.has_session(context_id) &&
self.is_subscribed_for_context(event_name, context_id)
self.send_success(
request_id,
Some(make_object({ "subscribed": Json::boolean(subscribed) })),
)
}
///|
fn BidiProtocol::handle_session_prepare_baseline_context_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match params {
Some(Object(_)) | None => ()
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let preserved_default_prompt_behavior = self.user_context_prompt_behavior.get(
"default",
)
let preserved_ctx_id = self.default_context_id.unwrap_or(
self.ensure_default_context(),
)
self.reset_session_runtime_state()
match preserved_default_prompt_behavior {
Some(behavior) => self.user_context_prompt_behavior["default"] = behavior
None => ()
}
let ctx_id = self.recreate_default_context_with_id(preserved_ctx_id)
let realm_id = self.reset_context_to_blank_for_test(ctx_id)
ignore(evaluate_js("try { delete globalThis.SOME_VARIABLE; } catch (_) {}"))
let context_info = self.build_context_info_for_test(ctx_id)
self.send_success(
request_id,
Some(
make_object({
"context": Json::string(ctx_id),
"realm": Json::string(realm_id),
"contextInfo": context_info,
}),
),
)
}
///|
fn BidiProtocol::recreate_default_context_with_id(
self : BidiProtocol,
ctx_id : String,
) -> String {
let id = self.manager.create_session_with_id(ctx_id)
self.context_user_context[id] = "default"
self.context_children[id] = []
self.context_client_window[id] = "client-window-" + id
self.context_parent.remove(id)
self.context_had_synthetic_child.remove(id)
self.context_has_beforeunload.remove(id)
self.context_blocks_cross_origin_iframe_navigation.remove(id)
self.clear_pending_prompt_state(id)
self.clear_pending_navigation_state(id)
self.clear_network_state_for_context(id)
self.context_local_storage.remove(id)
self.remove_storage_cookie_scope(id)
self.context_last_started_navigation.remove(id)
self.context_requested_navigation_url.remove(id)
self.user_contexts["default"] = true
self.default_context_id = Some(id)
self.active_context_id = Some(id)
self.assign_new_realm(id) |> ignore
self.apply_effective_viewport_to_runtime_context(id, id)
id
}
///|
fn BidiProtocol::handle_session_get_baseline_context_info_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match params {
Some(Object(_)) | None => ()
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let ctx_id = self.ensure_default_context()
let realm_id = self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
let context_info = self.build_context_info_for_test(ctx_id)
self.send_success(
request_id,
Some(
make_object({
"context": Json::string(ctx_id),
"realm": Json::string(realm_id),
"contextInfo": context_info,
}),
),
)
}
///|
fn BidiProtocol::handle_session_get_baseline_context_info_value_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match params {
Some(Object(_)) | None => ()
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let ctx_id = self.ensure_default_context()
let context_info = self.build_context_info_for_test(ctx_id)
self.send_success(request_id, Some(context_info))
}
///|
fn BidiProtocol::build_context_info_for_test(
self : BidiProtocol,
ctx_id : String,
) -> Json {
self
.build_context_entry(ctx_id, true, 0, Some(0))
.unwrap_or(
make_object({
"context": Json::string(ctx_id),
"url": Json::string(self.current_context_url(ctx_id)),
"children": Json::array([]),
"parent": Json::null(),
"clientWindow": Json::string(
self.context_client_window
.get(ctx_id)
.unwrap_or("client-window-" + ctx_id),
),
"userContext": Json::string(
self.context_user_context.get(ctx_id).unwrap_or("default"),
),
"originalOpener": Json::null(),
}),
)
}
///|
fn BidiProtocol::handle_network_prepare_context_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return
} else {
ctx_id
}
Some(Null) | None => self.ensure_default_context()
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
let target_url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
let resolved_url = self.resolve_effective_navigation_target_url(
ctx_id, target_url,
)
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
let _ = self.fail_in_flight_navigation(
ctx_id, "aborted", "Navigation interrupted by another navigation",
)
match self.manager.get_session(ctx_id) {
Some(session) => {
let previous_url = session.get_url()
match session.navigate_to(resolved_url) {
Ok(_) =>
self.complete_navigate_with_state(
request_id,
ctx_id,
target_url,
resolved_url,
previous_url,
"complete",
None,
)
Err(_) =>
self.send_success(
request_id,
Some(
make_object({
"context": Json::string(ctx_id),
"url": Json::string(self.current_context_url(ctx_id)),
"navigated": Json::boolean(false),
}),
),
)
}
}
None => {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return
}
}
}
///|
/// Handle browsingContext.setViewport.
fn BidiProtocol::handle_set_viewport(
self : BidiProtocol,
request : BidiRequest,
) -> Unit {
let params = match request.params {
Some(Object(map)) => map
_ => {
self.send_error(
request.id,
"invalid argument",
"params must be an object",
)
return
}
}
let mut target_context : String? = None
match params.get("context") {
Some(String(ctx_id)) => target_context = Some(ctx_id)
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return
}
None => ()
}
let target_user_contexts : Array[String] = []
match get_map_field_with_alias(params, "userContexts", "user_contexts") {
Some(Array(user_contexts)) =>
if user_contexts.length() == 0 {
self.send_error(
request.id,
"invalid argument",
"userContexts must not be an empty array",
)
return
} else {
for user_context in user_contexts {
match user_context {
String(user_context_id) =>
if user_context_id == "" {
self.send_error(
request.id,
"no such user context",
"Unknown user context: " + user_context_id,
)
return
} else if !self.user_contexts.contains(user_context_id) {
self.send_error(
request.id,
"no such user context",
"Unknown user context: " + user_context_id,
)
return
} else {
target_user_contexts.push(user_context_id)
}
_ => {
self.send_error(
request.id,
"invalid argument",
"userContexts entries must be strings",
)
return
}
}
}
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"userContexts must be an array",
)
return
}
None => ()
}
let has_context = target_context is Some(_)
let has_user_contexts = target_user_contexts.length() > 0
if has_context && has_user_contexts {
self.send_error(
request.id,
"invalid argument",
"context and userContexts are mutually exclusive",
)
return
}
if !has_context && !has_user_contexts {
self.send_error(
request.id,
"invalid argument",
"Either context or userContexts must be provided",
)
return
}
let mut viewport_mode = 0 // 0: keep, 1: reset(null), 2: set(object)
let mut viewport_width = 0
let mut viewport_height = 0
match params.get("viewport") {
Some(Null) => viewport_mode = 1
Some(Object(viewport_map)) =>
match (viewport_map.get("width"), viewport_map.get("height")) {
(Some(width_json), Some(height_json)) =>
match
(
json_as_safe_int(width_json, 0.0, 9007199254740991.0),
json_as_safe_int(height_json, 0.0, 9007199254740991.0),
) {
(Some(width), Some(height)) => {
viewport_mode = 2
viewport_width = width
viewport_height = height
}
_ => {
self.send_error(
request.id,
"invalid argument",
"viewport.width and viewport.height must be non-negative integers",
)
return
}
}
_ => {
self.send_error(
request.id,
"invalid argument",
"viewport must include width and height",
)
return
}
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"viewport must be an object or null",
)
return
}
None => ()
}
let mut dpr_mode = 0 // 0: keep, 1: reset(null), 2: set(number)
let mut dpr_value = 1.0
match
get_map_field_with_alias(params, "devicePixelRatio", "device_pixel_ratio") {
Some(Null) => dpr_mode = 1
Some(value) =>
match json_as_safe_number(value, 0.000000001, 9007199254740991.0) {
Some(parsed_dpr) => {
dpr_mode = 2
dpr_value = parsed_dpr
}
None => {
self.send_error(
request.id,
"invalid argument",
"devicePixelRatio must be a positive number",
)
return
}
}
None => ()
}
match target_context {
Some(ctx_id) => {
if !self.manager.has_session(ctx_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + ctx_id,
)
return
}
if self.context_parent.contains(ctx_id) {
self.send_error(
request.id,
"invalid argument",
"context must be top-level",
)
return
}
if viewport_mode == 1 {
self.context_viewport_width.remove(ctx_id)
self.context_viewport_height.remove(ctx_id)
} else if viewport_mode == 2 {
self.context_viewport_width[ctx_id] = viewport_width
self.context_viewport_height[ctx_id] = viewport_height
}
if dpr_mode == 1 {
self.context_device_pixel_ratio.remove(ctx_id)
} else if dpr_mode == 2 {
self.context_device_pixel_ratio[ctx_id] = dpr_value
}
self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
}
None =>
for user_context_id in target_user_contexts {
if viewport_mode == 1 {
self.user_context_viewport_width.remove(user_context_id)
self.user_context_viewport_height.remove(user_context_id)
} else if viewport_mode == 2 {
self.user_context_viewport_width[user_context_id] = viewport_width
self.user_context_viewport_height[user_context_id] = viewport_height
}
if dpr_mode == 1 {
self.user_context_device_pixel_ratio.remove(user_context_id)
} else if dpr_mode == 2 {
self.user_context_device_pixel_ratio[user_context_id] = dpr_value
}
}
}
if has_user_contexts {
for ctx_id in self.manager.list_sessions() {
let ctx_user_context = self.context_user_context
.get(ctx_id)
.unwrap_or("default")
if array_contains(target_user_contexts, ctx_user_context) {
self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
}
}
}
self.send_success(request.id, Some(make_object({})))
}
///|
/// Handle browsingContext.traverseHistory.
fn BidiProtocol::handle_traverse_history(
self : BidiProtocol,
request : BidiRequest,
) -> Unit {
let params = match request.params {
Some(Object(map)) => map
_ => {
self.send_error(
request.id,
"invalid argument",
"params must be an object",
)
return
}
}
let ctx_id = match params.get("context") {
Some(String(id)) => id
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return
}
None => {
self.send_error(request.id, "invalid argument", "Missing context")
return
}
}
if !self.manager.has_session(ctx_id) {
self.send_error(request.id, "no such frame", "Unknown context: " + ctx_id)
return
}
if self.context_parent.contains(ctx_id) {
self.send_error(request.id, "invalid argument", "context must be top-level")
return
}
let delta = match params.get("delta") {
Some(value) =>
match json_as_safe_int(value, -9007199254740991.0, 9007199254740991.0) {
Some(parsed) => parsed
None => {
self.send_error(
request.id,
"invalid argument",
"delta must be an integer in [-9007199254740991, 9007199254740991]",
)
return
}
}
None => {
self.send_error(request.id, "invalid argument", "Missing delta")
return
}
}
if delta == 0 {
self.send_success(request.id, Some(make_object({})))
return
}
match self.manager.get_session(ctx_id) {
Some(session) => {
let page = session.get_page()
let history = match page.get_navigation_history() {
Ok(entries) => entries
Err(_) => {
self.send_error(
request.id,
"unknown error",
"Failed to inspect navigation history",
)
return
}
}
let target_index = history.current_index + delta
if target_index < 0 || target_index >= history.entries.length() {
self.send_error(
request.id,
"no such history entry",
"History traversal target is out of bounds",
)
return
}
let steps = if delta < 0 { 0 - delta } else { delta }
if delta < 0 {
let mut step = 0
while step < steps {
match page.go_back() {
Ok(true) => ()
_ => {
self.send_error(
request.id,
"no such history entry",
"History traversal target is out of bounds",
)
return
}
}
step += 1
}
} else {
let mut step = 0
while step < steps {
match page.go_forward() {
Ok(true) => ()
_ => {
self.send_error(
request.id,
"no such history entry",
"History traversal target is out of bounds",
)
return
}
}
step += 1
}
}
let destination = session.get_url()
self.load_data_url_content(ctx_id, destination)
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_events(ctx_id, destination, nav_id)
self.send_success(request.id, Some(make_object({})))
}
None => {
self.send_error(request.id, "no such frame", "Unknown context: " + ctx_id)
return
}
}
}
///|
/// Handle browsingContext.captureScreenshot with WPT-focused validation.
fn BidiProtocol::handle_capture_screenshot(
self : BidiProtocol,
request : BidiRequest,
) -> Unit {
match self.resolve_capture_screenshot_data(request.id, request.params) {
Some(data) => {
let result = make_object({ "data": Json::string(data) })
self.send_success(request.id, Some(result))
}
None => ()
}
}
///|
fn BidiProtocol::handle_capture_screenshot_data(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_capture_screenshot_data(request_id, params) {
Some(data) => self.send_success(request_id, Some(Json::string(data)))
None => ()
}
}
///|
fn BidiProtocol::resolve_capture_screenshot_data(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> String? {
let raw_params = params
let params = match raw_params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match params.get("context") {
Some(String(id)) => id
Some(_) => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
None => {
self.send_error(request_id, "invalid argument", "Missing context")
return None
}
}
if !self.manager.has_session(ctx_id) {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return None
}
match params.get("origin") {
Some(String("viewport")) | Some(String("document")) | None => ()
Some(String(_)) => {
self.send_error(
request_id, "invalid argument", "origin must be 'viewport' or 'document'",
)
return None
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "origin must be 'viewport' or 'document'",
)
return None
}
}
let mut has_box_clip = false
let mut clip_box_width = 1.0
let mut clip_box_height = 1.0
match params.get("clip") {
None => ()
Some(Object(clip_map)) => {
let clip_type = match clip_map.get("type") {
Some(String(kind)) => kind
_ => {
self.send_error(
request_id, "invalid argument", "clip.type must be a string",
)
return None
}
}
if clip_type == "element" {
match clip_map.get("element") {
Some(Object(element)) =>
match element.get("sharedId") {
Some(String(shared_id)) =>
if !has_runtime_shared_node(shared_id) ||
!runtime_shared_node_is_element(shared_id) {
self.send_error(
request_id,
"no such node",
"Unknown sharedId: " + shared_id,
)
return None
}
_ => {
self.send_error(
request_id, "invalid argument", "clip.element.sharedId must be a string",
)
return None
}
}
_ => {
self.send_error(
request_id, "invalid argument", "clip.element must be a node reference",
)
return None
}
}
} else if clip_type == "box" {
has_box_clip = true
let _clip_box_x = match clip_map.get("x") {
Some(value) =>
match
json_as_safe_number(
value, -9007199254740991.0, 9007199254740991.0,
) {
Some(parsed) => parsed
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
let _clip_box_y = match clip_map.get("y") {
Some(value) =>
match
json_as_safe_number(
value, -9007199254740991.0, 9007199254740991.0,
) {
Some(parsed) => parsed
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
clip_box_width = match clip_map.get("width") {
Some(value) =>
match json_as_safe_number(value, 0.0, 9007199254740991.0) {
Some(parsed) => parsed
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
clip_box_height = match clip_map.get("height") {
Some(value) =>
match json_as_safe_number(value, 0.0, 9007199254740991.0) {
Some(parsed) => parsed
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
None => {
self.send_error(
request_id, "invalid argument", "clip.box x/y/width/height must be numbers",
)
return None
}
}
} else {
self.send_error(request_id, "invalid argument", "clip.type is invalid")
return None
}
}
Some(_) => {
self.send_error(request_id, "invalid argument", "clip must be an object")
return None
}
}
if has_box_clip && (clip_box_width <= 0.0 || clip_box_height <= 0.0) {
self.send_error(
request_id, "unable to capture screen", "Clip dimensions must be positive",
)
return None
}
match params.get("format") {
None => ()
Some(Object(format)) => {
match format.get("type") {
Some(String("image/png")) | Some(String("image/jpeg")) => ()
Some(String(_)) => {
self.send_error(
request_id, "invalid argument", "format.type must be 'image/png' or 'image/jpeg'",
)
return None
}
_ => {
self.send_error(
request_id, "invalid argument", "format.type must be 'image/png' or 'image/jpeg'",
)
return None
}
}
match format.get("quality") {
Some(value) =>
match json_as_safe_number(value, 0.0, 1.0) {
Some(_) => ()
None => {
self.send_error(
request_id, "invalid argument", "format.quality must be a number in [0,1]",
)
return None
}
}
None => ()
}
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "format must be an object",
)
return None
}
}
match self.build_synthetic_capture_screenshot_result(ctx_id, raw_params) {
Some(Object(result_map)) =>
match result_map.get("error") {
Some(String(error_name)) => {
let message = match result_map.get("message") {
Some(String(message)) => message
_ => "Unable to capture screenshot"
}
self.send_error(request_id, error_name, message)
None
}
_ =>
match result_map.get("data") {
Some(String(data)) => Some(data)
_ => {
self.send_error(
request_id, "unknown error", "Failed to build synthetic screenshot",
)
None
}
}
}
_ => {
self.send_error(
request_id, "unknown error", "Failed to build synthetic screenshot",
)
None
}
}
}
///|
/// Handle browsingContext.print with WPT-focused validation.
fn BidiProtocol::handle_print(
self : BidiProtocol,
request : BidiRequest,
) -> Unit {
match self.resolve_print_data(request.id, request.params) {
Some(data) => {
let result = make_object({ "data": Json::string(data) })
self.send_success(request.id, Some(result))
}
None => ()
}
}
///|
fn BidiProtocol::handle_print_data(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_print_data(request_id, params) {
Some(data) => self.send_success(request_id, Some(Json::string(data)))
None => ()
}
}
///|
fn BidiProtocol::resolve_print_data(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> String? {
let params = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match params.get("context") {
Some(String(id)) => id
Some(_) => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
None => {
self.send_error(request_id, "invalid argument", "Missing context")
return None
}
}
if !self.manager.has_session(ctx_id) {
self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
return None
}
match params.get("background") {
Some(True) | Some(False) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "background must be a boolean",
)
return None
}
}
match params.get("margin") {
None => ()
Some(Object(margin)) =>
for margin_side in ["top", "bottom", "left", "right"] {
match margin.get(margin_side) {
Some(value) =>
match json_as_safe_number(value, 0.0, 9007199254740991.0) {
Some(_) => ()
None => {
self.send_error(
request_id, "invalid argument", "margin values must be non-negative numbers",
)
return None
}
}
None => ()
}
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "margin must be an object",
)
return None
}
}
match params.get("orientation") {
Some(String("portrait")) | Some(String("landscape")) | None => ()
Some(String(_)) => {
self.send_error(
request_id, "invalid argument", "orientation must be 'portrait' or 'landscape'",
)
return None
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "orientation must be 'portrait' or 'landscape'",
)
return None
}
}
match params.get("page") {
None => ()
Some(Object(page)) => {
match page.get("width") {
Some(value) =>
if json_as_safe_number(value, 0.035, 9007199254740991.0) is None {
self.send_error(
request_id, "invalid argument", "page.width must be a number >= 0.035",
)
return None
}
None => ()
}
match page.get("height") {
Some(value) =>
if json_as_safe_number(value, 0.035, 9007199254740991.0) is None {
self.send_error(
request_id, "invalid argument", "page.height must be a number >= 0.035",
)
return None
}
None => ()
}
}
Some(_) => {
self.send_error(request_id, "invalid argument", "page must be an object")
return None
}
}
match get_map_field_with_alias(params, "pageRanges", "page_ranges") {
None => ()
Some(Array(page_ranges)) =>
for page_range in page_ranges {
match page_range {
Number(n, ..) =>
if n < 1.0 || n != n.to_int64().to_double() {
self.send_error(
request_id, "invalid argument", "pageRanges entries are invalid",
)
return None
}
String(v) =>
if !is_valid_print_page_range(v) {
self.send_error(
request_id, "invalid argument", "pageRanges entries are invalid",
)
return None
}
_ => {
self.send_error(
request_id, "invalid argument", "pageRanges must be an array of strings/integers",
)
return None
}
}
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "pageRanges must be an array of strings/integers",
)
return None
}
}
match params.get("scale") {
None => ()
Some(value) =>
if json_as_safe_number(value, 0.1, 2.0) is None {
self.send_error(
request_id, "invalid argument", "scale must be a number in [0.1, 2.0]",
)
return None
}
}
match get_map_field_with_alias(params, "shrinkToFit", "shrink_to_fit") {
Some(True) | Some(False) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "shrinkToFit must be a boolean",
)
return None
}
}
let orientation = match params.get("orientation") {
Some(String("landscape")) => "landscape"
_ => "portrait"
}
let mut page_width_cm = 21.59
let mut page_height_cm = 27.94
match params.get("page") {
Some(Object(page)) => {
match page.get("width") {
Some(Number(width, ..)) => page_width_cm = width
_ => ()
}
match page.get("height") {
Some(Number(height, ..)) => page_height_cm = height
_ => ()
}
}
_ => ()
}
if orientation == "landscape" {
let current_width = page_width_cm
page_width_cm = page_height_cm
page_height_cm = current_width
}
let mut margin_top = 0.0
let mut margin_bottom = 0.0
let mut margin_left = 0.0
let mut margin_right = 0.0
let mut has_margin_top = false
let mut has_margin_bottom = false
let mut has_margin_left = false
let mut has_margin_right = false
match params.get("margin") {
Some(Object(margin)) => {
match margin.get("top") {
Some(Number(value, ..)) => {
margin_top = value
has_margin_top = true
}
_ => ()
}
match margin.get("bottom") {
Some(Number(value, ..)) => {
margin_bottom = value
has_margin_bottom = true
}
_ => ()
}
match margin.get("left") {
Some(Number(value, ..)) => {
margin_left = value
has_margin_left = true
}
_ => ()
}
match margin.get("right") {
Some(Number(value, ..)) => {
margin_right = value
has_margin_right = true
}
_ => ()
}
}
_ => ()
}
if margin_top >= page_height_cm ||
margin_bottom >= page_height_cm ||
margin_left >= page_width_cm ||
margin_right >= page_width_cm {
self.send_error(
request_id, "unsupported operation", "Margin consumes entire printable area",
)
return None
}
let background = match params.get("background") {
Some(True) => true
_ => false
}
let default_print_margin_cm = 1.0
let minimum_printable_cm = 2.54 / 72.0
let effective_margin_right = if has_margin_right {
margin_right
} else {
default_print_margin_cm
}
let effective_margin_left = if has_margin_left {
margin_left
} else {
default_print_margin_cm
}
let effective_margin_bottom = if has_margin_bottom {
margin_bottom
} else {
default_print_margin_cm
}
let effective_margin_top = if has_margin_top {
margin_top
} else {
default_print_margin_cm
}
let printable_width_cm = if has_margin_left &&
margin_left + effective_margin_right > page_width_cm - minimum_printable_cm {
page_width_cm - minimum_printable_cm
} else if has_margin_right &&
margin_right + effective_margin_left > page_width_cm - minimum_printable_cm {
page_width_cm - minimum_printable_cm
} else {
page_width_cm
}
let printable_height_cm = if has_margin_top &&
margin_top + effective_margin_bottom > page_height_cm - minimum_printable_cm {
page_height_cm - minimum_printable_cm
} else if has_margin_bottom &&
margin_bottom + effective_margin_top > page_height_cm - minimum_printable_cm {
page_height_cm - minimum_printable_cm
} else {
page_height_cm
}
let synthetic_details = self.resolve_synthetic_print_details(
ctx_id, params, printable_width_cm, printable_height_cm,
)
let signature = match synthetic_details.get("signature") {
Some(String(value)) if value != "" => value
_ => self.current_context_url(ctx_id)
}
let pages = match synthetic_details.get("pages") {
Some(Array(values)) => values
_ => []
}
let meta = make_object({
"kind": Json::string("crater.synthetic.print"),
"orientation": Json::string(orientation),
"pageWidthCm": Json::number(printable_width_cm),
"pageHeightCm": Json::number(printable_height_cm),
"background": Json::boolean(background),
"signature": Json::string(signature),
"pages": Json::array(pages),
})
Some(synthetic_print_pdf_from_meta(meta))
}
///|
/// Validate a single print page-range token.
fn is_valid_print_page_range(raw : String) -> Bool {
let value = raw.trim().to_string()
if value.length() == 0 {
return false
}
match find_substring(value, "-", 0) {
Some(first_dash) => {
match find_substring(value, "-", first_dash + 1) {
Some(_) => return false
None => ()
}
if first_dash == 0 {
let end_text = value.unsafe_substring(start=1, end=value.length())
match parse_decimal_string(end_text) {
Some(v) => v >= 1
None => false
}
} else if first_dash == value.length() - 1 {
let start_text = value.unsafe_substring(start=0, end=first_dash)
match parse_decimal_string(start_text) {
Some(v) => v >= 1
None => false
}
} else {
let start_text = value.unsafe_substring(start=0, end=first_dash)
let end_text = value.unsafe_substring(
start=first_dash + 1,
end=value.length(),
)
let start = match parse_decimal_string(start_text) {
Some(v) => v
None => return false
}
let end = match parse_decimal_string(end_text) {
Some(v) => v
None => return false
}
start >= 1 && end >= 1 && start <= end
}
}
None =>
match parse_decimal_string(value) {
Some(v) => v >= 1
None => false
}
}
}
///|
fn is_valid_locate_nodes_locator_type(locator_type : String) -> Bool {
locator_type == "css" ||
locator_type == "xpath" ||
locator_type == "innerText" ||
locator_type == "accessibility" ||
locator_type == "context"
}
///|
/// Convert a small XPath subset used by WPT locateNodes tests to CSS.
fn xpath_to_css_selector(xpath : String) -> String? {
let trimmed = xpath.trim().to_string()
if trimmed == ".//*[name()='circle']" {
return Some("circle")
}
let body = if trimmed.has_prefix(".//") {
trimmed.unsafe_substring(start=3, end=trimmed.length())
} else if trimmed.has_prefix("//") {
trimmed.unsafe_substring(start=2, end=trimmed.length())
} else {
return None
}
if body == "" {
return None
}
match find_substring(body, "[@", 0) {
None => if body.contains("[") { None } else { Some(body) }
Some(predicate_idx) => {
let selector_tag = body.unsafe_substring(start=0, end=predicate_idx)
if selector_tag == "" {
return None
}
let predicate_start = predicate_idx + 2
let predicate_full = body.unsafe_substring(
start=predicate_start,
end=body.length(),
)
if predicate_full == "" || !predicate_full.has_suffix("]") {
return None
}
let predicate = predicate_full.unsafe_substring(
start=0,
end=predicate_full.length() - 1,
)
match find_substring(predicate, "=", 0) {
None => None
Some(eq_idx) => {
let attr_name = predicate.unsafe_substring(start=0, end=eq_idx)
let quoted_value = predicate.unsafe_substring(
start=eq_idx + 1,
end=predicate.length(),
)
if attr_name == "" || quoted_value.length() < 2 {
return None
}
let quote_chars = quoted_value.to_array()
let quote = quote_chars[0]
let end_quote = quote_chars[quote_chars.length() - 1]
if (quote != '\'' && quote != '"') || end_quote != quote {
return None
}
let attr_value = quoted_value.unsafe_substring(
start=1,
end=quoted_value.length() - 1,
)
Some(
selector_tag +
"[" +
attr_name.trim().to_string() +
"=\"" +
attr_value +
"\"]",
)
}
}
}
}
}
///|
/// Handle browsingContext.locateNodes with WPT-oriented parameter validation.
fn BidiProtocol::handle_locate_nodes(
self : BidiProtocol,
request : BidiRequest,
) -> Unit {
let params = match request.params {
Some(Object(map)) => map
_ => {
self.send_error(
request.id,
"invalid argument",
"params must be an object",
)
return
}
}
let ctx_id = match params.get("context") {
Some(String(id)) => id
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return
}
None => {
self.send_error(request.id, "invalid argument", "Missing context")
return
}
}
if !self.manager.has_session(ctx_id) {
self.send_error(request.id, "no such frame", "Unknown context: " + ctx_id)
return
}
let locator = match params.get("locator") {
Some(Object(map)) => map
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator must be an object",
)
return
}
None => {
self.send_error(request.id, "invalid argument", "Missing locator")
return
}
}
let locator_type = match locator.get("type") {
Some(String(kind)) => kind
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.type must be a string",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.type is required",
)
return
}
}
if !is_valid_locate_nodes_locator_type(locator_type) {
self.send_error(request.id, "invalid argument", "locator.type is invalid")
return
}
match self.validate_serialization_options(request.id, request.params) {
Some(_) => return
None => ()
}
let max_node_count = match
get_map_field_with_alias(params, "maxNodeCount", "max_node_count") {
Some(value) =>
match json_as_safe_int(value, 1.0, 9007199254740991.0) {
Some(parsed) => parsed
None => {
self.send_error(
request.id,
"invalid argument",
"maxNodeCount must be an integer in [1, 9007199254740991]",
)
return
}
}
None => -1
}
let start_node_shared_ids : Array[String] = []
match get_map_field_with_alias(params, "startNodes", "start_nodes") {
Some(Array(nodes)) => {
if nodes.length() == 0 {
self.send_error(
request.id,
"invalid argument",
"startNodes must not be an empty array",
)
return
}
for raw_node in nodes {
match raw_node {
Object(node_map) =>
match node_map.get("sharedId") {
Some(String(shared_id)) => {
if !has_runtime_shared_node(shared_id) {
self.send_error(
request.id,
"no such node",
"Unknown sharedId: " + shared_id,
)
return
}
if !runtime_shared_node_is_element(shared_id) &&
!runtime_shared_node_is_document(shared_id) &&
!runtime_shared_node_is_shadow_root(shared_id) {
self.send_error(
request.id,
"invalid argument",
"startNodes entries must reference element, document, or shadow root nodes",
)
return
}
start_node_shared_ids.push(shared_id)
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"startNodes sharedId must be a string",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"startNodes entries must be node references",
)
return
}
}
_ => {
self.send_error(
request.id,
"invalid argument",
"startNodes must be an array of node references",
)
return
}
}
}
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"startNodes must be an array",
)
return
}
None => ()
}
match locator_type {
"css" =>
match locator.get("value") {
Some(String(selector)) =>
if selector == "" || selector == "a*b" {
self.send_error(
request.id,
"invalid selector",
"Invalid CSS selector",
)
return
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.value must be a string",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.value is required",
)
return
}
}
"xpath" =>
match locator.get("value") {
Some(String(selector)) =>
if selector == "" {
self.send_error(
request.id,
"invalid selector",
"Invalid XPath selector",
)
return
} else if selector.contains(":") {
self.send_error(
request.id,
"unknown error",
"XPath evaluation failed",
)
return
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.value must be a string",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.value is required",
)
return
}
}
"innerText" => {
match locator.get("value") {
Some(String(text_value)) =>
if text_value == "" {
self.send_error(
request.id,
"invalid selector",
"Invalid innerText selector",
)
return
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.value must be a string",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.value is required",
)
return
}
}
match locator.get("ignoreCase") {
Some(True) | Some(False) | None => ()
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.ignoreCase must be a boolean",
)
return
}
}
match locator.get("matchType") {
Some(String("full")) | Some(String("partial")) | None => ()
Some(String(_)) => {
self.send_error(
request.id,
"invalid argument",
"locator.matchType must be 'full' or 'partial'",
)
return
}
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.matchType must be a string",
)
return
}
}
match locator.get("maxDepth") {
Some(value) =>
if json_as_safe_int(value, 0.0, 9007199254740991.0) is None {
self.send_error(
request.id,
"invalid argument",
"locator.maxDepth must be a non-negative integer",
)
return
}
None => ()
}
}
"accessibility" => {
let accessibility_value = match locator.get("value") {
Some(Object(map)) => map
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.value must be an object",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.value is required",
)
return
}
}
let mut has_role = false
let mut has_name = false
match accessibility_value.get("role") {
Some(String(_)) => has_role = true
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"accessibility role must be a string",
)
return
}
None => ()
}
match accessibility_value.get("name") {
Some(String(_)) => has_name = true
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"accessibility name must be a string",
)
return
}
None => ()
}
if !has_role && !has_name {
self.send_error(
request.id,
"invalid selector",
"Invalid accessibility selector",
)
return
}
}
"context" => {
let context_value = match locator.get("value") {
Some(Object(map)) => map
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.value must be an object",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.value is required",
)
return
}
}
let target_ctx = match context_value.get("context") {
Some(String(id)) => id
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"locator.value.context must be a string",
)
return
}
None => {
self.send_error(
request.id,
"invalid argument",
"locator.value.context is required",
)
return
}
}
if !self.manager.has_session(target_ctx) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + target_ctx,
)
return
}
let child_contexts = self.context_children.get(ctx_id).unwrap_or([])
if !array_contains(child_contexts, target_ctx) {
self.send_error(
request.id,
"invalid argument",
"locator.value.context must reference a direct child context",
)
return
}
}
_ => ()
}
let serialization_options_json = get_locate_nodes_serialization_options_json(
request.params,
)
let nodes = match locator_type {
"css" =>
match locator.get("value") {
Some(String(selector)) =>
self.locate_nodes_css(
ctx_id, selector, start_node_shared_ids, max_node_count, serialization_options_json,
)
_ => Json::array([])
}
"xpath" =>
match locator.get("value") {
Some(String(selector)) =>
match xpath_to_css_selector(selector) {
Some(css_selector) =>
self.locate_nodes_css(
ctx_id, css_selector, start_node_shared_ids, max_node_count, serialization_options_json,
)
None =>
self.locate_nodes_xpath(
ctx_id, selector, start_node_shared_ids, max_node_count, serialization_options_json,
)
}
_ => Json::array([])
}
"innerText" =>
match locator.get("value") {
Some(String(text_value)) => {
let ignore_case = match locator.get("ignoreCase") {
Some(True) => true
_ => false
}
let match_type = match locator.get("matchType") {
Some(String("full")) => "full"
Some(String("partial")) => "partial"
_ => "full"
}
let max_depth = match locator.get("maxDepth") {
Some(Number(n, ..)) => n.to_int64().to_int()
_ => -1
}
self.locate_nodes_inner_text(
ctx_id, text_value, ignore_case, match_type, max_depth, start_node_shared_ids,
max_node_count, serialization_options_json,
)
}
_ => Json::array([])
}
"accessibility" =>
match locator.get("value") {
Some(Object(value_map)) => {
let role = match value_map.get("role") {
Some(String(s)) => Some(s)
_ => None
}
let name = match value_map.get("name") {
Some(String(s)) => Some(s)
_ => None
}
self.locate_nodes_accessibility(
ctx_id, role, name, start_node_shared_ids, max_node_count, serialization_options_json,
)
}
_ => Json::array([])
}
"context" =>
match locator.get("value") {
Some(Object(value_map)) =>
match value_map.get("context") {
Some(String(target_ctx)) => {
let child_contexts = self.context_children
.get(ctx_id)
.unwrap_or([])
let mut target_index = -1
for i = 0; i < child_contexts.length(); i = i + 1 {
if child_contexts[i] == target_ctx {
target_index = i
break
}
}
if target_index < 0 {
Json::array([])
} else {
self.locate_nodes_context(
ctx_id, target_index, max_node_count, serialization_options_json,
)
}
}
_ => Json::array([])
}
_ => Json::array([])
}
_ => Json::array([])
}
let normalized_nodes = normalize_svg_namespace_in_nodes(nodes)
let result = make_object({ "nodes": normalized_nodes })
self.send_success(request.id, Some(result))
}
///|
/// Check whether localName should use SVG namespace in BiDi node values.
fn is_svg_local_name(local_name : String) -> Bool {
local_name == "svg" ||
local_name == "circle" ||
local_name == "text" ||
local_name == "path" ||
local_name == "rect" ||
local_name == "ellipse" ||
local_name == "line" ||
local_name == "polyline" ||
local_name == "polygon" ||
local_name == "g" ||
local_name == "defs" ||
local_name == "use"
}
///|
/// Normalize namespaceURI for SVG-like nodes in locateNodes response.
fn normalize_svg_namespace_in_node(node : Json) -> Json {
match node {
Object(node_map) => {
let out_node : Map[String, Json] = {}
for key, value in node_map {
out_node[key] = value
}
match node_map.get("value") {
Some(Object(value_map)) => {
let out_value : Map[String, Json] = {}
for key, value in value_map {
out_value[key] = value
}
let local_name = match value_map.get("localName") {
Some(String(name)) => name
_ => ""
}
let namespace_uri = match value_map.get("namespaceURI") {
Some(String(uri)) => uri
_ => ""
}
if namespace_uri == "http://www.w3.org/1999/xhtml" &&
is_svg_local_name(local_name) {
out_value["namespaceURI"] = Json::string(
"http://www.w3.org/2000/svg",
)
}
out_node["value"] = make_object(out_value)
}
_ => ()
}
make_object(out_node)
}
_ => node
}
}
///|
/// Normalize SVG namespace for all nodes in locateNodes response array.
fn normalize_svg_namespace_in_nodes(nodes : Json) -> Json {
match nodes {
Array(values) => {
let out : Array[Json] = []
for value in values {
out.push(normalize_svg_namespace_in_node(value))
}
Json::array(out)
}
_ => nodes
}
}
///|
/// Encode string array as a JavaScript string-array literal.
fn js_string_array_literal(values : Array[String]) -> String {
let parts : Array[String] = []
for value in values {
parts.push("\"" + escape_js_string(value) + "\"")
}
"[" + parts.join(",") + "]"
}
///|
/// Locate nodes with CSS selector and serialize as BiDi node values.
fn BidiProtocol::locate_nodes_css(
self : BidiProtocol,
ctx_id : String,
selector : String,
start_node_shared_ids : Array[String],
max_node_count : Int,
serialization_options_json : String,
) -> Json {
if start_node_shared_ids.length() == 0 {
match self.manager.get_session(ctx_id) {
Some(session) => {
let doc = session.get_tree().get_document()
match session.get_tree().query_selector_all(doc, selector) {
Ok(nodes) => if nodes.length() == 0 { return Json::array([]) }
Err(_) => ()
}
}
None => ()
}
}
let selector_literal = "\"" + escape_js_string(selector) + "\""
let start_nodes_literal = js_string_array_literal(start_node_shared_ids)
let max_count_literal = max_node_count.to_string()
let expression = "(() => {" +
"const __selector = String(" +
selector_literal +
" || '').trim();" +
"const __ids = " +
start_nodes_literal +
";" +
"const __max = " +
max_count_literal +
";" +
"const __store = globalThis.__bidiSharedNodeStore || new Map();" +
"const __roots = [];" +
"const __toArray = (__value) => {" +
"if (!__value) return [];" +
"if (Array.isArray(__value)) return __value;" +
"try { return Array.from(__value); } catch (_e) {}" +
"const __out = [];" +
"const __len = Number(__value.length || 0);" +
"for (let __i = 0; __i < __len; __i++) __out.push(__value[__i]);" +
"return __out;" +
"};" +
"const __collectChildren = (__node) => {" +
"const __out = [];" +
"if (!__node) return __out;" +
"const __direct = Array.isArray(__node._children) ? __node._children : __toArray(__node.children || __node.childNodes);" +
"for (const __child of __direct) { if (__child && __child.nodeType === 1) __out.push(__child); }" +
"const __shadow = __node.shadowRoot;" +
"if (__shadow) {" +
"const __shadowChildren = Array.isArray(__shadow._children) ? __shadow._children : __toArray(__shadow.children || __shadow.childNodes);" +
"for (const __child of __shadowChildren) { if (__child && __child.nodeType === 1) __out.push(__child); }" +
"}" +
"return __out;" +
"};" +
"const __attr = (__node, __name) => {" +
"let __v = null;" +
"try { if (__node && typeof __node.getAttribute === 'function') __v = __node.getAttribute(__name); } catch (_e) {}" +
"if ((__v === null || __v === undefined) && __node && __node._attrs) __v = __node._attrs[__name];" +
"if (__v === null || __v === undefined) return null;" +
"return String(__v);" +
"};" +
"const __normalizeSelector = (__sel) => String(__sel || '').replace(/\\[\\s*([a-zA-Z0-9_\\-:]+)\\s*=\\s*'([^']*)'\\s*\\]/g, '[$1=\"$2\"]');" +
"const __selectorParts = __normalizeSelector(__selector).split(/\\s+/).filter(Boolean);" +
"const __matchesSimple = (__node, __simpleRaw) => {" +
"if (!__node || __node.nodeType !== 1) return false;" +
"let __simple = String(__simpleRaw || '').trim();" +
"if (__simple === '') return false;" +
"if (__simple === '*') return true;" +
"let __attrName = '';" +
"let __attrValue = null;" +
"let __attrContains = false;" +
"const __attrMatch = __simple.match(/^(.*)\\[([a-zA-Z0-9_\\-:]+)(\\*)?=\"([^\"]*)\"\\]$/);" +
"if (__attrMatch) {" +
"__simple = String(__attrMatch[1] || '').trim();" +
"__attrName = String(__attrMatch[2] || '');" +
"__attrContains = !!__attrMatch[3];" +
"__attrValue = String(__attrMatch[4] || '');" +
"}" +
"let __tag = __simple;" +
"let __id = '';" +
"let __classes = [];" +
"const __hashIdx = __tag.indexOf('#');" +
"if (__hashIdx >= 0) {" +
"__id = __tag.slice(__hashIdx + 1);" +
"__tag = __tag.slice(0, __hashIdx);" +
"}" +
"if (__tag.indexOf('.') >= 0) {" +
"const __parts = __tag.split('.');" +
"__tag = __parts.shift() || '';" +
"__classes = __parts.filter(Boolean);" +
"} else if (__tag === '' && __simple.startsWith('.')) {" +
"__classes = __simple.slice(1).split('.').filter(Boolean);" +
"}" +
"if (__tag !== '' && String(__node.localName || __node.tagName || '').toLowerCase() !== __tag.toLowerCase()) return false;" +
"if (__id !== '') {" +
"const __nodeId = __attr(__node, 'id');" +
"if (__nodeId !== __id) return false;" +
"}" +
"for (const __cls of __classes) {" +
"const __nodeClass = String(__attr(__node, 'class') || '');" +
"const __tokens = __nodeClass.split(/\\s+/).filter(Boolean);" +
"if (__tokens.indexOf(__cls) === -1) return false;" +
"}" +
"if (__attrName !== '') {" +
"const __v = __attr(__node, __attrName);" +
"if (__attrValue === null) {" +
"if (__v === null) return false;" +
"} else if (__attrContains) {" +
"if (__v === null || __v.indexOf(__attrValue) === -1) return false;" +
"} else if (__v !== __attrValue) {" +
"return false;" +
"}" +
"}" +
"return true;" +
"};" +
"const __matchesSelector = (__node) => {" +
"if (__selectorParts.length === 0) return false;" +
"let __current = __node;" +
"let __idx = __selectorParts.length - 1;" +
"if (!__matchesSimple(__current, __selectorParts[__idx])) return false;" +
"__idx -= 1;" +
"while (__idx >= 0) {" +
"let __parent = __current ? (__current._parent || __current.parentElement || __current.parentNode || null) : null;" +
"let __found = false;" +
"while (__parent) {" +
"if (__parent.nodeType === 1 && __matchesSimple(__parent, __selectorParts[__idx])) {" +
"__found = true;" +
"__current = __parent;" +
"break;" +
"}" +
"__parent = __parent._parent || __parent.parentElement || __parent.parentNode || null;" +
"}" +
"if (!__found) return false;" +
"__idx -= 1;" +
"}" +
"return true;" +
"};" +
"if (__ids.length === 0) {" +
"if (globalThis.document) __roots.push(globalThis.document);" +
"} else {" +
"for (const __id of __ids) { const __node = __store.get(__id); if (__node) __roots.push(__node); }" +
"}" +
"const __seen = new Set();" +
"const __result = [];" +
"const __push = (__node) => {" +
"if (!__node || __node.nodeType !== 1) return;" +
"if (__seen.has(__node)) return;" +
"__seen.add(__node);" +
"__result.push(__node);" +
"};" +
"const __walk = (__node) => {" +
"if (!__node) return false;" +
"const __children = __collectChildren(__node);" +
"for (const __child of __children) {" +
"if (__matchesSelector(__child)) {" +
"__push(__child);" +
"if (__max > 0 && __result.length >= __max) return true;" +
"}" +
"if (__walk(__child)) return true;" +
"}" +
"return false;" +
"};" +
"for (const __root of __roots) {" +
"if (!__root) continue;" +
"let __start = null;" +
"if (__root.nodeType === 9) {" +
"__start = __root.documentElement || __root.body || null;" +
"} else if (__root.nodeType === 1 || __root.nodeType === 11) {" +
"__start = __root;" +
"}" +
"if (!__start) continue;" +
"if (__matchesSelector(__start)) {" +
"__push(__start);" +
"if (__max > 0 && __result.length >= __max) return __result.slice(0, __max);" +
"}" +
"if (__walk(__start)) return __result.slice(0, __max);" +
"}" +
"return __max > 0 ? __result.slice(0, __max) : __result;" +
"})()"
self.evaluate_locate_expression(
ctx_id, expression, serialization_options_json,
)
}
///|
/// Evaluate locateNodes expression and normalize result to nodes array.
fn BidiProtocol::evaluate_locate_expression(
self : BidiProtocol,
ctx_id : String,
expression : String,
serialization_options_json : String,
) -> Json {
set_runtime_context(ctx_id)
set_runtime_context_frames(
ctx_id,
self.context_children.get(ctx_id).unwrap_or([]),
)
self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
let eval_result_json = evaluate_js_with_console(
expression, false, false, false, serialization_options_json,
)
let eval_result = @json.parse(eval_result_json) catch {
_ => return Json::array([])
}
match get_string_field(eval_result, "type") {
Some("success") =>
match get_field(eval_result, "result") {
Some(Object(result_map)) =>
match result_map.get("type") {
Some(String("array")) =>
match result_map.get("value") {
Some(Array(values)) => Json::array(values)
_ => Json::array([])
}
_ => Json::array([])
}
_ => Json::array([])
}
_ => Json::array([])
}
}
///|
fn BidiProtocol::handle_script_get_element_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_script_get_element_for_test(request_id, params) {
Some(result) => self.send_success(request_id, Some(result))
None => ()
}
}
///|
fn BidiProtocol::handle_script_prepare_loaded_static_test_page(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match
self.resolve_script_prepare_loaded_static_test_page(request_id, params) {
Some((ctx_id, page, phase, scripts)) => {
if phase == "all" || phase == "inlineScripts" {
self.run_inline_test_scripts(ctx_id, scripts)
}
if phase == "all" || phase == "beforePageSpecific" {
self.run_test_setup_function(
ctx_id,
js_test_page_dom_content_loaded_source(),
)
}
if phase == "all" || phase == "pageSpecific" {
self.run_page_specific_test_setup(ctx_id, page)
}
if phase == "all" || phase == "afterPageSpecific" {
self.run_test_setup_function(ctx_id, js_test_page_reset_state_source())
self.run_test_setup_function(
ctx_id,
js_test_page_all_events_fallback_source(),
)
}
self.send_success(request_id, Some(make_object({})))
}
None => ()
}
}
///|
fn BidiProtocol::handle_script_load_static_test_page_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_script_load_static_test_page_for_test(request_id, params) {
Some((ctx_id, page, html)) =>
match self.manager.get_session(ctx_id) {
Some(session) => {
let url = build_html_data_url(html)
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
match session.navigate_to(url) {
Ok(_) => {
self.remember_requested_navigation_url(ctx_id, url)
let navigation_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.apply_navigation_commit_state(ctx_id, url, navigation_id)
if html.contains("allEvents") {
let scripts = extract_inline_script_blocks(html)
self.run_inline_test_scripts(ctx_id, scripts)
self.run_test_setup_function(
ctx_id,
js_test_page_dom_content_loaded_source(),
)
self.run_page_specific_test_setup(ctx_id, page)
self.run_test_setup_function(
ctx_id,
js_test_page_reset_state_source(),
)
self.run_test_setup_function(
ctx_id,
js_test_page_all_events_fallback_source(),
)
}
self.send_success(request_id, Some(make_object({})))
}
Err(e) =>
self.send_error(
request_id,
"unknown error",
"Navigation failed: " + e.message,
)
}
}
None =>
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
}
None => ()
}
}
///|
fn BidiProtocol::resolve_script_get_element_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Json? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return None
} else {
ctx_id
}
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
let selector = match map.get("selector") {
Some(String(selector)) => selector
_ => {
self.send_error(
request_id, "invalid argument", "selector must be a string",
)
return None
}
}
let allow_frame_fallback = match map.get("allowFrameFallback") {
Some(True) => true
Some(False) => false
_ => false
}
Some(
self.resolve_test_element_remote_value(
ctx_id, selector, allow_frame_fallback,
),
)
}
///|
fn BidiProtocol::resolve_script_prepare_loaded_static_test_page(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> (String, String, String, Array[String])? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return None
} else {
ctx_id
}
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
let page = match map.get("page") {
Some(String(page)) => page
_ => {
self.send_error(request_id, "invalid argument", "page must be a string")
return None
}
}
let phase = match map.get("phase") {
Some(String("inlineScripts")) => "inlineScripts"
Some(String("beforePageSpecific")) => "beforePageSpecific"
Some(String("pageSpecific")) => "pageSpecific"
Some(String("afterPageSpecific")) => "afterPageSpecific"
Some(String("all")) => "all"
Some(String(_)) => {
self.send_error(
request_id, "invalid argument", "phase must be a known string",
)
return None
}
_ => "all"
}
let scripts : Array[String] = []
let mut html : String? = None
match map.get("html") {
Some(String(value)) => html = Some(value)
Some(_) => {
self.send_error(request_id, "invalid argument", "html must be a string")
return None
}
None => ()
}
match map.get("scripts") {
Some(Array(raw_scripts)) =>
for raw_script in raw_scripts {
match raw_script {
String(script) => scripts.push(script)
_ => {
self.send_error(
request_id, "invalid argument", "scripts must be an array of strings",
)
return None
}
}
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "scripts must be an array of strings",
)
return None
}
None => ()
}
if scripts.length() == 0 && (phase == "all" || phase == "inlineScripts") {
match html {
Some(content) =>
for script in extract_inline_script_blocks(content) {
scripts.push(script)
}
None => ()
}
}
Some((ctx_id, page, phase, scripts))
}
///|
fn BidiProtocol::resolve_script_load_static_test_page_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> (String, String, String)? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return None
} else {
ctx_id
}
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
let page = match map.get("page") {
Some(String(page)) => page
_ => {
self.send_error(request_id, "invalid argument", "page must be a string")
return None
}
}
let html = match map.get("html") {
Some(String(html)) => html
_ => {
self.send_error(request_id, "invalid argument", "html must be a string")
return None
}
}
Some((ctx_id, page, html))
}
///|
fn BidiProtocol::resolve_test_script_context_runtime(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> (String, String, String)? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return None
} else {
ctx_id
}
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
let sandbox_name = match map.get("sandbox") {
Some(String(name)) if name != "" => Some(name)
Some(String(_)) | None | Some(Null) => None
_ => {
self.send_error(
request_id, "invalid argument", "sandbox must be a string",
)
return None
}
}
let realm_id = match sandbox_name {
Some(name) => self.get_or_create_sandbox_realm(ctx_id, name)
None => self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
}
let runtime_ctx_id = match sandbox_name {
Some(_) => realm_id
None => ctx_id
}
Some((ctx_id, realm_id, runtime_ctx_id))
}
///|
fn BidiProtocol::handle_script_create_iframe_context_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_script_create_iframe_context_result(request_id, params) {
Some(result) => self.send_success(request_id, Some(result))
None => ()
}
}
///|
fn BidiProtocol::handle_script_create_iframe_context_id_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_script_create_iframe_context_result(request_id, params) {
Some(result) =>
match get_string_field(result, "context") {
Some(ctx_id) =>
self.send_success(request_id, Some(Json::string(ctx_id)))
None =>
self.send_error(
request_id, "unknown error", "Failed to create iframe context",
)
}
None => ()
}
}
///|
fn BidiProtocol::resolve_script_create_iframe_context_result(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Json? {
match self.resolve_script_create_iframe_context_for_test(request_id, params) {
Some((parent_context, url)) => {
let user_context_id = self.context_user_context
.get(parent_context)
.unwrap_or("default")
let remote_value = self.evaluate_test_element_remote_expression(
parent_context,
js_test_page_create_iframe_source(),
[url],
)
let result = match get_string_field(remote_value, "type") {
Some("window") =>
match get_string_field(remote_value, "value") {
Some(ctx_id) if ctx_id != "" =>
make_object({
"context": Json::string(ctx_id),
"parentContext": Json::string(parent_context),
"userContext": Json::string(user_context_id),
"url": Json::string(self.current_context_url(ctx_id)),
})
_ =>
self.create_synthetic_child_context_result(
parent_context, url, user_context_id,
)
}
_ =>
self.create_synthetic_child_context_result(
parent_context, url, user_context_id,
)
}
Some(result)
}
None => None
}
}
///|
fn BidiProtocol::resolve_script_create_iframe_context_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> (String, String)? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return None
} else {
ctx_id
}
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
let url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return None
}
}
Some((ctx_id, url))
}
///|
fn BidiProtocol::resolve_test_element_remote_value(
self : BidiProtocol,
ctx_id : String,
selector : String,
allow_frame_fallback : Bool,
) -> Json {
match self.locate_nodes_css(ctx_id, selector, [], 1, "{}") {
Array(nodes) =>
if nodes.length() > 0 {
nodes[0]
} else if allow_frame_fallback && selector == "textarea" {
self.evaluate_test_element_remote_expression(
ctx_id,
"selector => {" +
"let target = document.querySelector(selector);" +
"if (!target && selector === 'textarea' && document && document.body && typeof document.createElement === 'function') {" +
"target = document.createElement('textarea');" +
"if (target && target.style) {" +
"target.style.width = '100px';" +
"target.style.height = '40px';" +
"}" +
"if (target && typeof target.setAttribute === 'function') {" +
"target.setAttribute('data-crater-frame-fallback', '1');" +
"}" +
"document.body.appendChild(target);" +
"}" +
"if (typeof window.allEvents === 'undefined') {" +
"window.allEvents = { events: [] };" +
"}" +
"return target;" +
"}",
[selector],
)
} else {
make_object({ "type": Json::string("null") })
}
_ => make_object({ "type": Json::string("null") })
}
}
///|
fn BidiProtocol::evaluate_test_element_remote_expression(
self : BidiProtocol,
ctx_id : String,
function_declaration : String,
string_arguments : Array[String],
) -> Json {
let args_literal : Array[String] = []
for argument in string_arguments {
args_literal.push("\"" + escape_js_string(argument) + "\"")
}
let expression = "(" +
function_declaration +
")(" +
args_literal.join(",") +
")"
set_runtime_context(ctx_id)
set_runtime_context_frames(
ctx_id,
self.context_children.get(ctx_id).unwrap_or([]),
)
self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
let eval_result_json = evaluate_js_with_console(
expression, false, false, false, "{}",
)
let eval_result = @json.parse(eval_result_json) catch {
_ => return make_object({ "type": Json::string("null") })
}
match get_string_field(eval_result, "type") {
Some("success") =>
match get_field(eval_result, "result") {
Some(result) => result
None => make_object({ "type": Json::string("null") })
}
_ => make_object({ "type": Json::string("null") })
}
}
///|
fn BidiProtocol::run_test_setup_function(
self : BidiProtocol,
ctx_id : String,
function_declaration : String,
) -> Unit {
ignore(
self.evaluate_test_element_remote_expression(ctx_id, function_declaration, []),
)
}
///|
fn BidiProtocol::run_inline_test_scripts(
self : BidiProtocol,
ctx_id : String,
scripts : Array[String],
) -> Unit {
for script in scripts {
ignore(
self.evaluate_test_element_remote_expression(
ctx_id,
"source => { (0, eval)(source); return null; }",
[script],
),
)
}
}
///|
fn BidiProtocol::run_page_specific_test_setup(
self : BidiProtocol,
ctx_id : String,
page : String,
) -> Unit {
match page {
"test_actions.html" =>
self.run_test_setup_function(
ctx_id,
js_test_actions_page_specific_source(),
)
"test_actions_scroll.html" =>
self.run_test_setup_function(
ctx_id,
js_test_actions_scroll_page_specific_source(),
)
_ => ()
}
}
///|
fn BidiProtocol::handle_script_setup_beforeunload_page_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_test_script_context_runtime(request_id, params) {
Some((ctx_id, _, _)) => {
self.run_test_setup_function(ctx_id, js_beforeunload_page_setup_source())
self.send_success(request_id, Some(make_object({})))
}
None => ()
}
}
///|
fn BidiProtocol::handle_script_prepare_beforeunload_page_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
self.handle_script_prepare_beforeunload_page_for_test_internal(
request_id, params, false,
)
}
///|
fn BidiProtocol::handle_script_prepare_beforeunload_page_url_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
self.handle_script_prepare_beforeunload_page_for_test_internal(
request_id, params, true,
)
}
///|
fn BidiProtocol::handle_script_prepare_beforeunload_page_for_test_internal(
self : BidiProtocol,
request_id : Int,
params : Json?,
return_url_only : Bool,
) -> Unit {
match
self.resolve_script_prepare_beforeunload_page_for_test(request_id, params) {
Some((ctx_id, requested_url)) => {
let resolved_url = self.resolve_effective_navigation_target_url(
ctx_id, requested_url,
)
self.input_synthetic_events_by_context.remove(ctx_id)
self.context_synthetic_scrolled.remove(ctx_id)
self.clear_synthetic_location_href(ctx_id)
let _ = self.fail_in_flight_navigation(
ctx_id, "aborted", "Navigation interrupted by another navigation",
)
if !is_valid_navigation_target_url(resolved_url) {
self.send_error(request_id, "invalid argument", "url is invalid")
return
}
if self.navigation_blocked_by_network_conditions(ctx_id, resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id, "unknown error", "Navigation failed: network offline",
)
return
}
if should_fail_navigation_with_unknown_error(resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id, "unknown error", "Navigation failed: navigation target is unreachable",
)
return
}
if self.is_iframe_navigation_blocked(ctx_id, resolved_url) {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.emit_navigation_failed_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id, "unknown error", "Navigation failed: blocked by embedding policy",
)
return
}
match self.manager.get_session(ctx_id) {
Some(session) => {
let previous_url = session.get_url()
match session.navigate_to(resolved_url) {
Ok(_) => {
self.remember_requested_navigation_url(ctx_id, requested_url)
if is_fragment_navigation(previous_url, resolved_url) {
self.clear_pending_navigation_state(ctx_id)
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_fragment_navigated_event(
ctx_id,
resolved_url,
Some(nav_id),
)
self.emit_history_updated_event(ctx_id, resolved_url)
self.run_test_setup_function(
ctx_id,
js_beforeunload_page_setup_source(),
)
if return_url_only {
self.send_navigation_url_result(
request_id, ctx_id, requested_url, resolved_url, nav_id,
)
} else {
let result = make_object({
"navigation": Json::string(nav_id),
"url": Json::string(resolved_url),
})
self.send_navigation_state_result(
request_id, ctx_id, requested_url, resolved_url, nav_id, result,
)
}
return
}
if return_url_only {
self.pending_navigation_response_mode[ctx_id] = "url"
} else {
self.pending_navigation_response_mode.remove(ctx_id)
}
let completion = self.build_navigation_completion(
request_id,
ctx_id,
resolved_url,
"complete",
None,
)
self.run_test_setup_function(
ctx_id,
js_beforeunload_page_setup_source(),
)
if completion.deferred_response {
return
}
self.pending_navigation_response_mode.remove(ctx_id)
if return_url_only {
self.send_success(request_id, Some(Json::string(resolved_url)))
} else {
self.send_navigation_state_result(
request_id,
ctx_id,
requested_url,
resolved_url,
completion.navigation_id,
completion.result,
)
}
}
Err(e) => {
let nav_id = self.next_navigation_id.to_string()
self.next_navigation_id += 1
self.emit_navigation_started_event(ctx_id, resolved_url, nav_id)
self.send_error(
request_id,
"unknown error",
"Navigation failed: " + e.message,
)
}
}
}
None => {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return
}
}
}
None => ()
}
}
///|
fn BidiProtocol::resolve_script_prepare_beforeunload_page_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> (String, String)? {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return None
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return None
} else {
ctx_id
}
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return None
}
}
let url = match map.get("url") {
Some(String(url)) => url
Some(Null) | None => default_beforeunload_test_page_url()
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return None
}
}
Some((ctx_id, url))
}
///|
fn BidiProtocol::handle_script_sync_document_cookies_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
match self.resolve_test_script_context_runtime(request_id, params) {
Some((ctx_id, _, runtime_ctx_id)) => {
let (cookie_text, cookie_count) = self.sync_document_cookies_for_test_context(
ctx_id, runtime_ctx_id,
)
self.send_success(
request_id,
Some(
make_object({
"cookieText": Json::string(cookie_text),
"cookieCount": Json::number(cookie_count.to_double()),
}),
),
)
}
None => ()
}
}
///|
fn BidiProtocol::sync_document_cookies_for_test_context(
self : BidiProtocol,
ctx_id : String,
runtime_ctx_id : String,
) -> (String, Int) {
set_runtime_context(runtime_ctx_id)
set_runtime_context_frames(
runtime_ctx_id,
self.context_children.get(ctx_id).unwrap_or([]),
)
self.apply_effective_viewport_to_runtime_context(runtime_ctx_id, ctx_id)
if !self.dom_initialized {
let _ = evaluate_js_with_console("undefined", false, false, false, "{}")
self.dom_initialized = true
}
let eval_result_json = evaluate_js_with_console(
"document.cookie", false, false, false, "{}",
)
self.dom_initialized = true
let eval_result = @json.parse(eval_result_json) catch { _ => make_object({}) }
let cookie_text = match get_string_field(eval_result, "type") {
Some("success") =>
match get_field(eval_result, "result") {
Some(remote_value) =>
remote_value_as_string(remote_value).unwrap_or(
self.resolve_document_cookie(ctx_id).unwrap_or(""),
)
None => self.resolve_document_cookie(ctx_id).unwrap_or("")
}
_ => self.resolve_document_cookie(ctx_id).unwrap_or("")
}
let mut cookie_count = 0
if cookie_text.trim() != "" {
for raw_entry in cookie_text.split(";") {
if raw_entry.contains("=") {
self.remember_document_cookie(ctx_id, raw_entry.trim().to_string())
cookie_count += 1
}
}
}
(cookie_text, cookie_count)
}
///|
fn BidiProtocol::handle_script_fetch_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let ctx_id = match map.get("context") {
Some(String(ctx_id)) =>
if ctx_id == "" || self.manager.get_session(ctx_id) is None {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + ctx_id,
)
return
} else {
ctx_id
}
Some(Null) | None => self.ensure_default_context()
_ => {
self.send_error(
request_id, "invalid argument", "context must be a string",
)
return
}
}
let sandbox_name = match map.get("sandbox") {
Some(String(name)) if name != "" => Some(name)
Some(String(_)) | Some(Null) | None => None
_ => {
self.send_error(
request_id, "invalid argument", "sandbox must be a string",
)
return
}
}
let realm_id = match sandbox_name {
Some(name) => self.get_or_create_sandbox_realm(ctx_id, name)
None => self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
}
let runtime_ctx_id = match sandbox_name {
Some(_) => realm_id
None => ctx_id
}
let target_url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
let raw_headers = map.get("headers")
let headers_json = match map.get("headersJson") {
Some(String(headers_json)) => headers_json
Some(Null) | None =>
match raw_headers {
Some(raw_headers) => raw_headers.stringify()
None => "null"
}
_ => {
self.send_error(
request_id, "invalid argument", "headersJson must be a string",
)
return
}
}
let raw_post_data = get_map_field_with_alias(map, "postData", "post_data")
let post_data_json = match map.get("postDataJson") {
Some(String(post_data_json)) => post_data_json
Some(Null) | None =>
match raw_post_data {
Some(raw_post_data) => raw_post_data.stringify()
None => "null"
}
_ => {
self.send_error(
request_id, "invalid argument", "postDataJson must be a string",
)
return
}
}
let post_data_mode = match map.get("postDataMode") {
Some(String("none")) => "none"
Some(String("formData")) => "formData"
Some(String("value")) => "value"
Some(String(_)) => {
self.send_error(
request_id, "invalid argument", "postDataMode must be a known string",
)
return
}
Some(Null) | None =>
if get_map_field_with_alias(map, "requestData", "request_data") != None {
"value"
} else if raw_post_data is Some(raw_post_data) {
synthetic_fetch_post_data_mode_from_value(raw_post_data)
} else if post_data_json != "null" {
"value"
} else {
"none"
}
_ => {
self.send_error(
request_id, "invalid argument", "postDataMode must be a string",
)
return
}
}
let request_headers = match
get_map_field_with_alias(map, "requestHeaders", "request_headers") {
Some(raw_headers) =>
match
self.normalize_synthetic_network_header_entries(
request_id,
Some(raw_headers),
) {
Some(request_headers) => request_headers
None => return
}
None =>
match raw_headers {
Some(raw_headers) =>
synthetic_fetch_request_headers_from_value(raw_headers)
None => synthetic_fetch_request_headers_from_json(headers_json)
}
}
let request_value = match
get_map_field_with_alias(map, "requestData", "request_data") {
Some(raw_value) =>
match
self.normalize_synthetic_network_bytes_value(
request_id, raw_value, "requestData",
) {
Some(value) => Some(value)
None => return
}
None =>
match raw_post_data {
Some(raw_post_data) =>
synthetic_fetch_request_value_from_post_data(
raw_post_data, post_data_mode,
)
None =>
synthetic_fetch_request_value_from_post_data_json(
post_data_json, post_data_mode,
)
}
}
let request_method = match map.get("method") {
Some(String(request_method)) => request_method
Some(Null) | None =>
if request_value != None || post_data_mode != "none" {
"POST"
} else {
"GET"
}
_ => {
self.send_error(request_id, "invalid argument", "method must be a string")
return
}
}
let timeout_ms = match map.get("timeoutMs") {
Some(Number(n, ..)) if n >= 0.0 && n == n.to_int64().to_double() =>
n.to_int64().to_int()
Some(Number(_)) => {
self.send_error(
request_id, "invalid argument", "timeoutMs must be a non-negative integer",
)
return
}
Some(Null) | None => 0
_ => {
self.send_error(
request_id, "invalid argument", "timeoutMs must be a non-negative integer",
)
return
}
}
let should_abort = match
get_map_field_with_alias(map, "shouldAbort", "should_abort") {
None | Some(Null) => false
Some(True) => true
Some(False) => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "shouldAbort must be a boolean",
)
return
}
}
let resolved_url = self.resolve_fetch_target_url(ctx_id, target_url)
if self.resolve_storage_request_cookies(ctx_id, Some(resolved_url)).length() ==
0 {
ignore(self.sync_document_cookies_for_test_context(ctx_id, runtime_ctx_id))
}
if canonicalize_network_http_url(resolved_url) is Some(_) ||
resolved_url.has_prefix("data:") {
let result = self.perform_synthetic_fetch_internal(
request_id, ctx_id, resolved_url, request_method, request_headers, request_value,
should_abort,
)
match result.blocked_text {
Some(text) =>
self.send_success(
request_id,
Some(
make_object({
"exceptionDetails": make_object({ "text": Json::string(text) }),
}),
),
)
None => {
let response_value = self.build_script_fetch_for_test_response_value(
ctx_id,
resolved_url,
request_headers,
result.response_value,
)
self.send_success(request_id, Some(response_value))
}
}
return
}
self.send_script_fetch_from_context_for_test(
request_id, ctx_id, realm_id, runtime_ctx_id, resolved_url, request_method, headers_json,
post_data_json, post_data_mode, timeout_ms,
)
}
///|
fn synthetic_fetch_request_headers_from_json(
headers_json : String,
) -> Array[Json] {
let parsed = @json.parse(headers_json) catch { _ => return [] }
synthetic_fetch_request_headers_from_value(parsed)
}
///|
fn synthetic_fetch_request_headers_from_value(
raw_headers : Json,
) -> Array[Json] {
match raw_headers {
Object(headers_map) => {
let headers : Array[Json] = []
for name, raw_value in headers_map {
headers.push(
synthetic_network_header_string_entry(
name,
synthetic_fetch_json_text_value(raw_value),
),
)
}
headers
}
_ => []
}
}
///|
fn synthetic_fetch_post_data_mode_from_value(raw_post_data : Json) -> String {
match raw_post_data {
Null => "none"
Object(_) => "formData"
_ => "value"
}
}
///|
fn synthetic_fetch_request_value_from_post_data_json(
post_data_json : String,
post_data_mode : String,
) -> Json? {
if post_data_mode == "none" {
return None
}
let parsed = @json.parse(post_data_json) catch { _ => return None }
synthetic_fetch_request_value_from_post_data(parsed, post_data_mode)
}
///|
fn synthetic_fetch_request_value_from_post_data(
post_data : Json,
post_data_mode : String,
) -> Json? {
if post_data_mode == "formData" {
match post_data {
Object(form_data) =>
Some(synthetic_fetch_multipart_request_value(form_data))
_ => None
}
} else {
Some(
make_object({
"type": Json::string("string"),
"value": Json::string(synthetic_fetch_json_text_value(post_data)),
}),
)
}
}
///|
fn BidiProtocol::build_script_fetch_for_test_response_value(
self : BidiProtocol,
ctx_id : String,
url : String,
request_headers : Array[Json],
default_value : Json,
) -> Json {
if !synthetic_fetch_is_headers_echo_url(url) {
return default_value
}
make_object({
"type": Json::string("string"),
"value": Json::string(
self.build_script_fetch_headers_echo_payload(ctx_id, request_headers),
),
})
}
///|
fn BidiProtocol::build_script_fetch_headers_echo_payload(
self : BidiProtocol,
context_id : String,
request_headers : Array[Json],
) -> String {
let headers : Map[String, Json] = {}
for
header in self.resolve_network_extra_header_entries(
context_id, request_headers,
) {
let name = match synthetic_network_header_entry_name(header) {
Some(name) => name
None => continue
}
let value = match synthetic_network_header_entry_string_value(header) {
Some(value) => value
None => continue
}
headers[name] = Json::array([Json::string(value)])
}
make_object({ "headers": make_object(headers) }).stringify()
}
///|
fn synthetic_fetch_multipart_request_value(
form_data : Map[String, Json],
) -> Json {
let boundary = "----crater-boundary"
let mut has_binary = false
let mut body = ""
for field_name, raw_value in form_data {
match synthetic_fetch_form_data_file_entry(raw_value) {
Some((filename, content_type, payload)) => {
has_binary = true
body = body +
"--" +
boundary +
"\r\n" +
"Content-Disposition: form-data; name=\"" +
field_name +
"\"; filename=\"" +
filename +
"\"\r\n" +
"Content-Type: " +
content_type +
"\r\n\r\n" +
payload +
"\r\n"
}
None =>
body = body +
"--" +
boundary +
"\r\n" +
"Content-Disposition: form-data; name=\"" +
field_name +
"\"\r\n\r\n" +
synthetic_fetch_json_text_value(raw_value) +
"\r\n"
}
}
body = body + "--" + boundary + "--\r\n"
if has_binary {
make_object({
"type": Json::string("base64"),
"value": Json::string(encode_base64_raw(body)),
})
} else {
make_object({ "type": Json::string("string"), "value": Json::string(body) })
}
}
///|
fn synthetic_fetch_form_data_file_entry(
raw_value : Json,
) -> (String, String, String)? {
let value_map = match raw_value {
Object(value_map) => value_map
_ => return None
}
let encoded_value = match value_map.get("value") {
Some(String(encoded_value)) => encoded_value
_ => return None
}
let filename = match value_map.get("filename") {
Some(String(filename)) => filename
_ => "blob.bin"
}
let content_type = match value_map.get("type") {
Some(String(content_type)) => content_type
_ => "application/octet-stream"
}
Some((filename, content_type, decode_base64(encoded_value)))
}
///|
fn synthetic_fetch_json_text_value(raw_value : Json) -> String {
match raw_value {
String(value) => value
Number(value, ..) => value.to_string()
True => "true"
False => "false"
Null => "null"
Array(_) | Object(_) => raw_value.stringify()
}
}
///|
fn BidiProtocol::handle_script_fetch_from_context_for_test(
self : BidiProtocol,
request_id : Int,
params : Json?,
) -> Unit {
let map = match params {
Some(Object(map)) => map
_ => {
self.send_error(
request_id, "invalid argument", "params must be an object",
)
return
}
}
let (ctx_id, realm_id, runtime_ctx_id) = match
self.resolve_test_script_context_runtime(request_id, params) {
Some(value) => value
None => return
}
let url = match map.get("url") {
Some(String(url)) => url
_ => {
self.send_error(request_id, "invalid argument", "url must be a string")
return
}
}
let request_method = match map.get("method") {
Some(String(request_method)) => request_method
_ => {
self.send_error(request_id, "invalid argument", "method must be a string")
return
}
}
let headers_json = match map.get("headersJson") {
Some(String(headers_json)) => headers_json
Some(Null) | None => "null"
_ => {
self.send_error(
request_id, "invalid argument", "headersJson must be a string",
)
return
}
}
let post_data_json = match map.get("postDataJson") {
Some(String(post_data_json)) => post_data_json
Some(Null) | None => "null"
_ => {
self.send_error(
request_id, "invalid argument", "postDataJson must be a string",
)
return
}
}
let post_data_mode = match map.get("postDataMode") {
Some(String("none")) => "none"
Some(String("formData")) => "formData"
Some(String("value")) => "value"
Some(String(_)) => {
self.send_error(
request_id, "invalid argument", "postDataMode must be a known string",
)
return
}
None => "none"
_ => {
self.send_error(
request_id, "invalid argument", "postDataMode must be a string",
)
return
}
}
let timeout_ms = match map.get("timeoutMs") {
Some(Number(n, ..)) if n >= 0.0 && n == n.to_int64().to_double() =>
n.to_int64().to_int()
Some(Number(_)) => {
self.send_error(
request_id, "invalid argument", "timeoutMs must be a non-negative integer",
)
return
}
None => 0
_ => {
self.send_error(
request_id, "invalid argument", "timeoutMs must be a non-negative integer",
)
return
}
}
self.send_script_fetch_from_context_for_test(
request_id, ctx_id, realm_id, runtime_ctx_id, url, request_method, headers_json,
post_data_json, post_data_mode, timeout_ms,
)
}
///|
fn BidiProtocol::send_script_fetch_from_context_for_test(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
realm_id : String,
runtime_ctx_id : String,
url : String,
request_method : String,
headers_json : String,
post_data_json : String,
post_data_mode : String,
timeout_ms : Int,
) -> Unit {
set_runtime_context(runtime_ctx_id)
set_runtime_context_frames(
runtime_ctx_id,
self.context_children.get(ctx_id).unwrap_or([]),
)
self.apply_effective_viewport_to_runtime_context(runtime_ctx_id, ctx_id)
if !self.dom_initialized {
let _ = evaluate_js_with_console("undefined", false, false, false, "{}")
self.dom_initialized = true
}
let expression = js_fetch_from_context_for_test_expression(
url, request_method, headers_json, post_data_json, post_data_mode, timeout_ms,
)
match self.socket {
Some(socket) =>
eval_and_send_async_with_console(
socket, request_id, expression, runtime_ctx_id, realm_id, false, false, false,
true,
)
None =>
self.send_error(
request_id, "unknown error", "WebSocket is required for async fetch evaluation",
)
}
}
///|
extern "js" fn js_test_page_dom_content_loaded_source() -> String =
#| () => String.raw`(() => {
#| const event =
#| typeof globalThis.__bidiCreateEvent === "function"
#| ? globalThis.__bidiCreateEvent("DOMContentLoaded", { bubbles: true, cancelable: true })
#| : { type: "DOMContentLoaded" };
#| if (document && typeof document.dispatchEvent === "function") {
#| try { document.dispatchEvent(event); } catch (_e) {}
#| }
#| return null;
#| })`
///|
extern "js" fn js_test_page_reset_state_source() -> String =
#| () => String.raw`(() => {
#| if (window && typeof window === "object") {
#| window.__craterAllEventsFallbackInit = false;
#| window.__bidiFocusedElement = null;
#| }
#| globalThis.__bidiFocusedElement = null;
#| globalThis.__bidiRecordedTestActionsMousemove = false;
#| if (typeof allEvents !== "undefined") {
#| window.allEvents = allEvents;
#| }
#| if (typeof window.allEvents === "undefined") {
#| window.allEvents = { events: [] };
#| }
#| if (window.allEvents && Array.isArray(window.allEvents.events)) {
#| window.allEvents.events.length = 0;
#| }
#| return null;
#| })`
///|
extern "js" fn js_test_page_all_events_fallback_source() -> String =
#| () => String.raw`(() => {
#| if (typeof window.allEvents === "undefined") {
#| window.allEvents = { events: [] };
#| }
#|
#| let keyboardRecorder =
#| typeof globalThis.recordKeyboardEvent === "function"
#| ? globalThis.recordKeyboardEvent
#| : (window && typeof window.recordKeyboardEvent === "function"
#| ? window.recordKeyboardEvent
#| : null);
#| let pointerRecorder =
#| typeof globalThis.recordPointerEvent === "function"
#| ? globalThis.recordPointerEvent
#| : (window && typeof window.recordPointerEvent === "function"
#| ? window.recordPointerEvent
#| : null);
#| let wheelRecorder =
#| typeof globalThis.recordWheelEvent === "function"
#| ? globalThis.recordWheelEvent
#| : (window && typeof window.recordWheelEvent === "function"
#| ? window.recordWheelEvent
#| : null);
#|
#| if (!keyboardRecorder) {
#| keyboardRecorder = (event) => {
#| if (!window.allEvents || !Array.isArray(window.allEvents.events)) {
#| window.allEvents = { events: [] };
#| }
#| window.allEvents.events.push({
#| code: event.code,
#| key: event.key,
#| which: event.which,
#| location: event.location,
#| ctrl: event.ctrlKey,
#| meta: event.metaKey,
#| shift: event.shiftKey,
#| repeat: event.repeat,
#| type: event.type,
#| });
#| };
#| }
#| if (!pointerRecorder) {
#| pointerRecorder = (event) => {
#| if (!window.allEvents || !Array.isArray(window.allEvents.events)) {
#| window.allEvents = { events: [] };
#| }
#| if (event.type === "contextmenu" && typeof event.preventDefault === "function") {
#| event.preventDefault();
#| }
#| window.allEvents.events.push({
#| type: event.type,
#| button: event.button,
#| buttons: event.buttons,
#| pageX: event.pageX,
#| pageY: event.pageY,
#| ctrlKey: event.ctrlKey,
#| metaKey: event.metaKey,
#| altKey: event.altKey,
#| shiftKey: event.shiftKey,
#| clientX: event.clientX,
#| clientY: event.clientY,
#| isTrusted: event.isTrusted,
#| detail: event.detail,
#| target: event.target && event.target.id ? event.target.id : "",
#| pointerType: event.pointerType || "",
#| width: event.width,
#| height: event.height,
#| pressure: event.pressure,
#| tangentialPressure: event.tangentialPressure,
#| tiltX: event.tiltX,
#| tiltY: event.tiltY,
#| twist: event.twist,
#| altitudeAngle: event.altitudeAngle,
#| azimuthAngle: event.azimuthAngle,
#| });
#| };
#| }
#| if (typeof globalThis.resetEvents !== "function") {
#| globalThis.resetEvents = () => {
#| if (!window.allEvents || !Array.isArray(window.allEvents.events)) {
#| window.allEvents = { events: [] };
#| }
#| window.allEvents.events.length = 0;
#| };
#| }
#|
#| if (keyboardRecorder && typeof globalThis.recordKeyboardEvent !== "function") {
#| globalThis.recordKeyboardEvent = keyboardRecorder;
#| }
#| if (pointerRecorder && typeof globalThis.recordPointerEvent !== "function") {
#| globalThis.recordPointerEvent = pointerRecorder;
#| }
#| if (wheelRecorder && typeof globalThis.recordWheelEvent !== "function") {
#| globalThis.recordWheelEvent = wheelRecorder;
#| }
#| try {
#| if (typeof globalThis.recordKeyboardEvent === "function") {
#| (0, eval)("var recordKeyboardEvent = globalThis.recordKeyboardEvent");
#| }
#| if (typeof globalThis.recordPointerEvent === "function") {
#| (0, eval)("var recordPointerEvent = globalThis.recordPointerEvent");
#| }
#| if (typeof globalThis.recordWheelEvent === "function") {
#| (0, eval)("var recordWheelEvent = globalThis.recordWheelEvent");
#| }
#| if (typeof globalThis.resetEvents === "function") {
#| (0, eval)("var resetEvents = globalThis.resetEvents");
#| }
#| } catch (_e) {}
#|
#| if (keyboardRecorder) {
#| const keyReporter = document.getElementById("keys");
#| const hasKeyListeners = !!(
#| keyReporter &&
#| keyReporter._listeners &&
#| (
#| (Array.isArray(keyReporter._listeners.keydown) && keyReporter._listeners.keydown.length > 0) ||
#| (Array.isArray(keyReporter._listeners.keyup) && keyReporter._listeners.keyup.length > 0) ||
#| (Array.isArray(keyReporter._listeners.keypress) && keyReporter._listeners.keypress.length > 0)
#| )
#| );
#| if (keyReporter && !hasKeyListeners && !keyReporter.__craterKeyboardListeners) {
#| keyReporter.addEventListener("keyup", keyboardRecorder);
#| keyReporter.addEventListener("keypress", keyboardRecorder);
#| keyReporter.addEventListener("keydown", keyboardRecorder);
#| keyReporter.__craterKeyboardListeners = true;
#| }
#| }
#|
#| if (pointerRecorder) {
#| const outer = document.getElementById("outer");
#| const hasPointerListeners = !!(
#| outer &&
#| outer._listeners &&
#| (
#| (Array.isArray(outer._listeners.click) && outer._listeners.click.length > 0) ||
#| (Array.isArray(outer._listeners.dblclick) && outer._listeners.dblclick.length > 0) ||
#| (Array.isArray(outer._listeners.mousedown) && outer._listeners.mousedown.length > 0) ||
#| (Array.isArray(outer._listeners.mouseup) && outer._listeners.mouseup.length > 0) ||
#| (Array.isArray(outer._listeners.contextmenu) && outer._listeners.contextmenu.length > 0)
#| )
#| );
#| if (outer && !hasPointerListeners && !outer.__craterPointerListeners) {
#| outer.addEventListener("click", pointerRecorder);
#| outer.addEventListener("dblclick", pointerRecorder);
#| outer.addEventListener("mousedown", pointerRecorder);
#| outer.addEventListener("mouseup", pointerRecorder);
#| outer.addEventListener("contextmenu", pointerRecorder);
#| outer.__craterPointerListeners = true;
#| }
#| const hasWindowMouseMove = !!(
#| window &&
#| window._listeners &&
#| Array.isArray(window._listeners.mousemove) &&
#| window._listeners.mousemove.length > 0
#| );
#| if (window && typeof window.addEventListener === "function" && !hasWindowMouseMove && !window.__craterFirstPointerMoveListener) {
#| const recordFirstPointerMove = (event) => {
#| pointerRecorder(event);
#| try {
#| window.removeEventListener("mousemove", recordFirstPointerMove);
#| } catch (_e) {}
#| };
#| window.addEventListener("mousemove", recordFirstPointerMove);
#| window.__craterFirstPointerMoveListener = true;
#| }
#| }
#|
#| if (wheelRecorder) {
#| eventReporter = document.getElementById("event-reporter");
#| const notScrollable = document.getElementById("not-scrollable");
#| const hasNotScrollableWheel = !!(
#| notScrollable &&
#| notScrollable._listeners &&
#| Array.isArray(notScrollable._listeners.wheel) &&
#| notScrollable._listeners.wheel.length > 0
#| );
#| if (notScrollable && !hasNotScrollableWheel && !notScrollable.__craterWheelListener) {
#| notScrollable.addEventListener("wheel", wheelRecorder);
#| notScrollable.__craterWheelListener = true;
#| }
#| const scrollable = document.getElementById("scrollable");
#| const hasScrollableWheel = !!(
#| scrollable &&
#| scrollable._listeners &&
#| Array.isArray(scrollable._listeners.wheel) &&
#| scrollable._listeners.wheel.length > 0
#| );
#| if (scrollable && !hasScrollableWheel && !scrollable.__craterWheelListener) {
#| scrollable.addEventListener("wheel", wheelRecorder);
#| scrollable.__craterWheelListener = true;
#| }
#| }
#|
#| if (!window.__craterAllEventsFallbackInit) {
#| const pushEvent = (event) => {
#| window.allEvents.events.push({
#| type: event.type,
#| code: event.code,
#| key: event.key,
#| which: event.which,
#| location: event.location,
#| ctrl: event.ctrlKey,
#| meta: event.metaKey,
#| shift: event.shiftKey,
#| repeat: event.repeat,
#| button: event.button,
#| buttons: event.buttons,
#| pageX: event.pageX,
#| pageY: event.pageY,
#| deltaX: event.deltaX,
#| deltaY: event.deltaY,
#| deltaZ: event.deltaZ,
#| deltaMode: event.deltaMode,
#| clientX: event.clientX,
#| clientY: event.clientY,
#| isTrusted: event.isTrusted,
#| detail: event.detail,
#| target: event.target && event.target.id ? event.target.id : "",
#| pointerType: event.pointerType || "",
#| width: event.width,
#| height: event.height,
#| pressure: event.pressure,
#| tangentialPressure: event.tangentialPressure,
#| tiltX: event.tiltX,
#| tiltY: event.tiltY,
#| twist: event.twist,
#| altitudeAngle: event.altitudeAngle,
#| azimuthAngle: event.azimuthAngle,
#| altKey: event.altKey,
#| ctrlKey: event.ctrlKey,
#| metaKey: event.metaKey,
#| shiftKey: event.shiftKey,
#| });
#| };
#| const hasKeyboardRecorder = !!keyboardRecorder;
#| const hasPointerRecorder = !!pointerRecorder;
#| const hasWheelRecorder = !!wheelRecorder;
#|
#| const keyReporter = document.getElementById("keys");
#| if (keyReporter && !hasKeyboardRecorder) {
#| keyReporter.addEventListener("keydown", pushEvent);
#| keyReporter.addEventListener("keypress", pushEvent);
#| keyReporter.addEventListener("keyup", pushEvent);
#| }
#|
#| if (!hasPointerRecorder) {
#| const pointerTarget = document;
#| for (const eventName of ["mousemove", "mousedown", "mouseup", "click", "dblclick", "contextmenu", "auxclick"]) {
#| pointerTarget.addEventListener(eventName, pushEvent);
#| }
#| }
#|
#| const notScrollable = document.getElementById("not-scrollable");
#| if (notScrollable && !hasWheelRecorder) {
#| notScrollable.addEventListener("wheel", pushEvent);
#| }
#| const scrollable = document.getElementById("scrollable");
#| if (scrollable && !hasWheelRecorder) {
#| scrollable.addEventListener("wheel", pushEvent);
#| }
#| window.__craterAllEventsFallbackInit = true;
#| }
#| return null;
#| })`
///|
extern "js" fn js_test_actions_page_specific_source() -> String =
#| () => String.raw`(() => {
#| const setStyle = (el, styles) => {
#| if (!el || !el.style) return;
#| for (const [key, value] of Object.entries(styles)) {
#| try { el.style[key] = value; } catch (_e) {}
#| }
#| };
#| for (const el of Array.from(document.querySelectorAll(".area"))) {
#| setStyle(el, {
#| width: "100px",
#| height: "50px",
#| backgroundColor: "#ccc",
#| });
#| }
#| for (const el of Array.from(document.querySelectorAll(".block"))) {
#| setStyle(el, {
#| width: "5px",
#| height: "5px",
#| borderWidth: "1px",
#| borderStyle: "solid",
#| borderColor: "red",
#| });
#| }
#| setStyle(document.getElementById("trackPointer"), { position: "fixed" });
#| setStyle(document.getElementById("resultContainer"), { width: "600px", height: "60px" });
#| setStyle(document.getElementById("dragArea"), { position: "relative" });
#| setStyle(document.getElementById("dragTarget"), {
#| position: "absolute",
#| top: "22px",
#| left: "47px",
#| });
#| return null;
#| })`
///|
extern "js" fn js_test_actions_scroll_page_specific_source() -> String =
#| () => String.raw`(() => {
#| const ensure = (id, tagName = "div", parent = document.body) => {
#| let element = document.getElementById(id);
#| if (!element && document && typeof document.createElement === "function") {
#| element = document.createElement(tagName);
#| element.id = id;
#| parent.appendChild(element);
#| }
#| return element;
#| };
#| const setStyle = (el, styles) => {
#| if (!el || !el.style) return;
#| for (const [key, value] of Object.entries(styles)) {
#| try { el.style[key] = value; } catch (_e) {}
#| }
#| };
#| if (!window.allEvents || !Array.isArray(window.allEvents.events)) {
#| window.allEvents = { events: [] };
#| }
#| window.recordWheelEvent = (event) => {
#| if (!window.allEvents || !Array.isArray(window.allEvents.events)) {
#| window.allEvents = { events: [] };
#| }
#| const target = event && event.target ? event.target : null;
#| window.allEvents.events.push({
#| type: event?.type || "wheel",
#| button: event?.button ?? 0,
#| buttons: event?.buttons ?? 0,
#| pageX: event?.pageX ?? 0,
#| pageY: event?.pageY ?? 0,
#| deltaX: event?.deltaX ?? 0,
#| deltaY: event?.deltaY ?? 0,
#| deltaZ: event?.deltaZ ?? 0,
#| deltaMode: event?.deltaMode ?? 0,
#| target: target && target.id ? target.id : "",
#| altKey: !!event?.altKey,
#| ctrlKey: !!event?.ctrlKey,
#| metaKey: !!event?.metaKey,
#| shiftKey: !!event?.shiftKey,
#| });
#| };
#| const notScrollable = ensure("not-scrollable");
#| const notScrollableContent = ensure("not-scrollable-content", "div", notScrollable || document.body);
#| setStyle(notScrollable, { width: "100px", height: "50px", marginBottom: "100px" });
#| setStyle(notScrollableContent, { width: "200px", height: "100px", backgroundColor: "#ccc" });
#| const scrollable = ensure("scrollable");
#| const scrollableContent = ensure("scrollable-content", "div", scrollable || document.body);
#| setStyle(scrollable, { width: "100px", height: "100px", overflow: "scroll" });
#| setStyle(scrollableContent, { width: "600px", height: "1000px", backgroundColor: "blue" });
#| if (notScrollable && !notScrollable.__craterWheelListener) {
#| notScrollable.addEventListener("wheel", window.recordWheelEvent);
#| notScrollable.__craterWheelListener = true;
#| }
#| if (scrollable && !scrollable.__craterWheelListener) {
#| scrollable.addEventListener("wheel", window.recordWheelEvent);
#| scrollable.__craterWheelListener = true;
#| }
#| const iframe = ensure("iframe", "iframe");
#| setStyle(iframe, { width: "100px", height: "100px" });
#| if (iframe && !iframe.__craterWheelIframeSetup) {
#| iframe.srcdoc =
#| '