mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
4715 lines
172 KiB
Rust
4715 lines
172 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
fs,
|
|
path::{Path as FsPath, PathBuf},
|
|
time::Duration,
|
|
};
|
|
|
|
use axum::{
|
|
body::Bytes,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::Response,
|
|
};
|
|
use moli_core::page::RendererDocumentLifecycleMilestone;
|
|
use moli_protocol::{
|
|
devtools_runtime::{
|
|
DevToolsCommand, DevToolsCommandContext, DevToolsCommandResult, DevToolsDomGeometryResult,
|
|
DevToolsDomNodeReference, DevToolsDownloadBehaviorSetting, DevToolsError,
|
|
DevToolsErrorKind, DevToolsFrameId, DevToolsGetFrameOwnerCommand,
|
|
DevToolsGetFrameOwnerResult, DevToolsGetRealmsCommand, DevToolsGetRealmsResult,
|
|
DevToolsGetServiceWorkerLogsCommand, DevToolsGetServiceWorkerLogsResult,
|
|
DevToolsGetTargetsCommand, DevToolsGetTargetsResult, DevToolsLayoutMetricsResult,
|
|
DevToolsLocateNodesResult, DevToolsProtocol, DevToolsQuerySelectorResult,
|
|
DevToolsRemoteHandleId, DevToolsRemoteValue, DevToolsScriptException, DevToolsScriptResult,
|
|
DevToolsSessionId, DevToolsSetDownloadBehaviorCommand, DevToolsSetFileInputFilesCommand,
|
|
DevToolsTargetId, DevToolsTargetInfo, DevToolsTargetKind, RuntimeConsoleEvent,
|
|
RuntimeExecutionContextEvent,
|
|
},
|
|
version,
|
|
};
|
|
use moli_protocol_webdriver_classic::{
|
|
CLASSIC_ELEMENT_REFERENCE_KEY, CLASSIC_FRAME_REFERENCE_KEY, CLASSIC_SHADOW_ROOT_REFERENCE_KEY,
|
|
CLASSIC_WINDOW_REFERENCE_KEY, ClassicDevToolsCommandContext,
|
|
ClassicElementOriginViewportPoints, ClassicError, ClassicErrorCode, ClassicPageLoadStrategy,
|
|
ClassicPromptHandler, ClassicUnhandledPromptBehavior, ClassicViewportBounds,
|
|
action_element_origin_ids, active_element_command, alert_handle_command, alert_text_command,
|
|
classic_attribute_value, classic_error_from_devtools_error, classic_property_value,
|
|
classic_rect_from_geometry, classic_shadow_root_reference, classic_text_value,
|
|
clear_element_command, create_initial_target_command, current_url_command,
|
|
delete_session_response, describe_node_command, describe_node_reference_command,
|
|
element_center_from_geometry, element_click_command, element_click_input_commands,
|
|
element_click_prepare_reference_commands, element_screenshot_command,
|
|
element_send_keys_input_commands, element_send_keys_prepare_text_control_command,
|
|
element_send_keys_text, execute_async_command, execute_sync_command, find_element_command,
|
|
find_element_command_with_root, get_element_attributes_reference_command,
|
|
get_element_computed_label_command, get_element_computed_role_command,
|
|
get_element_css_value_command, get_element_displayed_command, get_element_enabled_command,
|
|
get_element_property_reference_command, get_element_rect_reference_command,
|
|
get_element_rendered_text_command, get_element_text_reference_command, history_traversal_entry,
|
|
layout_metrics_command, matched_capabilities_from_new_session_params, navigate_command,
|
|
navigation_history_command, new_session_response, page_load_strategy_from_capabilities,
|
|
page_source_command, parse_timeouts, print_page_command, refresh_command,
|
|
release_remote_object_command, resolve_element_reference_command_with_execution_context,
|
|
resolve_shadow_root_reference_command_with_execution_context, screenshot_command,
|
|
shadow_root_attached_command, status_response as classic_status_response, timeouts_value,
|
|
title_command, traverse_history_command, unhandled_prompt_behavior_from_capabilities,
|
|
verify_element_attached_command, window_handles_command, window_handles_from_targets,
|
|
};
|
|
use serde_json::{Map, Value, json};
|
|
use tokio::time::sleep;
|
|
use tracing::warn;
|
|
|
|
use super::AppState;
|
|
use super::webdriver_files::{
|
|
downloadable_file_bytes, downloadable_file_zip_base64, selected_files_from_paths,
|
|
unique_download_directory, uploaded_file_from_base64_zip,
|
|
};
|
|
|
|
mod alerts;
|
|
mod cookies;
|
|
mod dom_refs;
|
|
mod helpers;
|
|
mod script_refs;
|
|
mod state;
|
|
mod window;
|
|
|
|
pub(super) use alerts::{
|
|
webdriver_classic_accept_alert, webdriver_classic_dismiss_alert,
|
|
webdriver_classic_get_alert_text, webdriver_classic_send_alert_text,
|
|
};
|
|
pub(super) use cookies::{
|
|
webdriver_classic_add_cookie, webdriver_classic_delete_all_cookies,
|
|
webdriver_classic_delete_cookie, webdriver_classic_get_cookies,
|
|
webdriver_classic_get_named_cookie,
|
|
};
|
|
use dom_refs::{resolve_classic_element_dom_reference, resolve_classic_shadow_root_dom_reference};
|
|
use helpers::{
|
|
classic_error_into_response, classic_json_body, classic_success_into_response,
|
|
classic_webdriver_json_response,
|
|
};
|
|
use script_refs::{
|
|
ClassicScriptCanonicalNodeReference, ClassicScriptFrameOwnerReferenceKey,
|
|
ClassicScriptResultReference, classic_script_canonical_dom_reference,
|
|
classic_script_canonical_dom_reference_from_described_node,
|
|
classic_script_frame_owner_dom_reference_from_described_node,
|
|
classic_script_result_contains_popup_window_reference, classic_script_result_reference,
|
|
collect_classic_script_dom_reference_node_ids,
|
|
collect_classic_script_frame_reference_owner_keys,
|
|
};
|
|
pub(super) use state::SharedClassicSessionRegistry;
|
|
use state::{ClassicSessionBinding, ClassicSessionRuntimeHandle};
|
|
pub(super) use window::{
|
|
webdriver_classic_close_window, webdriver_classic_fullscreen_window,
|
|
webdriver_classic_get_window, webdriver_classic_get_window_handles,
|
|
webdriver_classic_get_window_rect, webdriver_classic_maximize_window,
|
|
webdriver_classic_minimize_window, webdriver_classic_new_window,
|
|
webdriver_classic_set_window_rect, webdriver_classic_switch_window,
|
|
};
|
|
|
|
const CLASSIC_IMPLICIT_WAIT_POLL_INTERVAL: Duration = Duration::from_millis(50);
|
|
const CLASSIC_SCRIPT_ARG_ELEMENT_REFERENCE: &str = "__moliClassicElementReference";
|
|
const CLASSIC_SCRIPT_ARG_SHADOW_ROOT_REFERENCE: &str = "__moliClassicShadowRootReference";
|
|
const CLASSIC_SCRIPT_ARG_FRAME_REFERENCE: &str = "__moliClassicFrameReference";
|
|
const CLASSIC_SCRIPT_ARG_WINDOW_REFERENCE: &str = "__moliClassicWindowReference";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_MARKER: &str = "__moliClassicWebReference";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_NODE_ID: &str = "nodeId";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_BACKEND_NODE_ID: &str = "backendNodeId";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_ELEMENT: &str = "element";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_SHADOW_ROOT: &str = "shadow-root";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_FRAME: &str = "frame";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_WINDOW: &str = "window";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_POPUP_WINDOW: &str = "popup-window";
|
|
const CLASSIC_SCRIPT_WEB_REFERENCE_POPUP_ID: &str = "popupId";
|
|
const CLASSIC_SCRIPT_SENTINEL_STALE_ELEMENT: &str =
|
|
"__moli_webdriver_classic_stale_element_reference__";
|
|
const CLASSIC_SCRIPT_SENTINEL_DETACHED_SHADOW_ROOT: &str =
|
|
"__moli_webdriver_classic_detached_shadow_root__";
|
|
const CLASSIC_SCRIPT_SENTINEL_NO_SUCH_FRAME: &str = "__moli_webdriver_classic_no_such_frame__";
|
|
const CLASSIC_SCRIPT_SENTINEL_ELEMENT_NOT_INTERACTABLE: &str =
|
|
"__moli_webdriver_classic_element_not_interactable__";
|
|
|
|
fn classic_top_level_context(binding: &ClassicSessionBinding) -> ClassicDevToolsCommandContext {
|
|
ClassicDevToolsCommandContext::with_target_id(&binding.session_id, &binding.target_id)
|
|
}
|
|
|
|
fn classic_top_level_devtools_context(binding: &ClassicSessionBinding) -> DevToolsCommandContext {
|
|
DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverClassic,
|
|
session_id: Some(DevToolsSessionId::from(binding.session_id.as_str())),
|
|
target_id: Some(DevToolsTargetId::from(binding.target_id.as_str())),
|
|
browser_context_id: None,
|
|
}
|
|
}
|
|
|
|
fn classic_browsing_context(binding: &ClassicSessionBinding) -> ClassicDevToolsCommandContext {
|
|
ClassicDevToolsCommandContext::with_target_id(
|
|
&binding.session_id,
|
|
binding.browsing_context_target_id(),
|
|
)
|
|
}
|
|
|
|
fn classic_session_binding(
|
|
state: &AppState,
|
|
session_id: &str,
|
|
) -> Result<ClassicSessionBinding, ClassicError> {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.session_binding(session_id)
|
|
.ok_or_else(|| ClassicError::new(ClassicErrorCode::InvalidSessionId, "session not found"))
|
|
}
|
|
|
|
async fn classic_current_browsing_context_binding(
|
|
state: &AppState,
|
|
session_id: &str,
|
|
) -> Result<ClassicSessionBinding, ClassicError> {
|
|
classic_binding_with_existing_current_browsing_context(classic_session_binding(
|
|
state, session_id,
|
|
)?)
|
|
.await
|
|
}
|
|
|
|
async fn classic_top_level_browsing_context_binding(
|
|
state: &AppState,
|
|
session_id: &str,
|
|
) -> Result<ClassicSessionBinding, ClassicError> {
|
|
let binding =
|
|
classic_top_level_browsing_context_binding_without_prompt_handling(state, session_id)
|
|
.await?;
|
|
let context = classic_top_level_context(&binding);
|
|
classic_handle_unhandled_prompt(&binding, &context).await?;
|
|
Ok(binding)
|
|
}
|
|
|
|
async fn classic_top_level_browsing_context_binding_without_prompt_handling(
|
|
state: &AppState,
|
|
session_id: &str,
|
|
) -> Result<ClassicSessionBinding, ClassicError> {
|
|
let binding = classic_session_binding(state, session_id)?;
|
|
ensure_classic_top_level_browsing_context_exists(&binding).await?;
|
|
Ok(binding)
|
|
}
|
|
|
|
async fn classic_binding_with_existing_current_browsing_context(
|
|
binding: ClassicSessionBinding,
|
|
) -> Result<ClassicSessionBinding, ClassicError> {
|
|
ensure_classic_top_level_browsing_context_exists(&binding).await?;
|
|
let context = classic_top_level_context(&binding);
|
|
classic_handle_unhandled_prompt(&binding, &context).await?;
|
|
// Match ChromeDriver's window-command boundary: settle an unhandled
|
|
// prompt first, then wait for the current navigation before touching the
|
|
// current frame or its renderer objects.
|
|
classic_wait_for_current_document(&binding).await?;
|
|
ensure_classic_current_browsing_context_exists(&binding).await?;
|
|
Ok(binding)
|
|
}
|
|
|
|
async fn classic_wait_for_current_document(
|
|
binding: &ClassicSessionBinding,
|
|
) -> Result<(), ClassicError> {
|
|
let milestone = match binding.page_load_strategy {
|
|
ClassicPageLoadStrategy::None => return Ok(()),
|
|
ClassicPageLoadStrategy::Eager => RendererDocumentLifecycleMilestone::DomContentLoaded,
|
|
ClassicPageLoadStrategy::Normal => RendererDocumentLifecycleMilestone::Load,
|
|
};
|
|
binding
|
|
.runtime
|
|
.wait_for_document_lifecycle(
|
|
classic_top_level_devtools_context(binding),
|
|
milestone,
|
|
binding.timeouts.page_load.map(Duration::from_millis),
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
if error.kind == DevToolsErrorKind::Timeout {
|
|
ClassicError::new(ClassicErrorCode::Timeout, "page load timed out")
|
|
} else {
|
|
classic_error_from_devtools_error(error)
|
|
}
|
|
})
|
|
}
|
|
|
|
async fn classic_handle_unhandled_prompt(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
) -> Result<(), ClassicError> {
|
|
let dialog = match binding.runtime.execute(alert_text_command(context)).await {
|
|
Ok(DevToolsCommandResult::JavaScriptDialog(dialog)) => dialog,
|
|
Ok(_) => {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get alert text returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) if error.kind == DevToolsErrorKind::NoSuchAlert => return Ok(()),
|
|
Err(error) => return Err(classic_error_from_devtools_error(error)),
|
|
};
|
|
let handler = binding
|
|
.unhandled_prompt_behavior
|
|
.handler_for_prompt_type(&dialog.dialog_type);
|
|
match handler {
|
|
ClassicPromptHandler::Ignore => Err(classic_unexpected_alert_open_error(&dialog.message)),
|
|
ClassicPromptHandler::Accept { notify } | ClassicPromptHandler::Dismiss { notify } => {
|
|
let accept = matches!(handler, ClassicPromptHandler::Accept { .. });
|
|
match binding
|
|
.runtime
|
|
.execute(alert_handle_command(context, accept))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::Empty) => {
|
|
if notify {
|
|
Err(classic_unexpected_alert_open_error(&dialog.message))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"handle alert returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn classic_unexpected_alert_open_error(message: &str) -> ClassicError {
|
|
ClassicError::with_data(
|
|
ClassicErrorCode::UnexpectedAlertOpen,
|
|
"unexpected alert open",
|
|
json!({ "text": message }),
|
|
)
|
|
}
|
|
|
|
async fn ensure_classic_current_browsing_context_exists(
|
|
binding: &ClassicSessionBinding,
|
|
) -> Result<(), ClassicError> {
|
|
let Some(frame_id) = binding.current_frame_id.clone() else {
|
|
return Ok(());
|
|
};
|
|
ensure_classic_browsing_context_exists(binding, Some(frame_id)).await
|
|
}
|
|
|
|
fn classic_note_top_level_document_change(state: &AppState, session_id: &str, target_id: &str) {
|
|
let mut registry = state.classic_session_registry.lock();
|
|
registry.bump_document_generation(session_id, target_id);
|
|
registry.set_current_frame_id(session_id, None);
|
|
}
|
|
|
|
async fn ensure_classic_top_level_browsing_context_exists(
|
|
binding: &ClassicSessionBinding,
|
|
) -> Result<(), ClassicError> {
|
|
let context = ClassicDevToolsCommandContext::new(&binding.session_id);
|
|
match binding
|
|
.runtime
|
|
.execute(window_handles_command(&context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::GetTargets(result)) => {
|
|
if window_handles_from_targets(result)
|
|
.iter()
|
|
.any(|target_id| target_id == &binding.target_id)
|
|
{
|
|
Ok(())
|
|
} else {
|
|
Err(classic_no_such_window_for_missing_browsing_context())
|
|
}
|
|
}
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"window handles returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn ensure_classic_frame_switch_target_ready(
|
|
binding: &ClassicSessionBinding,
|
|
frame_id: &str,
|
|
) -> Result<(), ClassicError> {
|
|
match binding
|
|
.runtime
|
|
.browsing_context_exists(
|
|
binding.session_id.clone(),
|
|
binding.target_id.clone(),
|
|
Some(frame_id.to_owned()),
|
|
)
|
|
.await
|
|
{
|
|
Ok(true) => Ok(()),
|
|
Ok(false) => Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchFrame,
|
|
"frame not found",
|
|
)),
|
|
Err(error) => Err(classic_no_such_frame_error(error)),
|
|
}?;
|
|
|
|
let frame_context =
|
|
ClassicDevToolsCommandContext::with_target_id(&binding.session_id, frame_id);
|
|
match binding
|
|
.runtime
|
|
.execute(page_source_command(&frame_context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::GetOuterHtml(_)) => Ok(()),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"frame readiness probe returned an unexpected result",
|
|
)),
|
|
Err(error)
|
|
if matches!(
|
|
error.kind,
|
|
DevToolsErrorKind::NoSuchTarget | DevToolsErrorKind::NoSuchNode
|
|
) || error.message == "NoDocumentLoaded" =>
|
|
{
|
|
Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchFrame,
|
|
"frame not found",
|
|
))
|
|
}
|
|
Err(error) => Err(classic_no_such_frame_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn ensure_classic_browsing_context_exists(
|
|
binding: &ClassicSessionBinding,
|
|
frame_id: Option<String>,
|
|
) -> Result<(), ClassicError> {
|
|
match binding
|
|
.runtime
|
|
.browsing_context_exists(
|
|
binding.session_id.clone(),
|
|
binding.target_id.clone(),
|
|
frame_id,
|
|
)
|
|
.await
|
|
{
|
|
Ok(true) => Ok(()),
|
|
Ok(false) => Err(classic_no_such_window_for_missing_browsing_context()),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_no_such_window_for_missing_browsing_context() -> ClassicError {
|
|
ClassicError::new(
|
|
ClassicErrorCode::NoSuchWindow,
|
|
"browsing context no longer exists",
|
|
)
|
|
}
|
|
|
|
fn classic_element_reference_from_id(element_id: String) -> Value {
|
|
json!({
|
|
CLASSIC_ELEMENT_REFERENCE_KEY: element_id,
|
|
})
|
|
}
|
|
|
|
fn registered_classic_element_reference_from_dom_reference(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
node_id: u32,
|
|
reference: DevToolsDomNodeReference,
|
|
) -> Value {
|
|
let element_id = state
|
|
.classic_session_registry
|
|
.lock()
|
|
.register_element_reference(binding, node_id, reference);
|
|
classic_element_reference_from_id(element_id)
|
|
}
|
|
|
|
fn registered_classic_element_references_from_canonical_references(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
references: impl IntoIterator<Item = ClassicScriptCanonicalNodeReference>,
|
|
) -> Vec<Value> {
|
|
references
|
|
.into_iter()
|
|
.map(|reference| {
|
|
registered_classic_element_reference_from_dom_reference(
|
|
state,
|
|
binding,
|
|
reference.node_id,
|
|
reference.reference,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn registered_classic_shadow_root_reference_from_dom_reference(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
node_id: u32,
|
|
reference: DevToolsDomNodeReference,
|
|
) -> Value {
|
|
let shadow_root_id = state
|
|
.classic_session_registry
|
|
.lock()
|
|
.register_shadow_root_reference(binding, node_id, reference);
|
|
classic_shadow_root_reference(shadow_root_id)
|
|
}
|
|
|
|
fn classic_window_reference(window_id: impl Into<String>) -> Value {
|
|
json!({
|
|
CLASSIC_WINDOW_REFERENCE_KEY: window_id.into(),
|
|
})
|
|
}
|
|
|
|
fn classic_frame_reference(frame_id: impl Into<String>) -> Value {
|
|
json!({
|
|
CLASSIC_FRAME_REFERENCE_KEY: frame_id.into(),
|
|
})
|
|
}
|
|
|
|
async fn release_classic_remote_object(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
object_id: impl Into<String>,
|
|
label: &str,
|
|
) {
|
|
let object_id = object_id.into();
|
|
if let Err(error) = binding
|
|
.runtime
|
|
.execute(release_remote_object_command(context, object_id))
|
|
.await
|
|
{
|
|
warn!(
|
|
?error,
|
|
label, "failed to release WebDriver Classic remote object"
|
|
);
|
|
}
|
|
}
|
|
|
|
async fn release_classic_remote_handle(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
value: &DevToolsRemoteValue,
|
|
label: &str,
|
|
) {
|
|
let object_id = value
|
|
.handle
|
|
.as_ref()
|
|
.or(value.shared_id.as_ref())
|
|
.map(|handle| handle.as_str().to_owned());
|
|
if let Some(object_id) = object_id {
|
|
release_classic_remote_object(binding, context, object_id, label).await;
|
|
}
|
|
}
|
|
|
|
async fn resolve_classic_element_remote_object_from_reference(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
reference: DevToolsDomNodeReference,
|
|
execution_context_id: Option<i64>,
|
|
object_group: &str,
|
|
label: &str,
|
|
) -> Result<String, ClassicError> {
|
|
let resolve = resolve_element_reference_command_with_execution_context(
|
|
context,
|
|
reference,
|
|
execution_context_id,
|
|
object_group,
|
|
);
|
|
match binding.runtime.execute(resolve).await {
|
|
Ok(DevToolsCommandResult::ResolveNode(result)) => result
|
|
.object
|
|
.get("objectId")
|
|
.and_then(Value::as_str)
|
|
.map(ToOwned::to_owned)
|
|
.ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::StaleElementReference,
|
|
"element is no longer attached to the DOM",
|
|
)
|
|
}),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} resolve returned an unexpected result"),
|
|
)),
|
|
Err(error) if matches!(error.kind, DevToolsErrorKind::NoSuchNode) => {
|
|
Err(ClassicError::new(
|
|
ClassicErrorCode::StaleElementReference,
|
|
"element is no longer attached to the DOM",
|
|
))
|
|
}
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn resolve_classic_remote_object(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
object_group: &str,
|
|
label: &str,
|
|
) -> Result<String, ClassicError> {
|
|
let reference = resolve_classic_element_dom_reference(state, binding, element_id)?;
|
|
resolve_classic_element_remote_object_from_reference(
|
|
binding,
|
|
context,
|
|
reference,
|
|
None,
|
|
object_group,
|
|
label,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn resolve_classic_shadow_root_remote_object_from_reference(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
reference: DevToolsDomNodeReference,
|
|
execution_context_id: Option<i64>,
|
|
object_group: &str,
|
|
label: &str,
|
|
) -> Result<String, ClassicError> {
|
|
let resolve = resolve_shadow_root_reference_command_with_execution_context(
|
|
context,
|
|
reference,
|
|
execution_context_id,
|
|
object_group,
|
|
);
|
|
match binding.runtime.execute(resolve).await {
|
|
Ok(DevToolsCommandResult::ResolveNode(result)) => result
|
|
.object
|
|
.get("objectId")
|
|
.and_then(Value::as_str)
|
|
.map(ToOwned::to_owned)
|
|
.ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::DetachedShadowRoot,
|
|
"shadow root is detached",
|
|
)
|
|
}),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} resolve returned an unexpected result"),
|
|
)),
|
|
Err(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::DetachedShadowRoot,
|
|
"shadow root is detached",
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn classic_frame_owner_dom_reference(
|
|
binding: &ClassicSessionBinding,
|
|
frame_id: &str,
|
|
) -> Result<DevToolsDomNodeReference, ClassicError> {
|
|
let context = DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverClassic,
|
|
session_id: Some(DevToolsSessionId::from(binding.session_id.as_str())),
|
|
target_id: Some(DevToolsTargetId::from(binding.target_id.as_str())),
|
|
browser_context_id: None,
|
|
};
|
|
match binding
|
|
.runtime
|
|
.execute(DevToolsCommand::GetFrameOwner(
|
|
DevToolsGetFrameOwnerCommand {
|
|
context,
|
|
frame_id: DevToolsFrameId::from(frame_id),
|
|
},
|
|
))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::GetFrameOwner(owner)) => {
|
|
Ok(classic_dom_reference_from_frame_owner_result(owner))
|
|
}
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"frame owner lookup returned an unexpected result",
|
|
)),
|
|
Err(error)
|
|
if error.kind == DevToolsErrorKind::NoSuchTarget
|
|
|| error.kind == DevToolsErrorKind::NoSuchNode
|
|
|| error.message == "Frame with the given id does not belong to the target."
|
|
|| error.message == "Frame with the given id was not found."
|
|
|| error.message == "FrameOwnerRequiresPendingChildFrameResolution" =>
|
|
{
|
|
Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchFrame,
|
|
error.message,
|
|
))
|
|
}
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_dom_reference_from_frame_owner_result(
|
|
owner: DevToolsGetFrameOwnerResult,
|
|
) -> DevToolsDomNodeReference {
|
|
if owner.backend_node_id > 0 {
|
|
DevToolsDomNodeReference::BackendNodeId(owner.backend_node_id)
|
|
} else {
|
|
DevToolsDomNodeReference::FrontendNodeId(owner.node_id)
|
|
}
|
|
}
|
|
|
|
async fn classic_default_execution_context_id(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
label: &str,
|
|
) -> Result<i64, ClassicError> {
|
|
let command = DevToolsCommand::GetRealms(DevToolsGetRealmsCommand {
|
|
context: DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverClassic,
|
|
session_id: Some(DevToolsSessionId::from(context.session_id.as_str())),
|
|
target_id: context.target_id.as_deref().map(DevToolsTargetId::from),
|
|
browser_context_id: None,
|
|
},
|
|
realm_type: Some("window".to_owned()),
|
|
});
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::Realms(result)) => result
|
|
.realms
|
|
.iter()
|
|
.find(|realm| realm.is_default == Some(true) && realm.context_id.is_some())
|
|
.or_else(|| {
|
|
result
|
|
.realms
|
|
.iter()
|
|
.find(|realm| realm.context_id.is_some())
|
|
})
|
|
.and_then(|realm| realm.context_id)
|
|
.ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} could not resolve the current execution context"),
|
|
)
|
|
}),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} realm lookup returned an unexpected result"),
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn release_classic_remote_objects(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
object_ids: Vec<String>,
|
|
label: &str,
|
|
) {
|
|
for object_id in object_ids {
|
|
release_classic_remote_object(binding, context, object_id, label).await;
|
|
}
|
|
}
|
|
|
|
struct ClassicScriptArgumentHandles {
|
|
descriptors: Vec<Value>,
|
|
remote_handles: Vec<String>,
|
|
}
|
|
|
|
enum ClassicScriptArgumentReference<'a> {
|
|
Element(&'a str),
|
|
ShadowRoot(&'a str),
|
|
Frame(&'a str),
|
|
Window(&'a str),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum ClassicElementLiveIdentity {
|
|
BackendNodeId(u32),
|
|
FrontendNodeId(u32),
|
|
}
|
|
|
|
async fn prepare_classic_script_argument_handles(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
params: &Value,
|
|
label: &'static str,
|
|
) -> Result<ClassicScriptArgumentHandles, ClassicError> {
|
|
let descriptors = classic_script_argument_descriptors(params)?;
|
|
let references = classic_script_argument_references(&descriptors)?;
|
|
let script_execution_context_id = if references.iter().any(|reference| {
|
|
matches!(
|
|
reference,
|
|
ClassicScriptArgumentReference::Element(_)
|
|
| ClassicScriptArgumentReference::ShadowRoot(_)
|
|
)
|
|
}) {
|
|
Some(classic_default_execution_context_id(binding, context, label).await?)
|
|
} else {
|
|
None
|
|
};
|
|
let mut remote_handles = Vec::new();
|
|
for reference in references {
|
|
let object_id = match reference {
|
|
ClassicScriptArgumentReference::Element(element_id) => {
|
|
let reference =
|
|
match resolve_classic_element_dom_reference(state, binding, element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => {
|
|
release_classic_remote_objects(binding, context, remote_handles, label)
|
|
.await;
|
|
return Err(error);
|
|
}
|
|
};
|
|
resolve_classic_element_remote_object_from_reference(
|
|
binding,
|
|
context,
|
|
reference,
|
|
script_execution_context_id,
|
|
"webdriver-classic-script-argument",
|
|
label,
|
|
)
|
|
.await
|
|
}
|
|
ClassicScriptArgumentReference::ShadowRoot(shadow_root_id) => {
|
|
let reference =
|
|
match resolve_classic_shadow_root_dom_reference(state, binding, shadow_root_id)
|
|
{
|
|
Ok(reference) => reference,
|
|
Err(error) => {
|
|
release_classic_remote_objects(binding, context, remote_handles, label)
|
|
.await;
|
|
return Err(error);
|
|
}
|
|
};
|
|
match resolve_classic_shadow_root_remote_object_from_reference(
|
|
binding,
|
|
context,
|
|
reference,
|
|
script_execution_context_id,
|
|
"webdriver-classic-script-argument",
|
|
label,
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => {
|
|
if let Err(error) = verify_classic_shadow_root_remote_object_attached(
|
|
binding, context, &object_id, label,
|
|
)
|
|
.await
|
|
{
|
|
release_classic_remote_object(binding, context, object_id, label).await;
|
|
release_classic_remote_objects(binding, context, remote_handles, label)
|
|
.await;
|
|
return Err(error);
|
|
}
|
|
Ok(object_id)
|
|
}
|
|
Err(error) => Err(error),
|
|
}
|
|
}
|
|
ClassicScriptArgumentReference::Frame(frame_id) => {
|
|
let owner_reference =
|
|
match classic_frame_owner_dom_reference(binding, frame_id).await {
|
|
Ok(reference) => reference,
|
|
Err(error) => {
|
|
release_classic_remote_objects(binding, context, remote_handles, label)
|
|
.await;
|
|
return Err(error);
|
|
}
|
|
};
|
|
let top_context = classic_top_level_context(binding);
|
|
resolve_classic_element_remote_object_from_reference(
|
|
binding,
|
|
&top_context,
|
|
owner_reference,
|
|
None,
|
|
"webdriver-classic-script-argument",
|
|
label,
|
|
)
|
|
.await
|
|
}
|
|
ClassicScriptArgumentReference::Window(window_id) => {
|
|
if window_id != binding.browsing_context_target_id() {
|
|
release_classic_remote_objects(binding, context, remote_handles, label).await;
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchWindow,
|
|
"window with such id was not found",
|
|
));
|
|
}
|
|
continue;
|
|
}
|
|
};
|
|
match object_id {
|
|
Ok(object_id) => remote_handles.push(object_id),
|
|
Err(error) => {
|
|
release_classic_remote_objects(binding, context, remote_handles, label).await;
|
|
return Err(error);
|
|
}
|
|
}
|
|
}
|
|
Ok(ClassicScriptArgumentHandles {
|
|
descriptors,
|
|
remote_handles,
|
|
})
|
|
}
|
|
|
|
fn classic_script_argument_descriptors(params: &Value) -> Result<Vec<Value>, ClassicError> {
|
|
let Some(args) = params.get("args") else {
|
|
return Ok(Vec::new());
|
|
};
|
|
let Some(args) = args.as_array() else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"args must be an array",
|
|
));
|
|
};
|
|
args.iter()
|
|
.map(classic_script_argument_descriptor)
|
|
.collect()
|
|
}
|
|
|
|
fn classic_script_argument_descriptor(value: &Value) -> Result<Value, ClassicError> {
|
|
match value {
|
|
Value::Array(values) => values
|
|
.iter()
|
|
.map(classic_script_argument_descriptor)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map(Value::Array),
|
|
Value::Object(map) => {
|
|
if let Some(element_id) = map.get(CLASSIC_ELEMENT_REFERENCE_KEY) {
|
|
let Some(element_id) = element_id.as_str() else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"element reference must be a string",
|
|
));
|
|
};
|
|
return Ok(json!({
|
|
CLASSIC_SCRIPT_ARG_ELEMENT_REFERENCE: element_id,
|
|
}));
|
|
}
|
|
if let Some(shadow_root_id) = map.get(CLASSIC_SHADOW_ROOT_REFERENCE_KEY) {
|
|
let Some(shadow_root_id) = shadow_root_id.as_str() else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"shadow root reference must be a string",
|
|
));
|
|
};
|
|
return Ok(json!({
|
|
CLASSIC_SCRIPT_ARG_SHADOW_ROOT_REFERENCE: shadow_root_id,
|
|
}));
|
|
}
|
|
if let Some(frame_id) = map.get(CLASSIC_FRAME_REFERENCE_KEY) {
|
|
let Some(frame_id) = frame_id.as_str() else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"frame reference must be a string",
|
|
));
|
|
};
|
|
return Ok(json!({
|
|
CLASSIC_SCRIPT_ARG_FRAME_REFERENCE: frame_id,
|
|
}));
|
|
}
|
|
if let Some(window_id) = map.get(CLASSIC_WINDOW_REFERENCE_KEY) {
|
|
let Some(window_id) = window_id.as_str() else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"window reference must be a string",
|
|
));
|
|
};
|
|
return Ok(json!({
|
|
CLASSIC_SCRIPT_ARG_WINDOW_REFERENCE: window_id,
|
|
}));
|
|
}
|
|
let mut object = Map::new();
|
|
for (key, value) in map {
|
|
object.insert(key.clone(), classic_script_argument_descriptor(value)?);
|
|
}
|
|
Ok(Value::Object(object))
|
|
}
|
|
_ => Ok(value.clone()),
|
|
}
|
|
}
|
|
|
|
fn classic_script_argument_references(
|
|
descriptors: &[Value],
|
|
) -> Result<Vec<ClassicScriptArgumentReference<'_>>, ClassicError> {
|
|
let mut references = Vec::new();
|
|
for descriptor in descriptors {
|
|
collect_classic_script_argument_references(descriptor, &mut references)?;
|
|
}
|
|
Ok(references)
|
|
}
|
|
|
|
fn collect_classic_script_argument_references<'a>(
|
|
value: &'a Value,
|
|
out: &mut Vec<ClassicScriptArgumentReference<'a>>,
|
|
) -> Result<(), ClassicError> {
|
|
match value {
|
|
Value::Array(values) => {
|
|
for value in values {
|
|
collect_classic_script_argument_references(value, out)?;
|
|
}
|
|
}
|
|
Value::Object(map) => {
|
|
if let Some(element_id) =
|
|
classic_script_reference_id(map, CLASSIC_SCRIPT_ARG_ELEMENT_REFERENCE)?
|
|
{
|
|
out.push(ClassicScriptArgumentReference::Element(element_id));
|
|
return Ok(());
|
|
}
|
|
if let Some(shadow_root_id) =
|
|
classic_script_reference_id(map, CLASSIC_SCRIPT_ARG_SHADOW_ROOT_REFERENCE)?
|
|
{
|
|
out.push(ClassicScriptArgumentReference::ShadowRoot(shadow_root_id));
|
|
return Ok(());
|
|
}
|
|
if let Some(frame_id) =
|
|
classic_script_reference_id(map, CLASSIC_SCRIPT_ARG_FRAME_REFERENCE)?
|
|
{
|
|
out.push(ClassicScriptArgumentReference::Frame(frame_id));
|
|
return Ok(());
|
|
}
|
|
if let Some(window_id) =
|
|
classic_script_reference_id(map, CLASSIC_SCRIPT_ARG_WINDOW_REFERENCE)?
|
|
{
|
|
out.push(ClassicScriptArgumentReference::Window(window_id));
|
|
return Ok(());
|
|
}
|
|
for value in map.values() {
|
|
collect_classic_script_argument_references(value, out)?;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn classic_script_reference_id<'a>(
|
|
map: &'a Map<String, Value>,
|
|
key: &str,
|
|
) -> Result<Option<&'a str>, ClassicError> {
|
|
let Some(value) = map.get(key) else {
|
|
return Ok(None);
|
|
};
|
|
value.as_str().map(Some).ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"script argument reference id must be a string",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn apply_classic_script_argument_handles(
|
|
command: &mut DevToolsCommand,
|
|
handles: &ClassicScriptArgumentHandles,
|
|
) {
|
|
let DevToolsCommand::CallFunction(command) = command else {
|
|
return;
|
|
};
|
|
let user_function = command.function_declaration.clone();
|
|
command.function_declaration = classic_script_argument_deserializer_function(user_function);
|
|
command.arguments = std::iter::once(json!({ "value": handles.descriptors }))
|
|
.chain(
|
|
handles
|
|
.remote_handles
|
|
.iter()
|
|
.map(|object_id| json!({ "handle": object_id })),
|
|
)
|
|
.collect();
|
|
}
|
|
|
|
fn classic_script_argument_deserializer_function(user_function: String) -> String {
|
|
format!(
|
|
"function() {{\n\
|
|
const __moliUserFunction = ({user_function});\n\
|
|
const __moliArgumentDescriptors = arguments[0] || [];\n\
|
|
const __moliReferenceHandles = Array.prototype.slice.call(arguments, 1);\n\
|
|
function __deserialize(value) {{\n\
|
|
if (Array.isArray(value)) return value.map(__deserialize);\n\
|
|
if (value && typeof value === 'object') {{\n\
|
|
if (Object.prototype.hasOwnProperty.call(value, '{CLASSIC_SCRIPT_ARG_ELEMENT_REFERENCE}')) {{\n\
|
|
const index = __moliClassicReferenceIndex++;\n\
|
|
return __moliReferenceHandles[index];\n\
|
|
}}\n\
|
|
if (Object.prototype.hasOwnProperty.call(value, '{CLASSIC_SCRIPT_ARG_SHADOW_ROOT_REFERENCE}')) {{\n\
|
|
const index = __moliClassicReferenceIndex++;\n\
|
|
return __moliReferenceHandles[index];\n\
|
|
}}\n\
|
|
if (Object.prototype.hasOwnProperty.call(value, '{CLASSIC_SCRIPT_ARG_FRAME_REFERENCE}')) {{\n\
|
|
const index = __moliClassicReferenceIndex++;\n\
|
|
const frameElement = __moliReferenceHandles[index];\n\
|
|
if (!frameElement || !frameElement.contentWindow) throw new Error('{CLASSIC_SCRIPT_SENTINEL_NO_SUCH_FRAME}');\n\
|
|
return frameElement.contentWindow;\n\
|
|
}}\n\
|
|
if (Object.prototype.hasOwnProperty.call(value, '{CLASSIC_SCRIPT_ARG_WINDOW_REFERENCE}')) {{\n\
|
|
return window;\n\
|
|
}}\n\
|
|
const out = {{}};\n\
|
|
for (const key of Object.keys(value)) out[key] = __deserialize(value[key]);\n\
|
|
return out;\n\
|
|
}}\n\
|
|
return value;\n\
|
|
}}\n\
|
|
function __isElement(value) {{\n\
|
|
return typeof Element === 'function' && value instanceof Element;\n\
|
|
}}\n\
|
|
function __isShadowRoot(value) {{\n\
|
|
return typeof ShadowRoot === 'function' && value instanceof ShadowRoot;\n\
|
|
}}\n\
|
|
function __classicNodeType(value) {{\n\
|
|
try {{\n\
|
|
if (!value || typeof value !== 'object' || typeof value.nodeType !== 'number') return 0;\n\
|
|
if (typeof Node === 'function' && value instanceof Node) return value.nodeType;\n\
|
|
const tag = Object.prototype.toString.call(value);\n\
|
|
if (tag === '[object Attr]' || tag === '[object Text]' || tag === '[object CDATASection]' || tag === '[object ProcessingInstruction]' || tag === '[object Comment]' || tag === '[object Document]' || tag === '[object HTMLDocument]' || tag === '[object DocumentType]') return value.nodeType;\n\
|
|
}} catch (_) {{}}\n\
|
|
return 0;\n\
|
|
}}\n\
|
|
function __serializeNode(value) {{\n\
|
|
const nodeType = __classicNodeType(value);\n\
|
|
if (nodeType === 9) return {{ location: __serialize(value.location) }};\n\
|
|
if (nodeType) return {{}};\n\
|
|
return undefined;\n\
|
|
}}\n\
|
|
function __webReferenceForBackendNodeId(kind, backendNodeId) {{\n\
|
|
if (typeof backendNodeId !== 'number' || backendNodeId <= 0) return {{}};\n\
|
|
const out = {{\n\
|
|
'{CLASSIC_SCRIPT_WEB_REFERENCE_MARKER}': kind,\n\
|
|
'{CLASSIC_SCRIPT_WEB_REFERENCE_BACKEND_NODE_ID}': backendNodeId,\n\
|
|
}};\n\
|
|
return out;\n\
|
|
}}\n\
|
|
function __webReference(kind, node) {{\n\
|
|
const backendNodeId = __moliHostResolveBackendNodeIdForObject(node);\n\
|
|
return __webReferenceForBackendNodeId(kind, backendNodeId);\n\
|
|
}}\n\
|
|
function __isCollection(value) {{\n\
|
|
const iterator = typeof Symbol === 'function' ? Symbol.iterator : null;\n\
|
|
return !!iterator && !!value && typeof value[iterator] === 'function' && 'length' in value && typeof value.length === 'number';\n\
|
|
}}\n\
|
|
function __serializeCollection(value) {{\n\
|
|
const out = [];\n\
|
|
for (let index = 0; index < value.length; ++index) out.push(__serialize(value[index]));\n\
|
|
return out;\n\
|
|
}}\n\
|
|
function __serializeWithCycleGuard(value, callback) {{\n\
|
|
if (__moliClassicSeen.has(value)) throw new TypeError('cyclic object value');\n\
|
|
__moliClassicSeen.add(value);\n\
|
|
try {{ return callback(); }} finally {{ __moliClassicSeen.delete(value); }}\n\
|
|
}}\n\
|
|
function __serializeObjectToJson(value) {{\n\
|
|
const toJSON = value.toJSON;\n\
|
|
if (typeof toJSON !== 'function') return undefined;\n\
|
|
return __serializeWithCycleGuard(value, () => __serialize(toJSON.call(value)));\n\
|
|
}}\n\
|
|
function __isDocumentAll(value) {{\n\
|
|
try {{ return Object.prototype.toString.call(value) === '[object HTMLAllCollection]'; }} catch (_) {{ return false; }}\n\
|
|
}}\n\
|
|
function __serialize(value) {{\n\
|
|
if (value === null) return null;\n\
|
|
if (__isDocumentAll(value)) return __serializeWithCycleGuard(value, () => __serializeCollection(value));\n\
|
|
const type = typeof value;\n\
|
|
if (type === 'undefined' || type === 'function' || type === 'symbol') return null;\n\
|
|
if (type !== 'object') return value;\n\
|
|
if (value === window) {{\n\
|
|
return {{ '{CLASSIC_SCRIPT_WEB_REFERENCE_MARKER}': '{CLASSIC_SCRIPT_WEB_REFERENCE_WINDOW}' }};\n\
|
|
}}\n\
|
|
const popupId = __moliHostLightweightPopupIdForObject(value);\n\
|
|
if (popupId) return {{ '{CLASSIC_SCRIPT_WEB_REFERENCE_MARKER}': '{CLASSIC_SCRIPT_WEB_REFERENCE_POPUP_WINDOW}', '{CLASSIC_SCRIPT_WEB_REFERENCE_POPUP_ID}': String(popupId) }};\n\
|
|
const backendNodeId = __moliHostChildFrameOwnerBackendNodeIdForWindow(value);\n\
|
|
if (typeof backendNodeId === 'number') {{\n\
|
|
return __webReferenceForBackendNodeId('{CLASSIC_SCRIPT_WEB_REFERENCE_FRAME}', backendNodeId);\n\
|
|
}}\n\
|
|
if (__isShadowRoot(value)) {{\n\
|
|
if (!value.host || !value.host.isConnected) throw new Error('{CLASSIC_SCRIPT_SENTINEL_DETACHED_SHADOW_ROOT}');\n\
|
|
return __webReference('{CLASSIC_SCRIPT_WEB_REFERENCE_SHADOW_ROOT}', value);\n\
|
|
}}\n\
|
|
if (__isElement(value)) {{\n\
|
|
if (!value.isConnected) throw new Error('{CLASSIC_SCRIPT_SENTINEL_STALE_ELEMENT}');\n\
|
|
return __webReference('{CLASSIC_SCRIPT_WEB_REFERENCE_ELEMENT}', value);\n\
|
|
}}\n\
|
|
const nodeValue = __serializeNode(value);\n\
|
|
if (nodeValue !== undefined) return nodeValue;\n\
|
|
if (__moliClassicSeen.has(value)) throw new TypeError('cyclic object value');\n\
|
|
const jsonValue = __serializeObjectToJson(value);\n\
|
|
if (jsonValue !== undefined) return jsonValue;\n\
|
|
if (Array.isArray(value)) return __serializeWithCycleGuard(value, () => value.map(__serialize));\n\
|
|
if (__isCollection(value)) return __serializeWithCycleGuard(value, () => __serializeCollection(value));\n\
|
|
__moliClassicSeen.add(value);\n\
|
|
const out = {{}};\n\
|
|
try {{\n\
|
|
// WebDriver clone-an-object and ChromeDriver call_function.js enumerate inherited enumerable properties.\n\
|
|
for (const key in value) out[key] = __serialize(value[key]);\n\
|
|
return out;\n\
|
|
}} finally {{\n\
|
|
__moliClassicSeen.delete(value);\n\
|
|
}}\n\
|
|
}}\n\
|
|
let __moliClassicReferenceIndex = 0;\n\
|
|
const __moliClassicSeen = new WeakSet();\n\
|
|
const __moliArgs = __moliArgumentDescriptors.map(__deserialize);\n\
|
|
return Promise.resolve(__moliUserFunction.apply(this, __moliArgs)).then(__serialize);\n\
|
|
}}"
|
|
)
|
|
}
|
|
|
|
fn classic_script_result_value(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
value: Value,
|
|
dom_references_by_node_id: &BTreeMap<u32, ClassicScriptCanonicalNodeReference>,
|
|
frame_ids_by_owner_reference: &BTreeMap<ClassicScriptFrameOwnerReferenceKey, String>,
|
|
popup_window_handles_by_id: &BTreeMap<u64, String>,
|
|
) -> Result<Value, ClassicError> {
|
|
match value {
|
|
Value::Array(values) => values
|
|
.into_iter()
|
|
.map(|value| {
|
|
classic_script_result_value(
|
|
state,
|
|
binding,
|
|
value,
|
|
dom_references_by_node_id,
|
|
frame_ids_by_owner_reference,
|
|
popup_window_handles_by_id,
|
|
)
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map(Value::Array),
|
|
Value::Object(map) => {
|
|
if let Some(reference) = classic_script_result_reference(&map) {
|
|
return Ok(match reference {
|
|
ClassicScriptResultReference::Element {
|
|
node_id,
|
|
backend_node_id,
|
|
} => {
|
|
let reference = classic_script_canonical_dom_reference(
|
|
node_id,
|
|
backend_node_id,
|
|
dom_references_by_node_id,
|
|
);
|
|
registered_classic_element_reference_from_dom_reference(
|
|
state,
|
|
binding,
|
|
reference.node_id,
|
|
reference.reference,
|
|
)
|
|
}
|
|
ClassicScriptResultReference::ShadowRoot {
|
|
node_id,
|
|
backend_node_id,
|
|
} => {
|
|
let reference = classic_script_canonical_dom_reference(
|
|
node_id,
|
|
backend_node_id,
|
|
dom_references_by_node_id,
|
|
);
|
|
registered_classic_shadow_root_reference_from_dom_reference(
|
|
state,
|
|
binding,
|
|
reference.node_id,
|
|
reference.reference,
|
|
)
|
|
}
|
|
ClassicScriptResultReference::Frame {
|
|
owner: owner_reference,
|
|
} => {
|
|
let Some(frame_id) = frame_ids_by_owner_reference.get(&owner_reference)
|
|
else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchFrame,
|
|
"frame not found",
|
|
));
|
|
};
|
|
classic_frame_reference(frame_id.clone())
|
|
}
|
|
ClassicScriptResultReference::Window => {
|
|
if let Some(frame_id) = binding.current_frame_id.as_ref() {
|
|
classic_frame_reference(frame_id.clone())
|
|
} else {
|
|
classic_window_reference(binding.target_id.clone())
|
|
}
|
|
}
|
|
ClassicScriptResultReference::PopupWindow(popup_id) => {
|
|
let Some(handle) = popup_window_handles_by_id.get(&popup_id) else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchWindow,
|
|
"popup window handle not found",
|
|
));
|
|
};
|
|
classic_window_reference(handle.clone())
|
|
}
|
|
});
|
|
}
|
|
let mut out = Map::new();
|
|
for (key, value) in map {
|
|
out.insert(
|
|
key,
|
|
classic_script_result_value(
|
|
state,
|
|
binding,
|
|
value,
|
|
dom_references_by_node_id,
|
|
frame_ids_by_owner_reference,
|
|
popup_window_handles_by_id,
|
|
)?,
|
|
);
|
|
}
|
|
Ok(Value::Object(out))
|
|
}
|
|
_ => Ok(value),
|
|
}
|
|
}
|
|
|
|
async fn classic_webdriver_script_result_value(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
value: Value,
|
|
) -> Result<Value, ClassicError> {
|
|
let dom_references = classic_script_dom_references_by_node_id(binding, &value).await?;
|
|
let frame_ids = classic_script_frame_ids_by_owner_reference(binding, &value).await?;
|
|
let popup_window_handles_by_id =
|
|
classic_popup_window_handles_by_id_for_script_result(binding, &value).await?;
|
|
classic_script_result_value(
|
|
state,
|
|
binding,
|
|
value,
|
|
&dom_references,
|
|
&frame_ids,
|
|
&popup_window_handles_by_id,
|
|
)
|
|
}
|
|
|
|
async fn classic_script_dom_references_by_node_id(
|
|
binding: &ClassicSessionBinding,
|
|
value: &Value,
|
|
) -> Result<BTreeMap<u32, ClassicScriptCanonicalNodeReference>, ClassicError> {
|
|
let mut node_ids = Vec::new();
|
|
collect_classic_script_dom_reference_node_ids(value, &mut node_ids);
|
|
node_ids.sort_unstable();
|
|
node_ids.dedup();
|
|
|
|
let mut out = BTreeMap::new();
|
|
let context = classic_browsing_context(binding);
|
|
for node_id in node_ids {
|
|
let reference = match binding
|
|
.runtime
|
|
.execute(describe_node_command(&context, node_id, 0, false))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::DescribeNode(result)) => {
|
|
classic_script_canonical_dom_reference_from_described_node(&result.node, node_id)
|
|
}
|
|
Ok(_) => {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"script result node lookup returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => return Err(classic_error_from_devtools_error(error)),
|
|
};
|
|
out.insert(node_id, reference);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
async fn classic_script_frame_ids_by_owner_reference(
|
|
binding: &ClassicSessionBinding,
|
|
value: &Value,
|
|
) -> Result<BTreeMap<ClassicScriptFrameOwnerReferenceKey, String>, ClassicError> {
|
|
let mut owner_references = Vec::new();
|
|
collect_classic_script_frame_reference_owner_keys(value, &mut owner_references);
|
|
owner_references.sort_unstable();
|
|
owner_references.dedup();
|
|
let mut out = BTreeMap::new();
|
|
for owner_reference in owner_references {
|
|
let owner_dom_reference =
|
|
classic_script_frame_owner_dom_reference(binding, owner_reference).await?;
|
|
let frame_id = binding
|
|
.runtime
|
|
.frame_id_for_element(
|
|
binding.session_id.clone(),
|
|
binding.target_id.clone(),
|
|
binding.current_frame_id.clone(),
|
|
owner_dom_reference,
|
|
)
|
|
.await
|
|
.map_err(classic_no_such_frame_error)?;
|
|
out.insert(owner_reference, frame_id);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
async fn classic_script_frame_owner_dom_reference(
|
|
binding: &ClassicSessionBinding,
|
|
owner_reference: ClassicScriptFrameOwnerReferenceKey,
|
|
) -> Result<DevToolsDomNodeReference, ClassicError> {
|
|
if owner_reference.has_backend_node_id() {
|
|
return Ok(owner_reference.dom_reference());
|
|
}
|
|
|
|
let context = classic_browsing_context(binding);
|
|
match binding
|
|
.runtime
|
|
.execute(describe_node_command(
|
|
&context,
|
|
owner_reference.node_id(),
|
|
0,
|
|
false,
|
|
))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::DescribeNode(result)) => Ok(
|
|
classic_script_frame_owner_dom_reference_from_described_node(
|
|
owner_reference,
|
|
&result.node,
|
|
),
|
|
),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"script result frame owner lookup returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_no_such_frame_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_execute_script_exception_error(exception: DevToolsScriptException) -> ClassicError {
|
|
let text = classic_script_exception_text(&exception);
|
|
if classic_exception_text_is_webdriver_sentinel(&text, CLASSIC_SCRIPT_SENTINEL_STALE_ELEMENT) {
|
|
return ClassicError::new(
|
|
ClassicErrorCode::StaleElementReference,
|
|
"stale element reference",
|
|
);
|
|
}
|
|
if classic_exception_text_is_webdriver_sentinel(
|
|
&text,
|
|
CLASSIC_SCRIPT_SENTINEL_DETACHED_SHADOW_ROOT,
|
|
) {
|
|
return ClassicError::new(ClassicErrorCode::DetachedShadowRoot, "detached shadow root");
|
|
}
|
|
if classic_exception_text_is_webdriver_sentinel(&text, CLASSIC_SCRIPT_SENTINEL_NO_SUCH_FRAME) {
|
|
return ClassicError::new(ClassicErrorCode::NoSuchFrame, "no such frame");
|
|
}
|
|
ClassicError::new(ClassicErrorCode::JavascriptError, text)
|
|
}
|
|
|
|
fn classic_webdriver_command_exception_error(exception: DevToolsScriptException) -> ClassicError {
|
|
let text = classic_script_exception_text(&exception);
|
|
if classic_exception_text_is_webdriver_sentinel(&text, CLASSIC_SCRIPT_SENTINEL_STALE_ELEMENT)
|
|
|| classic_exception_text_is_webdriver_sentinel(&text, "stale element reference")
|
|
{
|
|
return ClassicError::new(
|
|
ClassicErrorCode::StaleElementReference,
|
|
"stale element reference",
|
|
);
|
|
}
|
|
if classic_exception_text_is_webdriver_sentinel(
|
|
&text,
|
|
CLASSIC_SCRIPT_SENTINEL_ELEMENT_NOT_INTERACTABLE,
|
|
) || classic_exception_text_is_webdriver_sentinel(&text, "element not interactable")
|
|
{
|
|
return ClassicError::new(
|
|
ClassicErrorCode::ElementNotInteractable,
|
|
"element not interactable",
|
|
);
|
|
}
|
|
ClassicError::new(ClassicErrorCode::JavascriptError, text)
|
|
}
|
|
|
|
fn classic_script_exception_text(exception: &DevToolsScriptException) -> String {
|
|
if exception.text != "Uncaught" {
|
|
return exception.text.clone();
|
|
}
|
|
if let Some(description) = exception
|
|
.value
|
|
.as_ref()
|
|
.and_then(|value| value.description.as_ref())
|
|
{
|
|
return description.clone();
|
|
}
|
|
if let Some(value) = exception
|
|
.value
|
|
.as_ref()
|
|
.and_then(|value| value.value.as_str())
|
|
{
|
|
return value.to_owned();
|
|
}
|
|
exception.text.clone()
|
|
}
|
|
|
|
fn classic_exception_text_is_webdriver_sentinel(text: &str, sentinel: &str) -> bool {
|
|
let first_line = text.lines().next().unwrap_or(text).trim();
|
|
first_line == sentinel
|
|
|| first_line.strip_prefix("Error: ") == Some(sentinel)
|
|
|| first_line.strip_prefix("Uncaught Error: ") == Some(sentinel)
|
|
|| first_line.strip_prefix("Uncaught (in promise) Error: ") == Some(sentinel)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_status(State(state): State<AppState>) -> Response {
|
|
let ready = state.classic_session_registry.lock().session_count() == 0;
|
|
classic_webdriver_json_response(StatusCode::OK, classic_status_response(ready, ""))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_new_session(
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let matched_capabilities = match matched_capabilities_from_new_session_params(¶ms) {
|
|
Ok(capabilities) => capabilities,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let page_load_strategy = match page_load_strategy_from_capabilities(&matched_capabilities) {
|
|
Ok(strategy) => strategy,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let unhandled_prompt_behavior =
|
|
match unhandled_prompt_behavior_from_capabilities(&matched_capabilities) {
|
|
Ok(behavior) => behavior,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let downloads_enabled = match downloads_enabled_from_capabilities(&matched_capabilities) {
|
|
Ok(enabled) => enabled,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let session = state
|
|
.classic_session_registry
|
|
.lock()
|
|
.create_session(page_load_strategy, unhandled_prompt_behavior.clone());
|
|
let download_directory = if downloads_enabled {
|
|
let directory = unique_download_directory(&session.session_id);
|
|
if let Err(error) = fs::create_dir_all(&directory) {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session.session_id);
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
format!("unable to create Selenium download directory: {error}"),
|
|
));
|
|
}
|
|
if !state
|
|
.classic_session_registry
|
|
.lock()
|
|
.register_download_directory(&session.session_id, directory.clone())
|
|
{
|
|
let _ = fs::remove_dir_all(&directory);
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
"unable to register Selenium download directory",
|
|
));
|
|
}
|
|
Some(directory)
|
|
} else {
|
|
None
|
|
};
|
|
let initial_cookies = state.cookie_profile.snapshot();
|
|
let initial_cookie_snapshot = initial_cookies.clone();
|
|
let initial_storage_partition = state.initial_storage_partition(initial_cookies);
|
|
let runtime = ClassicSessionRuntimeHandle::spawn(
|
|
initial_cookie_snapshot,
|
|
initial_storage_partition,
|
|
moli_core::runtime::NavigationRuntimeConfig::new(
|
|
state.fetch_config.clone(),
|
|
state.optional_resource_fetch_mask,
|
|
state.subframe_loading_enabled,
|
|
state.layout_policy,
|
|
),
|
|
);
|
|
let create_context = ClassicDevToolsCommandContext::new(session.session_id.as_str());
|
|
let target_id = match runtime
|
|
.execute(create_initial_target_command(&create_context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::CreateTarget(result)) => result.target_id.into_string(),
|
|
Ok(_) => {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session.session_id);
|
|
let _ = runtime.shutdown().await;
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
"initial target creation returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session.session_id);
|
|
let _ = runtime.shutdown().await;
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
error.message,
|
|
));
|
|
}
|
|
};
|
|
if let Err(error) = runtime.set_javascript_dialog_handler_enabled(true).await {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session.session_id);
|
|
let _ = runtime.shutdown().await;
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
error.message,
|
|
));
|
|
}
|
|
if let Some(directory) = download_directory.as_ref() {
|
|
let download_behavior =
|
|
selenium_download_behavior_command(&session.session_id, directory.as_path());
|
|
match runtime.execute(download_behavior).await {
|
|
Ok(DevToolsCommandResult::Empty) => {}
|
|
Ok(_) => {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session.session_id);
|
|
let _ = runtime.shutdown().await;
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
"Selenium download behavior returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => {
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session.session_id);
|
|
let _ = runtime.shutdown().await;
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::SessionNotCreated,
|
|
error.message,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.bind_runtime(&session.session_id, target_id, runtime);
|
|
let capabilities = webdriver_classic_capabilities(
|
|
&state,
|
|
&session.session_id,
|
|
page_load_strategy,
|
|
&unhandled_prompt_behavior,
|
|
downloads_enabled,
|
|
);
|
|
classic_webdriver_json_response(
|
|
StatusCode::OK,
|
|
new_session_response(&session.session_id, capabilities),
|
|
)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_delete_session(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let runtime = state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_session(&session_id);
|
|
if let Some(runtime) = runtime {
|
|
let cookie_commit = runtime.shutdown().await;
|
|
if let Err(error) = state.cookie_profile.commit_and_save(cookie_commit) {
|
|
warn!(?error, "failed to persist Classic cookie profile");
|
|
}
|
|
return classic_webdriver_json_response(StatusCode::OK, delete_session_response());
|
|
}
|
|
classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_upload_file(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) = classic_session_binding(&state, &session_id) {
|
|
return classic_error_into_response(error);
|
|
}
|
|
let Some(base64_zip) = params.get("file").and_then(Value::as_str) else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"missing or invalid 'file'",
|
|
));
|
|
};
|
|
let path = match uploaded_file_from_base64_zip(base64_zip, &session_id) {
|
|
Ok(path) => path,
|
|
Err(error) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
error,
|
|
));
|
|
}
|
|
};
|
|
let registered = state
|
|
.classic_session_registry
|
|
.lock()
|
|
.register_uploaded_file(&session_id, path.clone());
|
|
if !registered {
|
|
let _ = std::fs::remove_file(&path);
|
|
if let Some(parent) = path.parent() {
|
|
let _ = std::fs::remove_dir(parent);
|
|
}
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
classic_success_into_response(json!(path.to_string_lossy().to_string()))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_downloadable_files(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let directory = match classic_download_directory(&state, &session_id) {
|
|
Ok(directory) => directory,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let names = match classic_download_file_names(&directory) {
|
|
Ok(names) => names,
|
|
Err(error) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
error,
|
|
));
|
|
}
|
|
};
|
|
classic_success_into_response(json!({ "names": names }))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_download_file(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let directory = match classic_download_directory(&state, &session_id) {
|
|
Ok(directory) => directory,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let Some(name) = params.get("name").and_then(Value::as_str) else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"missing or invalid 'name'",
|
|
));
|
|
};
|
|
let name = match classic_download_file_name(name) {
|
|
Ok(name) => name,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let path = directory.join(&name);
|
|
match fs::metadata(&path) {
|
|
Ok(metadata) if metadata.is_file() => {}
|
|
_ => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"download file not found",
|
|
));
|
|
}
|
|
}
|
|
let bytes = match downloadable_file_bytes(&path) {
|
|
Ok(bytes) => bytes,
|
|
Err(error) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
error,
|
|
));
|
|
}
|
|
};
|
|
let contents = match downloadable_file_zip_base64(&name, &bytes) {
|
|
Ok(contents) => contents,
|
|
Err(error) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
error,
|
|
));
|
|
}
|
|
};
|
|
classic_success_into_response(json!({ "contents": contents }))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_delete_downloadable_files(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let directory = match classic_download_directory(&state, &session_id) {
|
|
Ok(directory) => directory,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) = classic_clear_download_directory(&directory) {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
error,
|
|
));
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_service_workers(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_session_binding(&state, &session_id) {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverClassic,
|
|
session_id: Some(DevToolsSessionId::from(binding.session_id.as_str())),
|
|
target_id: None,
|
|
browser_context_id: None,
|
|
};
|
|
let targets = match binding
|
|
.runtime
|
|
.execute(DevToolsCommand::GetTargets(DevToolsGetTargetsCommand {
|
|
context: context.clone(),
|
|
root: None,
|
|
max_depth: None,
|
|
filter: None,
|
|
}))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::GetTargets(result)) => result,
|
|
Ok(_) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"service worker target listing returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => return classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
};
|
|
let realms = match binding
|
|
.runtime
|
|
.execute(DevToolsCommand::GetRealms(DevToolsGetRealmsCommand {
|
|
context: context.clone(),
|
|
realm_type: Some("service-worker".to_owned()),
|
|
}))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::Realms(result)) => result,
|
|
Ok(_) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"service worker realm listing returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => return classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
};
|
|
let logs = match binding
|
|
.runtime
|
|
.execute(DevToolsCommand::GetServiceWorkerLogs(
|
|
DevToolsGetServiceWorkerLogsCommand {
|
|
context,
|
|
target_id: None,
|
|
},
|
|
))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::ServiceWorkerLogs(result)) => result,
|
|
Ok(_) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"service worker log listing returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => return classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
};
|
|
classic_success_into_response(classic_service_worker_observation_value(
|
|
targets, realms, logs,
|
|
))
|
|
}
|
|
|
|
fn classic_service_worker_observation_value(
|
|
targets: DevToolsGetTargetsResult,
|
|
realms: DevToolsGetRealmsResult,
|
|
logs: DevToolsGetServiceWorkerLogsResult,
|
|
) -> Value {
|
|
json!({
|
|
"targets": targets
|
|
.targets
|
|
.into_iter()
|
|
.filter(|target| target.kind == DevToolsTargetKind::ServiceWorker)
|
|
.filter_map(classic_service_worker_target_value)
|
|
.collect::<Vec<_>>(),
|
|
"realms": realms
|
|
.realms
|
|
.into_iter()
|
|
.filter(|realm| realm.context_type.as_deref() == Some("service-worker"))
|
|
.filter_map(classic_service_worker_realm_value)
|
|
.collect::<Vec<_>>(),
|
|
"logs": logs
|
|
.entries
|
|
.into_iter()
|
|
.filter_map(classic_service_worker_log_value)
|
|
.collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
|
|
fn classic_service_worker_target_value(target: DevToolsTargetInfo) -> Option<Value> {
|
|
let mut value = Map::new();
|
|
value.insert(
|
|
"targetId".to_owned(),
|
|
json!(target.target_id?.into_string()),
|
|
);
|
|
value.insert("type".to_owned(), json!("service_worker"));
|
|
value.insert("title".to_owned(), json!(target.title));
|
|
value.insert("url".to_owned(), json!(target.url));
|
|
value.insert("attached".to_owned(), json!(target.attached));
|
|
if let Some(browser_context_id) = target.browser_context_id {
|
|
value.insert(
|
|
"browserContextId".to_owned(),
|
|
json!(browser_context_id.into_string()),
|
|
);
|
|
}
|
|
Some(Value::Object(value))
|
|
}
|
|
|
|
fn classic_service_worker_realm_value(realm: RuntimeExecutionContextEvent) -> Option<Value> {
|
|
let mut value = Map::new();
|
|
value.insert("realm".to_owned(), json!(realm.realm_id?.into_string()));
|
|
value.insert("targetId".to_owned(), json!(realm.target_id?.into_string()));
|
|
value.insert("type".to_owned(), json!("service-worker"));
|
|
if let Some(context_id) = realm.context_id {
|
|
value.insert("executionContextId".to_owned(), json!(context_id));
|
|
}
|
|
if let Some(origin) = realm.origin {
|
|
value.insert("origin".to_owned(), json!(origin));
|
|
}
|
|
Some(Value::Object(value))
|
|
}
|
|
|
|
fn classic_service_worker_log_value(entry: RuntimeConsoleEvent) -> Option<Value> {
|
|
let mut value = Map::new();
|
|
value.insert("targetId".to_owned(), json!(entry.target_id?.into_string()));
|
|
value.insert("type".to_owned(), json!(entry.console_type));
|
|
value.insert("text".to_owned(), json!(entry.text));
|
|
value.insert("args".to_owned(), json!(entry.args));
|
|
if let Some(stack) = entry.stack {
|
|
value.insert("stack".to_owned(), json!(stack));
|
|
}
|
|
if let Some(context_id) = entry.execution_context_id {
|
|
value.insert("executionContextId".to_owned(), json!(context_id));
|
|
}
|
|
if let Some(timestamp) = entry.timestamp {
|
|
value.insert("timestamp".to_owned(), json!(timestamp));
|
|
}
|
|
Some(Value::Object(value))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_navigate(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
let command = match navigate_command(
|
|
&context,
|
|
¶ms,
|
|
binding.page_load_strategy.navigation_wait(),
|
|
) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
match webdriver_classic_execute_page_load_command(&binding, command).await {
|
|
Ok(DevToolsCommandResult::Navigate(_)) => {
|
|
classic_note_top_level_document_change(&state, &session_id, &binding.target_id);
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"navigation returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(error),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_url(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
match binding.runtime.execute(current_url_command(&context)).await {
|
|
Ok(DevToolsCommandResult::GetTargets(result)) => {
|
|
let Some(target) = result.targets.into_iter().next() else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::NoSuchWindow,
|
|
"current window not found",
|
|
));
|
|
};
|
|
classic_success_into_response(json!(target.url))
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"current URL returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_title(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
webdriver_classic_evaluate_string_command(session_id, state, title_command, "title").await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_timeouts(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let timeouts = match state.classic_session_registry.lock().timeouts(&session_id) {
|
|
Some(timeouts) => timeouts,
|
|
None => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
};
|
|
classic_success_into_response(timeouts_value(timeouts))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_set_timeouts(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
if body.is_empty() {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"timeouts must be an object",
|
|
));
|
|
}
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let current = match state.classic_session_registry.lock().timeouts(&session_id) {
|
|
Some(timeouts) => timeouts,
|
|
None => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
};
|
|
let timeouts = match parse_timeouts(¶ms, current) {
|
|
Ok(timeouts) => timeouts,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if !state
|
|
.classic_session_registry
|
|
.lock()
|
|
.set_timeouts(&session_id, timeouts)
|
|
{
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_source(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
match binding.runtime.execute(page_source_command(&context)).await {
|
|
Ok(DevToolsCommandResult::GetOuterHtml(result)) => {
|
|
classic_success_into_response(json!(result.outer_html))
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"source returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_take_screenshot(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
match binding.runtime.execute(screenshot_command(&context)).await {
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"screenshot returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_print_page(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
let command = match print_page_command(&context, ¶ms) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
match binding.runtime.execute(command).await {
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"print returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_evaluate_string_command(
|
|
session_id: String,
|
|
state: AppState,
|
|
build_command: impl FnOnce(&ClassicDevToolsCommandContext) -> DevToolsCommand,
|
|
label: &'static str,
|
|
) -> Response {
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
match binding
|
|
.runtime
|
|
.execute_with_pending_navigation_wait(
|
|
build_command(&context),
|
|
None,
|
|
binding.timeouts.page_load.map(Duration::from_millis),
|
|
)
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
if !value.value.is_string() {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} returned a non-string value"),
|
|
));
|
|
}
|
|
if label == "title" {
|
|
let title = value.value.as_str().unwrap_or_default();
|
|
return classic_success_into_response(json!(classic_webdriver_title(title)));
|
|
}
|
|
classic_success_into_response(value.value)
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} returned an unexpected result"),
|
|
)),
|
|
Err(error) if label == "title" && error.message == "NoDocumentLoaded" => {
|
|
classic_success_into_response(json!(""))
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_webdriver_title(title: &str) -> String {
|
|
title.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
}
|
|
|
|
async fn classic_current_url(binding: &ClassicSessionBinding) -> Result<String, ClassicError> {
|
|
let context = classic_top_level_context(binding);
|
|
match binding.runtime.execute(current_url_command(&context)).await {
|
|
Ok(DevToolsCommandResult::GetTargets(result)) => {
|
|
let Some(target) = result.targets.into_iter().next() else {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::NoSuchWindow,
|
|
"current window not found",
|
|
));
|
|
};
|
|
Ok(target.url)
|
|
}
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"current URL returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_switch_frame(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let Some(frame) = params.get("id") else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"id must be null, a non-negative integer, or a web element",
|
|
));
|
|
};
|
|
if frame.is_null() {
|
|
let binding = match classic_top_level_browsing_context_binding_without_prompt_handling(
|
|
&state,
|
|
&session_id,
|
|
)
|
|
.await
|
|
{
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) = ensure_classic_top_level_browsing_context_exists(&binding).await {
|
|
return classic_error_into_response(error);
|
|
}
|
|
if !state
|
|
.classic_session_registry
|
|
.lock()
|
|
.set_current_frame_id(&session_id, None)
|
|
{
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
return classic_success_into_response(Value::Null);
|
|
}
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
|
|
let frame_id = if let Some(index) = classic_frame_index(frame) {
|
|
// Non-null frame switches resolve relative to the current browsing context.
|
|
// `id: null` intentionally checks only the top-level context above.
|
|
if let Err(error) = ensure_classic_current_browsing_context_exists(&binding).await {
|
|
return classic_error_into_response(error);
|
|
}
|
|
let frame_id = match binding
|
|
.runtime
|
|
.frame_id_for_index(
|
|
binding.session_id.clone(),
|
|
binding.target_id.clone(),
|
|
binding.current_frame_id.clone(),
|
|
index,
|
|
)
|
|
.await
|
|
{
|
|
Ok(frame_id) => frame_id,
|
|
Err(error) => return classic_error_into_response(classic_no_such_frame_error(error)),
|
|
};
|
|
Some(frame_id)
|
|
} else if let Some(element_id) = classic_frame_element_id(frame) {
|
|
// Element-based frame switches also need the current context before
|
|
// resolving the reference against it.
|
|
if let Err(error) = ensure_classic_current_browsing_context_exists(&binding).await {
|
|
return classic_error_into_response(error);
|
|
}
|
|
let reference = match resolve_classic_element_dom_reference(&state, &binding, element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let frame_id = match binding
|
|
.runtime
|
|
.frame_id_for_element(
|
|
binding.session_id.clone(),
|
|
binding.target_id.clone(),
|
|
binding.current_frame_id.clone(),
|
|
reference,
|
|
)
|
|
.await
|
|
{
|
|
Ok(frame_id) => frame_id,
|
|
Err(error) => return classic_error_into_response(classic_no_such_frame_error(error)),
|
|
};
|
|
Some(frame_id)
|
|
} else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"id must be null, a non-negative integer, or a web element",
|
|
));
|
|
};
|
|
|
|
if let Some(frame_id) = frame_id.as_ref()
|
|
&& let Err(error) = ensure_classic_frame_switch_target_ready(&binding, frame_id).await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
|
|
if !state
|
|
.classic_session_registry
|
|
.lock()
|
|
.set_current_frame_id(&session_id, frame_id)
|
|
{
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_switch_parent_frame(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) = ensure_classic_top_level_browsing_context_exists(&binding).await {
|
|
return classic_error_into_response(error);
|
|
}
|
|
let frame_id = match binding.current_frame_id.as_deref() {
|
|
Some(current_frame_id) => match binding
|
|
.runtime
|
|
.parent_frame_id(
|
|
binding.session_id.clone(),
|
|
binding.target_id.clone(),
|
|
current_frame_id.to_owned(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(frame_id) => frame_id,
|
|
Err(error) if error.kind == DevToolsErrorKind::NoSuchTarget => None,
|
|
Err(error) => return classic_error_into_response(classic_no_such_frame_error(error)),
|
|
},
|
|
None => None,
|
|
};
|
|
if !state
|
|
.classic_session_registry
|
|
.lock()
|
|
.set_current_frame_id(&session_id, frame_id)
|
|
{
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::InvalidSessionId,
|
|
"session not found",
|
|
));
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
fn classic_frame_index(value: &Value) -> Option<usize> {
|
|
value
|
|
.as_u64()
|
|
.filter(|index| *index <= u16::MAX as u64)
|
|
.and_then(|index| usize::try_from(index).ok())
|
|
}
|
|
|
|
fn classic_frame_element_id(value: &Value) -> Option<&str> {
|
|
value
|
|
.as_object()
|
|
.and_then(|object| object.get(CLASSIC_ELEMENT_REFERENCE_KEY))
|
|
.and_then(Value::as_str)
|
|
}
|
|
|
|
fn classic_no_such_frame_error(error: DevToolsError) -> ClassicError {
|
|
match error.kind {
|
|
DevToolsErrorKind::NoSuchTarget | DevToolsErrorKind::NoSuchNode => {
|
|
ClassicError::new(ClassicErrorCode::NoSuchFrame, error.message)
|
|
}
|
|
_ => classic_error_from_devtools_error(error),
|
|
}
|
|
}
|
|
|
|
async fn classic_popup_window_handles_by_id_for_script_result(
|
|
binding: &ClassicSessionBinding,
|
|
value: &Value,
|
|
) -> Result<BTreeMap<u64, String>, ClassicError> {
|
|
if !classic_script_result_contains_popup_window_reference(value) {
|
|
return Ok(BTreeMap::new());
|
|
}
|
|
let context = classic_top_level_context(binding);
|
|
match binding
|
|
.runtime
|
|
.execute(window_handles_command(&context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::GetTargets(result)) => Ok(result
|
|
.targets
|
|
.into_iter()
|
|
.filter(|target| {
|
|
target.kind == moli_protocol::devtools_runtime::DevToolsTargetKind::Page
|
|
})
|
|
.filter_map(|target| {
|
|
let popup_id = target.moli_popup_id?;
|
|
let target_id = target.target_id?.into_string();
|
|
Some((popup_id, target_id))
|
|
})
|
|
.collect()),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"window handles returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_refresh(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
match webdriver_classic_execute_page_load_command(
|
|
&binding,
|
|
refresh_command(&context, binding.page_load_strategy.navigation_wait()),
|
|
)
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::Navigate(_)) => {
|
|
classic_note_top_level_document_change(&state, &session_id, &binding.target_id);
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"refresh returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(error),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_back(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
webdriver_classic_traverse_history(session_id, state, -1).await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_forward(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
webdriver_classic_traverse_history(session_id, state, 1).await
|
|
}
|
|
|
|
async fn webdriver_classic_traverse_history(
|
|
session_id: String,
|
|
state: AppState,
|
|
delta: i32,
|
|
) -> Response {
|
|
let binding = match classic_top_level_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
let history = match binding
|
|
.runtime
|
|
.execute(navigation_history_command(&context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::GetNavigationHistory(history)) => history,
|
|
Ok(_) => {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"navigation history returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => return classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
};
|
|
let Some((entry_id, url)) = history_traversal_entry(&history, delta) else {
|
|
return classic_success_into_response(Value::Null);
|
|
};
|
|
match webdriver_classic_execute_page_load_command(
|
|
&binding,
|
|
traverse_history_command(
|
|
&context,
|
|
entry_id,
|
|
url,
|
|
binding.page_load_strategy.navigation_wait(),
|
|
),
|
|
)
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::TraverseHistory(result)) => {
|
|
if !result.same_document {
|
|
classic_note_top_level_document_change(&state, &session_id, &binding.target_id);
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
Ok(DevToolsCommandResult::Empty) => classic_success_into_response(Value::Null),
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"history traversal returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(error),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_execute_page_load_command(
|
|
binding: &ClassicSessionBinding,
|
|
command: DevToolsCommand,
|
|
) -> Result<DevToolsCommandResult, ClassicError> {
|
|
match binding
|
|
.runtime
|
|
.execute_inner(
|
|
command,
|
|
binding.timeouts.page_load.map(Duration::from_millis),
|
|
)
|
|
.await
|
|
{
|
|
Ok(result) => Ok(result),
|
|
Err(error) if error.kind == DevToolsErrorKind::Timeout => Err(ClassicError::new(
|
|
ClassicErrorCode::Timeout,
|
|
"page load timed out",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_execute_sync(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let mut command = match execute_sync_command(&context, ¶ms) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let script_argument_handles = match prepare_classic_script_argument_handles(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
¶ms,
|
|
"execute sync script argument",
|
|
)
|
|
.await
|
|
{
|
|
Ok(handles) => handles,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
apply_classic_script_argument_handles(&mut command, &script_argument_handles);
|
|
let result = binding
|
|
.runtime
|
|
.execute_script(command, binding.timeouts.script.map(Duration::from_millis))
|
|
.await;
|
|
release_classic_remote_objects(
|
|
&binding,
|
|
&context,
|
|
script_argument_handles.remote_handles,
|
|
"execute sync script argument",
|
|
)
|
|
.await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
match classic_webdriver_script_result_value(&state, &binding, value.value).await {
|
|
Ok(value) => classic_success_into_response(value),
|
|
Err(error) => classic_error_into_response(error),
|
|
}
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_execute_script_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"execute sync returned an unexpected result",
|
|
)),
|
|
Err(error)
|
|
if error.kind == DevToolsErrorKind::Timeout
|
|
|| error.message == "MissingDevToolsCommandResult" =>
|
|
{
|
|
classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::ScriptTimeout,
|
|
error.message,
|
|
))
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_execute_async(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let mut command = match execute_async_command(&context, ¶ms) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let script_argument_handles = match prepare_classic_script_argument_handles(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
¶ms,
|
|
"execute async script argument",
|
|
)
|
|
.await
|
|
{
|
|
Ok(handles) => handles,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
apply_classic_script_argument_handles(&mut command, &script_argument_handles);
|
|
let result = binding
|
|
.runtime
|
|
.execute_script(command, binding.timeouts.script.map(Duration::from_millis))
|
|
.await;
|
|
release_classic_remote_objects(
|
|
&binding,
|
|
&context,
|
|
script_argument_handles.remote_handles,
|
|
"execute async script argument",
|
|
)
|
|
.await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
match classic_webdriver_script_result_value(&state, &binding, value.value).await {
|
|
Ok(value) => classic_success_into_response(value),
|
|
Err(error) => classic_error_into_response(error),
|
|
}
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_execute_script_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"execute async returned an unexpected result",
|
|
)),
|
|
Err(error)
|
|
if error.kind == DevToolsErrorKind::Timeout
|
|
|| error.message == "MissingDevToolsCommandResult" =>
|
|
{
|
|
classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::ScriptTimeout,
|
|
error.message,
|
|
))
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_element(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
webdriver_classic_find_element_with_mode(session_id, state, body, false).await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_elements(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
webdriver_classic_find_element_with_mode(session_id, state, body, true).await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_child_element(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
webdriver_classic_find_element_with_mode_and_root(
|
|
session_id,
|
|
state,
|
|
body,
|
|
false,
|
|
Some(element_id),
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_child_elements(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
webdriver_classic_find_element_with_mode_and_root(
|
|
session_id,
|
|
state,
|
|
body,
|
|
true,
|
|
Some(element_id),
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_shadow_root(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let reference = match resolve_classic_element_dom_reference(&state, &binding, &element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
|
|
if let Err(error) =
|
|
verify_classic_element_attached(&state, &binding, &context, &element_id).await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
|
|
let result = binding
|
|
.runtime
|
|
.execute(describe_node_reference_command(
|
|
&context, reference, 1, true,
|
|
))
|
|
.await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::DescribeNode(result)) => {
|
|
let Some((node_id, reference)) =
|
|
classic_author_shadow_root_reference_from_described_node(&result.node)
|
|
else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::NoSuchShadowRoot,
|
|
"shadow root not found",
|
|
));
|
|
};
|
|
classic_success_into_response(
|
|
registered_classic_shadow_root_reference_from_dom_reference(
|
|
&state, &binding, node_id, reference,
|
|
),
|
|
)
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element shadow root returned an unexpected result",
|
|
)),
|
|
Err(error) if error.kind == DevToolsErrorKind::NoSuchNode => {
|
|
classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::StaleElementReference,
|
|
"element is no longer attached to the DOM",
|
|
))
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn verify_classic_element_attached(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
) -> Result<(), ClassicError> {
|
|
let object_id = resolve_classic_remote_object(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
"webdriver-classic-shadow-host",
|
|
"get element shadow root",
|
|
)
|
|
.await?;
|
|
let result = binding
|
|
.runtime
|
|
.execute(verify_element_attached_command(context, object_id.clone()))
|
|
.await;
|
|
release_classic_remote_object(binding, context, object_id, "get element shadow root").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(_) => Ok(()),
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
Err(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element shadow root attachment check returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_author_shadow_root_reference_from_described_node(
|
|
node: &Value,
|
|
) -> Option<(u32, DevToolsDomNodeReference)> {
|
|
let shadow_root = node
|
|
.get("shadowRoots")?
|
|
.as_array()?
|
|
.iter()
|
|
.find(|shadow_root| {
|
|
matches!(
|
|
shadow_root.get("shadowRootType").and_then(Value::as_str),
|
|
Some("open" | "closed")
|
|
)
|
|
})?;
|
|
if let Some(backend_node_id) = shadow_root
|
|
.get("backendNodeId")
|
|
.and_then(Value::as_u64)
|
|
.and_then(|id| u32::try_from(id).ok())
|
|
.filter(|id| *id > 0)
|
|
{
|
|
return Some((
|
|
backend_node_id,
|
|
DevToolsDomNodeReference::BackendNodeId(backend_node_id),
|
|
));
|
|
}
|
|
let node_id = shadow_root
|
|
.get("nodeId")?
|
|
.as_u64()
|
|
.and_then(|id| u32::try_from(id).ok())
|
|
.filter(|id| *id > 0)?;
|
|
Some((node_id, DevToolsDomNodeReference::FrontendNodeId(node_id)))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_shadow_element(
|
|
Path((session_id, shadow_root_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
webdriver_classic_find_shadow_element_with_mode(session_id, shadow_root_id, state, body, false)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_shadow_elements(
|
|
Path((session_id, shadow_root_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
webdriver_classic_find_shadow_element_with_mode(session_id, shadow_root_id, state, body, true)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_find_element_with_mode(
|
|
session_id: String,
|
|
state: AppState,
|
|
body: Bytes,
|
|
multiple: bool,
|
|
) -> Response {
|
|
webdriver_classic_find_element_with_mode_and_root(session_id, state, body, multiple, None).await
|
|
}
|
|
|
|
async fn webdriver_classic_find_shadow_element_with_mode(
|
|
session_id: String,
|
|
shadow_root_id: String,
|
|
state: AppState,
|
|
body: Bytes,
|
|
multiple: bool,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let shadow_root_reference =
|
|
match resolve_classic_shadow_root_dom_reference(&state, &binding, &shadow_root_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) =
|
|
verify_classic_shadow_root_attached(&binding, &context, shadow_root_reference.clone()).await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
let root = Some(shadow_root_reference);
|
|
let command = match find_element_command_with_root(&context, ¶ms, multiple, root) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
match webdriver_classic_execute_find_element_command(&binding, command, multiple).await {
|
|
Ok(result) => {
|
|
webdriver_classic_find_element_result_response(
|
|
&state,
|
|
&binding,
|
|
result,
|
|
multiple,
|
|
"find shadow-root element",
|
|
)
|
|
.await
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_find_element_with_mode_and_root(
|
|
session_id: String,
|
|
state: AppState,
|
|
body: Bytes,
|
|
multiple: bool,
|
|
root_element_id: Option<String>,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let command = if let Some(root_element_id) = root_element_id {
|
|
let root = match resolve_classic_element_dom_reference(&state, &binding, &root_element_id) {
|
|
Ok(reference) => Some(reference),
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
match find_element_command_with_root(&context, ¶ms, multiple, root) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
}
|
|
} else {
|
|
match find_element_command(&context, ¶ms, multiple) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
}
|
|
};
|
|
match webdriver_classic_execute_find_element_command(&binding, command, multiple).await {
|
|
Ok(result) => {
|
|
webdriver_classic_find_element_result_response(
|
|
&state,
|
|
&binding,
|
|
result,
|
|
multiple,
|
|
"find element",
|
|
)
|
|
.await
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn verify_classic_shadow_root_attached(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
shadow_root_reference: DevToolsDomNodeReference,
|
|
) -> Result<(), ClassicError> {
|
|
let object_id = resolve_classic_shadow_root_remote_object_from_reference(
|
|
binding,
|
|
context,
|
|
shadow_root_reference,
|
|
None,
|
|
"webdriver-classic-shadow-root",
|
|
"find shadow-root element",
|
|
)
|
|
.await?;
|
|
let result = binding
|
|
.runtime
|
|
.execute(shadow_root_attached_command(context, object_id.clone()))
|
|
.await;
|
|
release_classic_remote_object(binding, context, object_id, "find shadow-root element").await;
|
|
verify_classic_shadow_root_attached_result(result, "find shadow-root element")
|
|
}
|
|
|
|
async fn verify_classic_shadow_root_remote_object_attached(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
object_id: &str,
|
|
label: &str,
|
|
) -> Result<(), ClassicError> {
|
|
let result = binding
|
|
.runtime
|
|
.execute(shadow_root_attached_command(context, object_id.to_owned()))
|
|
.await;
|
|
verify_classic_shadow_root_attached_result(result, label)
|
|
}
|
|
|
|
fn verify_classic_shadow_root_attached_result(
|
|
result: Result<DevToolsCommandResult, DevToolsError>,
|
|
label: &str,
|
|
) -> Result<(), ClassicError> {
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) if value.value.as_bool().unwrap_or(false) => Ok(()),
|
|
DevToolsScriptResult::Value(_) | DevToolsScriptResult::Exception(_) => {
|
|
Err(ClassicError::new(
|
|
ClassicErrorCode::DetachedShadowRoot,
|
|
"shadow root is detached",
|
|
))
|
|
}
|
|
},
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} attachment check returned an unexpected result"),
|
|
)),
|
|
Err(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::DetachedShadowRoot,
|
|
"shadow root is detached",
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_execute_find_element_command(
|
|
binding: &ClassicSessionBinding,
|
|
command: DevToolsCommand,
|
|
multiple: bool,
|
|
) -> Result<DevToolsCommandResult, DevToolsError> {
|
|
let implicit_wait = Duration::from_millis(binding.timeouts.implicit.unwrap_or(0));
|
|
let started = tokio::time::Instant::now();
|
|
loop {
|
|
match binding.runtime.execute(command.clone()).await {
|
|
Ok(DevToolsCommandResult::QuerySelector(result)) => {
|
|
if !result.node_ids.is_empty()
|
|
|| implicit_wait.is_zero()
|
|
|| started.elapsed() >= implicit_wait
|
|
{
|
|
return Ok(DevToolsCommandResult::QuerySelector(result));
|
|
}
|
|
}
|
|
Ok(DevToolsCommandResult::LocateNodes(result)) => {
|
|
if !result.node_ids.is_empty()
|
|
|| implicit_wait.is_zero()
|
|
|| started.elapsed() >= implicit_wait
|
|
{
|
|
return Ok(DevToolsCommandResult::LocateNodes(result));
|
|
}
|
|
}
|
|
Ok(result) => return Ok(result),
|
|
Err(error)
|
|
if classic_find_element_should_retry_error(&error)
|
|
&& !implicit_wait.is_zero()
|
|
&& started.elapsed() < implicit_wait => {}
|
|
Err(error) => return Err(error),
|
|
}
|
|
|
|
let remaining = implicit_wait.saturating_sub(started.elapsed());
|
|
if remaining.is_zero() {
|
|
return Ok(empty_classic_find_result_for_command(&command, multiple));
|
|
}
|
|
sleep(remaining.min(CLASSIC_IMPLICIT_WAIT_POLL_INTERVAL)).await;
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_find_element_result_response(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
result: DevToolsCommandResult,
|
|
multiple: bool,
|
|
label: &str,
|
|
) -> Response {
|
|
match result {
|
|
DevToolsCommandResult::QuerySelector(result) if multiple => {
|
|
let references = match classic_find_canonical_dom_references_by_frontend_node_id(
|
|
binding,
|
|
result.node_ids,
|
|
label,
|
|
)
|
|
.await
|
|
{
|
|
Ok(references) => references,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
classic_success_into_response(json!(
|
|
registered_classic_element_references_from_canonical_references(
|
|
state, binding, references
|
|
)
|
|
))
|
|
}
|
|
DevToolsCommandResult::LocateNodes(result) if multiple => {
|
|
let references = match classic_find_canonical_dom_references_by_frontend_node_id(
|
|
binding,
|
|
result.node_ids,
|
|
label,
|
|
)
|
|
.await
|
|
{
|
|
Ok(references) => references,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
classic_success_into_response(json!(
|
|
registered_classic_element_references_from_canonical_references(
|
|
state, binding, references
|
|
)
|
|
))
|
|
}
|
|
DevToolsCommandResult::QuerySelector(result) => {
|
|
let Some(frontend_node_id) = result.node_ids.first().copied() else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::NoSuchElement,
|
|
"element not found",
|
|
));
|
|
};
|
|
let reference = match classic_find_canonical_dom_reference_for_frontend_node_id(
|
|
binding,
|
|
frontend_node_id,
|
|
label,
|
|
)
|
|
.await
|
|
{
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
classic_success_into_response(registered_classic_element_reference_from_dom_reference(
|
|
state,
|
|
binding,
|
|
reference.node_id,
|
|
reference.reference,
|
|
))
|
|
}
|
|
DevToolsCommandResult::LocateNodes(result) => {
|
|
let Some(frontend_node_id) = result.node_ids.first().copied() else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::NoSuchElement,
|
|
"element not found",
|
|
));
|
|
};
|
|
let reference = match classic_find_canonical_dom_reference_for_frontend_node_id(
|
|
binding,
|
|
frontend_node_id,
|
|
label,
|
|
)
|
|
.await
|
|
{
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
classic_success_into_response(registered_classic_element_reference_from_dom_reference(
|
|
state,
|
|
binding,
|
|
reference.node_id,
|
|
reference.reference,
|
|
))
|
|
}
|
|
_ => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} returned an unexpected result"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn classic_find_canonical_dom_references_by_frontend_node_id(
|
|
binding: &ClassicSessionBinding,
|
|
frontend_node_ids: Vec<u32>,
|
|
label: &str,
|
|
) -> Result<Vec<ClassicScriptCanonicalNodeReference>, ClassicError> {
|
|
let mut cache: BTreeMap<u32, ClassicScriptCanonicalNodeReference> = BTreeMap::new();
|
|
let mut out = Vec::with_capacity(frontend_node_ids.len());
|
|
for frontend_node_id in frontend_node_ids {
|
|
let reference = if let Some(reference) = cache.get(&frontend_node_id) {
|
|
reference.clone()
|
|
} else {
|
|
let reference = classic_find_canonical_dom_reference_for_frontend_node_id(
|
|
binding,
|
|
frontend_node_id,
|
|
label,
|
|
)
|
|
.await?;
|
|
cache.insert(frontend_node_id, reference.clone());
|
|
reference
|
|
};
|
|
out.push(reference);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
async fn classic_find_canonical_dom_reference_for_frontend_node_id(
|
|
binding: &ClassicSessionBinding,
|
|
frontend_node_id: u32,
|
|
label: &str,
|
|
) -> Result<ClassicScriptCanonicalNodeReference, ClassicError> {
|
|
let context = classic_browsing_context(binding);
|
|
match binding
|
|
.runtime
|
|
.execute(describe_node_command(&context, frontend_node_id, 0, false))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::DescribeNode(result)) => {
|
|
Ok(classic_script_canonical_dom_reference_from_described_node(
|
|
&result.node,
|
|
frontend_node_id,
|
|
))
|
|
}
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} node lookup returned an unexpected result"),
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn empty_classic_find_result_for_command(
|
|
command: &DevToolsCommand,
|
|
multiple: bool,
|
|
) -> DevToolsCommandResult {
|
|
match command {
|
|
DevToolsCommand::LocateNodes(_) => {
|
|
DevToolsCommandResult::LocateNodes(DevToolsLocateNodesResult {
|
|
nodes: Vec::new(),
|
|
node_ids: Vec::new(),
|
|
})
|
|
}
|
|
_ => DevToolsCommandResult::QuerySelector(DevToolsQuerySelectorResult {
|
|
node_ids: Vec::new(),
|
|
multiple,
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn classic_find_element_should_retry_error(error: &DevToolsError) -> bool {
|
|
matches!(error.kind, DevToolsErrorKind::NavigationChangingDocument)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_attribute(
|
|
Path((session_id, element_id, name)): Path<(String, String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let reference = match resolve_classic_element_dom_reference(&state, &binding, &element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let command = get_element_attributes_reference_command(&context, reference);
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::GetAttributes(result)) => {
|
|
classic_success_into_response(json!(classic_attribute_value(result, &name)))
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element attribute returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_text(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"webdriver-classic-text",
|
|
"get element text",
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) if classic_text_should_fallback_to_dom_text(&error) => {
|
|
return webdriver_classic_get_element_text_from_dom(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
)
|
|
.await;
|
|
}
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let result = binding
|
|
.runtime
|
|
.execute(get_element_rendered_text_command(
|
|
&context,
|
|
object_id.clone(),
|
|
))
|
|
.await;
|
|
release_classic_remote_object(&binding, &context, object_id, "get element text").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
classic_success_into_response(json!(value.value.as_str().unwrap_or_default()))
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element text returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_get_element_text_from_dom(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
) -> Response {
|
|
let reference = match resolve_classic_element_dom_reference(state, binding, element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let command = get_element_text_reference_command(context, reference);
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::GetText(result)) => {
|
|
classic_success_into_response(json!(classic_text_value(result)))
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element text returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_text_should_fallback_to_dom_text(error: &ClassicError) -> bool {
|
|
matches!(
|
|
error.code,
|
|
ClassicErrorCode::NoSuchElement | ClassicErrorCode::StaleElementReference
|
|
)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_tag_name(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let reference = match resolve_classic_element_dom_reference(&state, &binding, &element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let command = get_element_property_reference_command(&context, reference, "localName");
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::GetProperty(result)) => {
|
|
classic_success_into_response(classic_property_value(result))
|
|
}
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element tag name returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_active_element(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
match binding
|
|
.runtime
|
|
.execute(active_element_command(&context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
let Some(node_id) = value.node_id else {
|
|
return classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"active element returned a non-node value",
|
|
));
|
|
};
|
|
let reference = value
|
|
.backend_node_id
|
|
.map(DevToolsDomNodeReference::BackendNodeId)
|
|
.unwrap_or(DevToolsDomNodeReference::FrontendNodeId(node_id));
|
|
let owner_node_id = value.backend_node_id.unwrap_or(node_id);
|
|
release_classic_remote_handle(&binding, &context, &value, "active element").await;
|
|
classic_success_into_response(
|
|
registered_classic_element_reference_from_dom_reference(
|
|
&state,
|
|
&binding,
|
|
owner_node_id,
|
|
reference,
|
|
),
|
|
)
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"active element returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_element_equals(
|
|
Path((session_id, element_id, other_element_id)): Path<(String, String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let element_identity =
|
|
match classic_element_live_identity(&state, &binding, &context, &element_id).await {
|
|
Ok(identity) => identity,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let other_element_identity =
|
|
match classic_element_live_identity(&state, &binding, &context, &other_element_id).await {
|
|
Ok(identity) => identity,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
classic_success_into_response(json!(element_identity == other_element_identity))
|
|
}
|
|
|
|
async fn classic_element_live_identity(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
) -> Result<ClassicElementLiveIdentity, ClassicError> {
|
|
let reference = resolve_classic_element_dom_reference(state, binding, element_id)?;
|
|
let command = describe_node_reference_command(context, reference, 0, false);
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::DescribeNode(result)) => {
|
|
classic_element_live_identity_from_described_node(&result.node).ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::NoSuchElement,
|
|
"element no longer describes a live DOM node",
|
|
)
|
|
})
|
|
}
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element equality returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_element_live_identity_from_described_node(
|
|
node: &Value,
|
|
) -> Option<ClassicElementLiveIdentity> {
|
|
node.get("backendNodeId")
|
|
.and_then(Value::as_u64)
|
|
.and_then(|id| u32::try_from(id).ok())
|
|
.filter(|id| *id > 0)
|
|
.map(ClassicElementLiveIdentity::BackendNodeId)
|
|
.or_else(|| {
|
|
node.get("nodeId")
|
|
.and_then(Value::as_u64)
|
|
.and_then(|id| u32::try_from(id).ok())
|
|
.filter(|id| *id > 0)
|
|
.map(ClassicElementLiveIdentity::FrontendNodeId)
|
|
})
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_is_element_enabled(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"webdriver-classic-enabled",
|
|
"is element enabled",
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let result = binding
|
|
.runtime
|
|
.execute(get_element_enabled_command(&context, object_id.clone()))
|
|
.await;
|
|
release_classic_remote_object(&binding, &context, object_id, "is element enabled").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
classic_success_into_response(json!(value.value.as_bool().unwrap_or(false)))
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"is element enabled returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_is_element_selected(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let tag_name = match webdriver_classic_element_property_value(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"localName",
|
|
"is element selected",
|
|
)
|
|
.await
|
|
{
|
|
Ok(value) => value.as_str().unwrap_or_default().to_owned(),
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let selected = match tag_name.as_str() {
|
|
"option" => match webdriver_classic_element_property_value(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"selected",
|
|
"is element selected",
|
|
)
|
|
.await
|
|
{
|
|
Ok(value) => value.as_bool().unwrap_or(false),
|
|
Err(error) => return classic_error_into_response(error),
|
|
},
|
|
"input" => {
|
|
let input_type = match webdriver_classic_element_property_value(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"type",
|
|
"is element selected",
|
|
)
|
|
.await
|
|
{
|
|
Ok(value) => value.as_str().unwrap_or_default().to_ascii_lowercase(),
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if input_type == "checkbox" || input_type == "radio" {
|
|
match webdriver_classic_element_property_value(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"checked",
|
|
"is element selected",
|
|
)
|
|
.await
|
|
{
|
|
Ok(value) => value.as_bool().unwrap_or(false),
|
|
Err(error) => return classic_error_into_response(error),
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
_ => false,
|
|
};
|
|
classic_success_into_response(json!(selected))
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_rect(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let reference = match resolve_classic_element_dom_reference(&state, &binding, &element_id) {
|
|
Ok(reference) => reference,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let command = get_element_rect_reference_command(&context, reference);
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::DomGeometry(result)) => match classic_rect_from_geometry(&result)
|
|
{
|
|
Ok(rect) => classic_success_into_response(rect),
|
|
Err(error) => classic_error_into_response(error),
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element rect returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_take_element_screenshot(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"webdriver-classic-element-screenshot",
|
|
"element screenshot",
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let result = binding
|
|
.runtime
|
|
.execute(element_screenshot_command(&context, object_id.clone()))
|
|
.await;
|
|
release_classic_remote_object(&binding, &context, object_id, "element screenshot").await;
|
|
match result {
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element screenshot returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_css_value(
|
|
Path((session_id, element_id, property_name)): Path<(String, String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"webdriver-classic-css",
|
|
"get element CSS value",
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let command = get_element_css_value_command(&context, object_id.clone(), property_name);
|
|
let result = binding.runtime.execute(command).await;
|
|
release_classic_remote_object(&binding, &context, object_id, "get element CSS value").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
classic_success_into_response(json!(value.value.as_str().unwrap_or_default()))
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element CSS value returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_computed_label(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
webdriver_classic_get_element_computed_value(
|
|
state,
|
|
session_id,
|
|
element_id,
|
|
ClassicComputedElementValue::Label,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_computed_role(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
webdriver_classic_get_element_computed_value(
|
|
state,
|
|
session_id,
|
|
element_id,
|
|
ClassicComputedElementValue::Role,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum ClassicComputedElementValue {
|
|
Label,
|
|
Role,
|
|
}
|
|
|
|
impl ClassicComputedElementValue {
|
|
fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Label => "get element computed label",
|
|
Self::Role => "get element computed role",
|
|
}
|
|
}
|
|
|
|
fn object_group(self) -> &'static str {
|
|
match self {
|
|
Self::Label => "webdriver-classic-computed-label",
|
|
Self::Role => "webdriver-classic-computed-role",
|
|
}
|
|
}
|
|
|
|
fn command(
|
|
self,
|
|
context: &ClassicDevToolsCommandContext,
|
|
object_id: String,
|
|
) -> DevToolsCommand {
|
|
match self {
|
|
Self::Label => get_element_computed_label_command(context, object_id),
|
|
Self::Role => get_element_computed_role_command(context, object_id),
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_get_element_computed_value(
|
|
state: AppState,
|
|
session_id: String,
|
|
element_id: String,
|
|
kind: ClassicComputedElementValue,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
kind.object_group(),
|
|
kind.label(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let result = binding
|
|
.runtime
|
|
.execute(kind.command(&context, object_id.clone()))
|
|
.await;
|
|
release_classic_remote_object(&binding, &context, object_id, kind.label()).await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
classic_success_into_response(json!(value.value.as_str().unwrap_or_default()))
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_execute_script_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{} returned an unexpected result", kind.label()),
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_is_element_displayed(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"webdriver-classic-displayed",
|
|
"is element displayed",
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let command = get_element_displayed_command(&context, object_id.clone());
|
|
let result = binding.runtime.execute(command).await;
|
|
release_classic_remote_object(&binding, &context, object_id, "is element displayed").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
classic_success_into_response(json!(value.value.as_bool().unwrap_or(false)))
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"is element displayed returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_element_property_value(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
name: impl Into<String>,
|
|
label: &'static str,
|
|
) -> Result<Value, ClassicError> {
|
|
let reference = resolve_classic_element_dom_reference(state, binding, element_id)?;
|
|
let command = get_element_property_reference_command(context, reference, name);
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::GetProperty(result)) => Ok(classic_property_value(result)),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
format!("{label} returned an unexpected result"),
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_get_element_property(
|
|
Path((session_id, element_id, name)): Path<(String, String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let params = json!({
|
|
"script": "return arguments[0][arguments[1]];",
|
|
"args": [
|
|
{ CLASSIC_ELEMENT_REFERENCE_KEY: element_id },
|
|
name
|
|
],
|
|
});
|
|
let mut command = match execute_sync_command(&context, ¶ms) {
|
|
Ok(command) => command,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let script_argument_handles = match prepare_classic_script_argument_handles(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
¶ms,
|
|
"get element property argument",
|
|
)
|
|
.await
|
|
{
|
|
Ok(handles) => handles,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
apply_classic_script_argument_handles(&mut command, &script_argument_handles);
|
|
let result = binding
|
|
.runtime
|
|
.execute_script(command, binding.timeouts.script.map(Duration::from_millis))
|
|
.await;
|
|
release_classic_remote_objects(
|
|
&binding,
|
|
&context,
|
|
script_argument_handles.remote_handles,
|
|
"get element property argument",
|
|
)
|
|
.await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
match classic_webdriver_script_result_value(&state, &binding, value.value).await {
|
|
Ok(value) => classic_success_into_response(value),
|
|
Err(error) => classic_error_into_response(error),
|
|
}
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_execute_script_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"get element property returned an unexpected result",
|
|
)),
|
|
Err(error)
|
|
if error.kind == DevToolsErrorKind::Timeout
|
|
|| error.message == "MissingDevToolsCommandResult" =>
|
|
{
|
|
classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::ScriptTimeout,
|
|
error.message,
|
|
))
|
|
}
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_clear_element(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let object_id = match resolve_classic_remote_object(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"webdriver-classic-clear",
|
|
"clear element",
|
|
)
|
|
.await
|
|
{
|
|
Ok(object_id) => object_id,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
|
|
let result = binding
|
|
.runtime
|
|
.execute(clear_element_command(&context, object_id.clone()))
|
|
.await;
|
|
release_classic_remote_object(&binding, &context, object_id, "clear element").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => match classic_clear_element_result(value.value) {
|
|
Ok(()) => classic_success_into_response(Value::Null),
|
|
Err(error) => classic_error_into_response(error),
|
|
},
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
classic_error_into_response(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => classic_error_into_response(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"clear element returned an unexpected result",
|
|
)),
|
|
Err(error) => classic_error_into_response(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_clear_element_result(value: Value) -> Result<(), ClassicError> {
|
|
match value.get("status").and_then(Value::as_str) {
|
|
Some("success") => Ok(()),
|
|
Some("invalid element state") => Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidElementState,
|
|
"element cannot be cleared",
|
|
)),
|
|
Some("unsupported") => Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidElementState,
|
|
"element is not clearable",
|
|
)),
|
|
_ => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"clear element returned an invalid result",
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_click_element(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
if let Err(error) = webdriver_classic_activate_element_by_handle(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
"element click",
|
|
"element click returned an unexpected result",
|
|
)
|
|
.await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_send_keys_to_element(
|
|
Path((session_id, element_id)): Path<(String, String)>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let text = match element_send_keys_text(¶ms) {
|
|
Ok(text) => text,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_binding_with_existing_current_browsing_context(binding).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let is_file_input = match webdriver_classic_element_is_file_input(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
)
|
|
.await
|
|
{
|
|
Ok(is_file_input) => is_file_input,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if is_file_input {
|
|
match webdriver_classic_send_file_input_keys(&state, &binding, &context, &element_id, &text)
|
|
.await
|
|
{
|
|
Ok(()) => {
|
|
return classic_success_into_response(Value::Null);
|
|
}
|
|
Err(error) => return classic_error_into_response(error),
|
|
}
|
|
}
|
|
let geometry =
|
|
match webdriver_classic_element_geometry(&state, &binding, &context, &element_id).await {
|
|
Ok(geometry) => geometry,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let send_keys_preflight = match webdriver_classic_prepare_text_control_for_send_keys(
|
|
&state,
|
|
&binding,
|
|
&context,
|
|
&element_id,
|
|
&text,
|
|
)
|
|
.await
|
|
{
|
|
Ok(preflight) => preflight,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if matches!(send_keys_preflight, ClassicSendKeysPreflight::ValueSet) {
|
|
return classic_success_into_response(Value::Null);
|
|
}
|
|
if matches!(
|
|
send_keys_preflight,
|
|
ClassicSendKeysPreflight::NotTextControl
|
|
) {
|
|
let input_context = classic_top_level_context(&binding);
|
|
let commands = match element_click_input_commands(&input_context, &geometry) {
|
|
Ok(commands) => commands,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) = webdriver_classic_execute_empty_commands(
|
|
&binding,
|
|
commands,
|
|
"element send keys focus returned an unexpected result",
|
|
)
|
|
.await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
}
|
|
if let Err(error) = webdriver_classic_execute_empty_commands(
|
|
&binding,
|
|
element_send_keys_input_commands(&classic_top_level_context(&binding), &text),
|
|
"element send keys input returned an unexpected result",
|
|
)
|
|
.await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum ClassicSendKeysPreflight {
|
|
TextControl,
|
|
ValueSet,
|
|
NotTextControl,
|
|
}
|
|
|
|
async fn webdriver_classic_prepare_text_control_for_send_keys(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
text: &str,
|
|
) -> Result<ClassicSendKeysPreflight, ClassicError> {
|
|
let object_id = resolve_classic_remote_object(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
"webdriver-classic-send-keys-preflight",
|
|
"element send keys preflight",
|
|
)
|
|
.await?;
|
|
let command = element_send_keys_prepare_text_control_command(context, object_id.clone(), text);
|
|
let result = binding.runtime.execute(command).await;
|
|
release_classic_remote_object(binding, context, object_id, "element send keys preflight").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => match *result {
|
|
DevToolsScriptResult::Value(value) => match value.value.as_str() {
|
|
Some("text-control") => Ok(ClassicSendKeysPreflight::TextControl),
|
|
Some("value-set") => Ok(ClassicSendKeysPreflight::ValueSet),
|
|
Some("not-text-control") => Ok(ClassicSendKeysPreflight::NotTextControl),
|
|
_ => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element send keys preflight returned an unexpected value",
|
|
)),
|
|
},
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
Err(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
},
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element send keys preflight returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_activate_element_by_handle(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
object_group: &str,
|
|
unexpected_result_message: &str,
|
|
) -> Result<(), ClassicError> {
|
|
let object_id = resolve_classic_remote_object(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
object_group,
|
|
object_group,
|
|
)
|
|
.await?;
|
|
let result = binding
|
|
.runtime
|
|
.execute_with_pending_navigation_wait(
|
|
element_click_command(context, object_id.clone()),
|
|
None,
|
|
binding.timeouts.page_load.map(Duration::from_millis),
|
|
)
|
|
.await;
|
|
release_classic_remote_object(binding, context, object_id, object_group).await;
|
|
let post_click_navigation = classic_wait_for_current_document(binding).await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Script(result)) => {
|
|
post_click_navigation?;
|
|
match *result {
|
|
DevToolsScriptResult::Value(value) => {
|
|
if value
|
|
.value
|
|
.get("detachedFrame")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false)
|
|
&& binding.current_frame_id.is_some()
|
|
{
|
|
state.classic_session_registry.lock().set_current_frame_id(
|
|
&binding.session_id,
|
|
Some(format!(
|
|
"__moli-detached-frame__:{}",
|
|
binding.browsing_context_target_id()
|
|
)),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
DevToolsScriptResult::Exception(exception) => {
|
|
Err(classic_webdriver_command_exception_error(exception))
|
|
}
|
|
}
|
|
}
|
|
Ok(_) => {
|
|
post_click_navigation?;
|
|
Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
unexpected_result_message,
|
|
))
|
|
}
|
|
// ChromeDriver treats execution-context loss caused by navigation as
|
|
// a navigation hint and resolves it at the outer window-command
|
|
// boundary. Moli emits this exact error only while consuming a
|
|
// response from a command already dispatched to the old renderer;
|
|
// replaying the JavaScript `.click()` against the successor Document
|
|
// could activate a second element. Wait for that successor and accept
|
|
// the side effect already dispatched in the old Document.
|
|
//
|
|
// Chromium references:
|
|
// chrome/test/chromedriver/element_commands.cc (ExecuteClickElement)
|
|
// chrome/test/chromedriver/window_commands.cc (ExecuteWindowCommand)
|
|
Err(error) if error.message == "Renderer attachment changed" => post_click_navigation,
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
async fn webdriver_classic_element_is_file_input(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
) -> Result<bool, ClassicError> {
|
|
let tag_name = webdriver_classic_element_property_value(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
"localName",
|
|
"send keys",
|
|
)
|
|
.await?;
|
|
if tag_name.as_str() != Some("input") {
|
|
return Ok(false);
|
|
}
|
|
let input_type = webdriver_classic_element_property_value(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
"type",
|
|
"send keys",
|
|
)
|
|
.await?;
|
|
Ok(input_type
|
|
.as_str()
|
|
.is_some_and(|input_type| input_type.eq_ignore_ascii_case("file")))
|
|
}
|
|
|
|
async fn webdriver_classic_send_file_input_keys(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
text: &str,
|
|
) -> Result<(), ClassicError> {
|
|
let multiple = webdriver_classic_element_property_value(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
"multiple",
|
|
"send keys",
|
|
)
|
|
.await?
|
|
.as_bool()
|
|
.unwrap_or(false);
|
|
let paths = classic_file_input_paths_from_send_keys(text)?;
|
|
if !multiple && paths.len() > 1 {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"the element can not hold multiple files",
|
|
));
|
|
}
|
|
let files = selected_files_from_paths(&paths, "WebDriver Classic file upload")
|
|
.map_err(|error| ClassicError::new(ClassicErrorCode::InvalidArgument, error.message))?;
|
|
let object_id = resolve_classic_remote_object(
|
|
state,
|
|
binding,
|
|
context,
|
|
element_id,
|
|
"webdriver-classic-file-upload",
|
|
"element file upload",
|
|
)
|
|
.await?;
|
|
let result = binding
|
|
.runtime
|
|
.execute(DevToolsCommand::SetFileInputFiles(
|
|
DevToolsSetFileInputFilesCommand {
|
|
context: DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverClassic,
|
|
session_id: Some(DevToolsSessionId::from(context.session_id.as_str())),
|
|
target_id: context.target_id.as_deref().map(DevToolsTargetId::from),
|
|
browser_context_id: None,
|
|
},
|
|
object_id: DevToolsRemoteHandleId::from(object_id.as_str()),
|
|
files,
|
|
append: multiple,
|
|
},
|
|
))
|
|
.await;
|
|
release_classic_remote_object(binding, context, object_id, "element file upload").await;
|
|
match result {
|
|
Ok(DevToolsCommandResult::Empty) => Ok(()),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element file upload returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_file_input_paths_from_send_keys(text: &str) -> Result<Vec<String>, ClassicError> {
|
|
if text.is_empty() {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"text is empty",
|
|
));
|
|
}
|
|
let paths = text
|
|
.split('\n')
|
|
.map(|path| path.trim().to_owned())
|
|
.filter(|path| !path.is_empty())
|
|
.collect::<Vec<_>>();
|
|
if paths.is_empty() {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"text is empty",
|
|
));
|
|
}
|
|
Ok(paths)
|
|
}
|
|
|
|
async fn webdriver_classic_element_geometry(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
element_id: &str,
|
|
) -> Result<DevToolsDomGeometryResult, ClassicError> {
|
|
let reference = resolve_classic_element_dom_reference(state, binding, element_id)?;
|
|
let commands = element_click_prepare_reference_commands(context, reference);
|
|
let mut geometry = None;
|
|
for command in commands {
|
|
match binding.runtime.execute(command).await {
|
|
Ok(DevToolsCommandResult::Empty) => {}
|
|
Ok(DevToolsCommandResult::DomGeometry(result)) => geometry = Some(result),
|
|
Ok(_) => {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element geometry returned an unexpected result",
|
|
));
|
|
}
|
|
Err(error) => return Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
geometry.ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"element geometry did not return geometry",
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn webdriver_classic_action_element_origin_points(
|
|
state: &AppState,
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
params: &serde_json::Value,
|
|
) -> Result<ClassicElementOriginViewportPoints, ClassicError> {
|
|
let element_ids = action_element_origin_ids(params)?;
|
|
let mut origins = ClassicElementOriginViewportPoints::new();
|
|
for element_id in element_ids {
|
|
let geometry =
|
|
webdriver_classic_element_geometry(state, binding, context, &element_id).await?;
|
|
let point = element_center_from_geometry(&geometry)?;
|
|
origins.insert(element_id, point);
|
|
}
|
|
Ok(origins)
|
|
}
|
|
|
|
async fn webdriver_classic_viewport_bounds(
|
|
binding: &ClassicSessionBinding,
|
|
context: &ClassicDevToolsCommandContext,
|
|
) -> Result<ClassicViewportBounds, ClassicError> {
|
|
match binding
|
|
.runtime
|
|
.execute(layout_metrics_command(context))
|
|
.await
|
|
{
|
|
Ok(DevToolsCommandResult::LayoutMetrics(metrics)) => Ok(classic_viewport_bounds(metrics)),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
"layout metrics returned an unexpected result",
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
fn classic_viewport_bounds(metrics: DevToolsLayoutMetricsResult) -> ClassicViewportBounds {
|
|
ClassicViewportBounds::new(
|
|
metrics.layout_viewport_width,
|
|
metrics.layout_viewport_height,
|
|
)
|
|
}
|
|
|
|
async fn webdriver_classic_execute_empty_commands(
|
|
binding: &ClassicSessionBinding,
|
|
commands: Vec<DevToolsCommand>,
|
|
unexpected_result_message: &str,
|
|
) -> Result<(), ClassicError> {
|
|
for command in commands {
|
|
webdriver_classic_expect_empty_result(
|
|
binding.runtime.execute(command).await,
|
|
unexpected_result_message,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn webdriver_classic_expect_empty_result(
|
|
result: Result<DevToolsCommandResult, DevToolsError>,
|
|
unexpected_result_message: &str,
|
|
) -> Result<(), ClassicError> {
|
|
match result {
|
|
Ok(DevToolsCommandResult::Empty) => Ok(()),
|
|
Ok(_) => Err(ClassicError::new(
|
|
ClassicErrorCode::UnknownError,
|
|
unexpected_result_message,
|
|
)),
|
|
Err(error) => Err(classic_error_from_devtools_error(error)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_perform_actions(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
body: Bytes,
|
|
) -> Response {
|
|
let params = match classic_json_body(&body) {
|
|
Ok(params) => params,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_browsing_context(&binding);
|
|
let input_context = classic_top_level_context(&binding);
|
|
let element_origins =
|
|
match webdriver_classic_action_element_origin_points(&state, &binding, &context, ¶ms)
|
|
.await
|
|
{
|
|
Ok(element_origins) => element_origins,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let viewport_bounds = match webdriver_classic_viewport_bounds(&binding, &input_context).await {
|
|
Ok(viewport_bounds) => viewport_bounds,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let ticks = match state.classic_session_registry.lock().perform_actions_ticks(
|
|
&session_id,
|
|
&input_context,
|
|
¶ms,
|
|
&element_origins,
|
|
viewport_bounds,
|
|
) {
|
|
Ok(ticks) => ticks,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
for tick in ticks {
|
|
if let Err(error) = webdriver_classic_execute_empty_commands(
|
|
&binding,
|
|
tick.commands,
|
|
"perform actions returned an unexpected result",
|
|
)
|
|
.await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
if tick.duration_ms > 0 {
|
|
sleep(Duration::from_millis(tick.duration_ms)).await;
|
|
}
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
pub(super) async fn webdriver_classic_release_actions(
|
|
Path(session_id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Response {
|
|
let binding = match classic_current_browsing_context_binding(&state, &session_id).await {
|
|
Ok(binding) => binding,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
let context = classic_top_level_context(&binding);
|
|
let commands = match state
|
|
.classic_session_registry
|
|
.lock()
|
|
.release_actions_commands(&session_id, &context)
|
|
{
|
|
Ok(commands) => commands,
|
|
Err(error) => return classic_error_into_response(error),
|
|
};
|
|
if let Err(error) = webdriver_classic_execute_empty_commands(
|
|
&binding,
|
|
commands,
|
|
"release actions returned an unexpected result",
|
|
)
|
|
.await
|
|
{
|
|
return classic_error_into_response(error);
|
|
}
|
|
classic_success_into_response(Value::Null)
|
|
}
|
|
|
|
fn webdriver_classic_capabilities(
|
|
state: &AppState,
|
|
session_id: &str,
|
|
page_load_strategy: ClassicPageLoadStrategy,
|
|
unhandled_prompt_behavior: &ClassicUnhandledPromptBehavior,
|
|
downloads_enabled: bool,
|
|
) -> serde_json::Value {
|
|
let web_socket_url = format!("{}/{}", state.bidi_ws_url.trim_end_matches('/'), session_id);
|
|
let mut capabilities = json!({
|
|
"browserName": "moli",
|
|
"browserVersion": version::PRODUCT,
|
|
"platformName": std::env::consts::OS,
|
|
"acceptInsecureCerts": !state.fetch_config.tls_verify_host(),
|
|
"pageLoadStrategy": page_load_strategy.as_str(),
|
|
"unhandledPromptBehavior": unhandled_prompt_behavior.returned_capability(),
|
|
"setWindowRect": false,
|
|
"webSocketUrl": web_socket_url,
|
|
});
|
|
if downloads_enabled && let Some(object) = capabilities.as_object_mut() {
|
|
object.insert("se:downloadsEnabled".to_owned(), json!(true));
|
|
}
|
|
capabilities
|
|
}
|
|
|
|
fn downloads_enabled_from_capabilities(
|
|
capabilities: &serde_json::Map<String, Value>,
|
|
) -> Result<bool, ClassicError> {
|
|
capabilities
|
|
.get("se:downloadsEnabled")
|
|
.map(downloads_enabled_capability_value)
|
|
.unwrap_or(Ok(false))
|
|
}
|
|
|
|
fn downloads_enabled_capability_value(value: &Value) -> Result<bool, ClassicError> {
|
|
value.as_bool().ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"se:downloadsEnabled must be a boolean",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn selenium_download_behavior_command(session_id: &str, directory: &FsPath) -> DevToolsCommand {
|
|
DevToolsCommand::SetDownloadBehavior(DevToolsSetDownloadBehaviorCommand {
|
|
context: DevToolsCommandContext {
|
|
protocol: DevToolsProtocol::WebDriverClassic,
|
|
session_id: Some(DevToolsSessionId::from(session_id)),
|
|
target_id: None,
|
|
browser_context_id: None,
|
|
},
|
|
behavior: Some(DevToolsDownloadBehaviorSetting {
|
|
behavior: "allow".to_owned(),
|
|
download_path: Some(directory.to_string_lossy().to_string()),
|
|
events_enabled: true,
|
|
}),
|
|
user_contexts: None,
|
|
})
|
|
}
|
|
|
|
fn classic_download_directory(state: &AppState, session_id: &str) -> Result<PathBuf, ClassicError> {
|
|
let _binding = classic_session_binding(state, session_id)?;
|
|
state
|
|
.classic_session_registry
|
|
.lock()
|
|
.download_directory(session_id)
|
|
.ok_or_else(|| {
|
|
ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"Selenium downloads are not enabled for this session",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn classic_download_file_names(directory: &FsPath) -> Result<Vec<String>, String> {
|
|
let entries = match fs::read_dir(directory) {
|
|
Ok(entries) => entries,
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
|
Err(error) => return Err(format!("unable to list download files: {error}")),
|
|
};
|
|
let mut names = Vec::new();
|
|
for entry in entries {
|
|
let entry = entry.map_err(|error| format!("unable to read download entry: {error}"))?;
|
|
let metadata = entry
|
|
.metadata()
|
|
.map_err(|error| format!("unable to stat download entry: {error}"))?;
|
|
if !metadata.is_file() {
|
|
continue;
|
|
}
|
|
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
continue;
|
|
};
|
|
if name.ends_with(".crdownload") {
|
|
continue;
|
|
}
|
|
names.push(name);
|
|
}
|
|
names.sort();
|
|
Ok(names)
|
|
}
|
|
|
|
fn classic_download_file_name(name: &str) -> Result<String, ClassicError> {
|
|
if name.is_empty() || name == "." || name == ".." || name.contains('/') || name.contains('\\') {
|
|
return Err(ClassicError::new(
|
|
ClassicErrorCode::InvalidArgument,
|
|
"invalid download file name",
|
|
));
|
|
}
|
|
Ok(name.to_owned())
|
|
}
|
|
|
|
fn classic_clear_download_directory(directory: &FsPath) -> Result<(), String> {
|
|
let entries = match fs::read_dir(directory) {
|
|
Ok(entries) => entries,
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
|
Err(error) => return Err(format!("unable to list download files: {error}")),
|
|
};
|
|
for entry in entries {
|
|
let entry = entry.map_err(|error| format!("unable to read download entry: {error}"))?;
|
|
let metadata = entry
|
|
.metadata()
|
|
.map_err(|error| format!("unable to stat download entry: {error}"))?;
|
|
let path = entry.path();
|
|
if metadata.is_file() {
|
|
fs::remove_file(&path)
|
|
.map_err(|error| format!("unable to remove download file: {error}"))?;
|
|
} else if metadata.is_dir() {
|
|
fs::remove_dir_all(&path)
|
|
.map_err(|error| format!("unable to remove download directory: {error}"))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn described_shadow_root_prefers_backend_node_reference() {
|
|
assert_eq!(
|
|
classic_author_shadow_root_reference_from_described_node(&json!({
|
|
"shadowRoots": [{
|
|
"nodeId": 7,
|
|
"backendNodeId": 42,
|
|
"shadowRootType": "open"
|
|
}]
|
|
})),
|
|
Some((42, DevToolsDomNodeReference::BackendNodeId(42)))
|
|
);
|
|
assert_eq!(
|
|
classic_author_shadow_root_reference_from_described_node(&json!({
|
|
"shadowRoots": [{
|
|
"backendNodeId": 42,
|
|
"shadowRootType": "closed"
|
|
}]
|
|
})),
|
|
Some((42, DevToolsDomNodeReference::BackendNodeId(42)))
|
|
);
|
|
assert_eq!(
|
|
classic_author_shadow_root_reference_from_described_node(&json!({
|
|
"shadowRoots": [{
|
|
"nodeId": 0,
|
|
"backendNodeId": 42,
|
|
"shadowRootType": "open"
|
|
}]
|
|
})),
|
|
Some((42, DevToolsDomNodeReference::BackendNodeId(42)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn described_user_agent_shadow_root_is_not_a_webdriver_shadow_root() {
|
|
assert_eq!(
|
|
classic_author_shadow_root_reference_from_described_node(&json!({
|
|
"shadowRoots": [{
|
|
"nodeId": 7,
|
|
"backendNodeId": 42,
|
|
"shadowRootType": "user-agent"
|
|
}]
|
|
})),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_owner_reference_prefers_backend_node_id() {
|
|
assert_eq!(
|
|
classic_dom_reference_from_frame_owner_result(DevToolsGetFrameOwnerResult {
|
|
node_id: 7,
|
|
backend_node_id: 2_000_000_007,
|
|
}),
|
|
DevToolsDomNodeReference::BackendNodeId(2_000_000_007)
|
|
);
|
|
assert_eq!(
|
|
classic_dom_reference_from_frame_owner_result(DevToolsGetFrameOwnerResult {
|
|
node_id: 7,
|
|
backend_node_id: 0,
|
|
}),
|
|
DevToolsDomNodeReference::FrontendNodeId(7)
|
|
);
|
|
}
|
|
}
|