use anyhow::Result;
use log::debug;
use axum::Router;
use axum::routing::get_service;
use std::net::SocketAddr;
use tokio::task::JoinHandle;
use tower_http::services::ServeDir;
use webdriverbidi::model::browser::ClientWindowInfo;
use webdriverbidi::model::browser::{CreateUserContextParameters, RemoveUserContextParameters};
use webdriverbidi::model::browsing_context::{
CreateParameters,
CreateType,
GetTreeParameters,
NavigateParameters,
ReadinessState,
};
use webdriverbidi::model::common::EmptyParams;
use webdriverbidi::model::script::{
CallFunctionParameters, ContextTarget, LocalValue, PrimitiveProtocolValue, StringValue, Target,
};
use webdriverbidi::model::script::{
EvaluateResult,
RemoteValue, };
use webdriverbidi::session::WebDriverBiDiSession;
use webdriverbidi::webdriver::capabilities::CapabilitiesRequest;
const HOST: &str = "localhost";
const PORT: u16 = 4444;
const STATIC_ROUTE: &str = "/static";
const STATIC_DIR: &str = "static";
const DEFAULT_ADDR: &str = "127.0.0.1:0";
pub mod session {
use super::*;
pub async fn init() -> Result<WebDriverBiDiSession> {
let capabilities = CapabilitiesRequest::default();
let mut bidi_session = WebDriverBiDiSession::new(HOST.into(), PORT, capabilities);
bidi_session.start().await?;
Ok(bidi_session)
}
pub async fn close(bidi_session: &mut WebDriverBiDiSession) -> Result<()> {
bidi_session.close().await?;
Ok(())
}
}
pub mod browser {
use super::*;
pub async fn get_user_context_ids(
bidi_session: &mut WebDriverBiDiSession,
) -> Result<Vec<String>> {
let user_contexts = bidi_session
.browser_get_user_contexts(EmptyParams::new())
.await?
.user_contexts;
let user_contexts = user_contexts
.into_iter()
.map(|user_context_info| user_context_info.user_context)
.collect::<Vec<_>>();
Ok(user_contexts)
}
pub async fn create_user_context(bidi_session: &mut WebDriverBiDiSession) -> Result<String> {
let user_context = bidi_session
.browser_create_user_context(CreateUserContextParameters::new(None, None, None))
.await?
.user_context;
Ok(user_context)
}
pub async fn get_client_windows(
bidi_session: &mut WebDriverBiDiSession,
) -> Result<Vec<ClientWindowInfo>> {
let client_windows = bidi_session
.browser_get_client_windows(EmptyParams::new())
.await?
.client_windows;
Ok(client_windows)
}
pub async fn remove_user_context(
bidi_session: &mut WebDriverBiDiSession,
user_context: String,
) -> Result<()> {
bidi_session
.browser_remove_user_context(RemoveUserContextParameters::new(user_context.clone()))
.await?;
Ok(())
}
}
pub mod browsing_context {
use super::*;
pub async fn new_tab_in_user_context(
session: &mut WebDriverBiDiSession,
user_context: String,
) -> Result<String> {
let create_params = CreateParameters::new(CreateType::Tab, None, None, Some(user_context));
let context = session
.browsing_context_create(create_params)
.await?
.context;
Ok(context)
}
pub async fn navigate(
session: &mut WebDriverBiDiSession,
context: String,
url: String,
) -> Result<()> {
let navigate_params = NavigateParameters::new(context, url, Some(ReadinessState::Complete));
session.browsing_context_navigate(navigate_params).await?;
Ok(())
}
pub async fn new_window(session: &mut WebDriverBiDiSession) -> Result<String> {
let create_params = CreateParameters::new(CreateType::Window, None, None, None);
let context = session
.browsing_context_create(create_params)
.await?
.context;
Ok(context)
}
pub async fn get_nth_context(
session: &mut WebDriverBiDiSession,
index: usize,
) -> Result<String> {
let get_tree_params = GetTreeParameters::new(None, None);
let get_tree_rslt = session.browsing_context_get_tree(get_tree_params).await?;
Ok(get_tree_rslt.contexts[index].context.clone())
}
pub async fn new_tab(session: &mut WebDriverBiDiSession) -> Result<String> {
let create_params = CreateParameters::new(CreateType::Tab, None, None, None);
let context = session
.browsing_context_create(create_params)
.await?
.context;
Ok(context)
}
}
fn target_context(context: &str) -> Target {
Target::ContextTarget(ContextTarget::new(context.to_string(), None))
}
fn local_value(str: &str) -> LocalValue {
LocalValue::PrimitiveProtocolValue(PrimitiveProtocolValue::StringValue(StringValue::new(
str.to_string(),
)))
}
pub mod local_storage {
use super::*;
pub async fn get(
bidi_session: &mut WebDriverBiDiSession,
context: &str,
key: &str,
) -> Result<Option<String>> {
let function_declaration = "(key) => localStorage.getItem(key)".to_string();
let key_local_value = local_value(key);
let args = Some(vec![key_local_value]);
let params = CallFunctionParameters::new(
function_declaration,
false,
target_context(context),
args,
None,
None,
None,
None,
);
let eval_result = bidi_session.script_call_function(params).await?;
match eval_result {
EvaluateResult::EvaluateResultSuccess(eval_rslt_success) => {
let remote_value = eval_rslt_success.result;
match remote_value {
RemoteValue::PrimitiveProtocolValue(
webdriverbidi::model::script::PrimitiveProtocolValue::StringValue(
string_value,
),
) => Ok(Some(string_value.value)),
_ => Ok(None),
}
}
_ => Ok(None),
}
}
pub async fn set(
bidi_session: &mut WebDriverBiDiSession,
context: &str,
key: &str,
value: &str,
) -> Result<()> {
let function_declaration = "(key, value) => localStorage.setItem(key, value)".to_string();
let key_local_value = local_value(key);
let value_local_value = local_value(value);
let args = Some(vec![key_local_value, value_local_value]);
let params = CallFunctionParameters::new(
function_declaration,
false,
target_context(context),
args,
None,
None,
None,
None,
);
bidi_session.script_call_function(params).await?;
Ok(())
}
}
pub mod axum_utils {
use super::*;
pub async fn serve_static(html_file: &str) -> Result<(String, JoinHandle<()>)> {
let app = Router::new().nest_service(STATIC_ROUTE, get_service(ServeDir::new(STATIC_DIR)));
let listener = tokio::net::TcpListener::bind(DEFAULT_ADDR).await?;
let addr: SocketAddr = listener.local_addr()?;
let server_handle = tokio::spawn(async move {
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
eprintln!("Server error: {}", e);
}
});
let url = format!("http://{}{}/{}", addr, STATIC_ROUTE, html_file);
debug!("Axum server running on {}", addr);
Ok((url, server_handle))
}
}
pub async fn is_element_focused(
bidi_session: &mut WebDriverBiDiSession,
context: &str,
selector: &str,
) -> Result<bool> {
let function_declaration = "(selector) => {
return document.querySelector(selector) === document.activeElement;
}"
.to_string();
let selector_local_value = local_value(selector);
let args = Some(vec![selector_local_value]);
let params = CallFunctionParameters::new(
function_declaration,
false,
target_context(context),
args,
None,
None,
None,
None,
);
let rslt = bidi_session.script_call_function(params).await?;
debug!("is_element_focused result: {:?}", rslt);
Ok(true)
}
pub async fn assert_document_status(
bidi_session: &mut WebDriverBiDiSession,
context: &str,
) -> Result<bool> {
let visibility_state = get_visibility_state(bidi_session, context).await?;
let doc_focus = get_document_focus(bidi_session, context).await?;
Ok(visibility_state == "visible" && doc_focus)
}
pub async fn get_visibility_state(
bidi_session: &mut WebDriverBiDiSession,
context: &str,
) -> Result<String> {
let function_declaration = r#"() => {
return document.visibilityState;
}"#
.to_string();
let params = CallFunctionParameters::new(
function_declaration,
false,
target_context(context),
None,
None,
None,
None,
None,
);
let rslt = bidi_session.script_call_function(params).await?;
match rslt {
EvaluateResult::EvaluateResultSuccess(eval_rslt_success) => {
match eval_rslt_success.result {
RemoteValue::PrimitiveProtocolValue(
webdriverbidi::model::script::PrimitiveProtocolValue::StringValue(string_value),
) => {
debug!("get_visibility_state result: {:?}", string_value);
Ok(string_value.value)
}
remote_val => Err(anyhow::anyhow!(
"Received EvaluateResultSuccess but not a string value, actual remote value: {:?}",
remote_val
)),
}
}
eval_rslt => Err(anyhow::anyhow!(
"Received unexpected EvaluateResult: {:?}",
eval_rslt
)),
}
}
pub async fn get_document_focus(
bidi_session: &mut WebDriverBiDiSession,
context: &str,
) -> Result<bool> {
let function_declaration = r#"() => {
return document.hasFocus();
}"#
.to_string();
let params = CallFunctionParameters::new(
function_declaration,
false,
target_context(context),
None,
None,
None,
None,
None,
);
let rslt = bidi_session.script_call_function(params).await?;
match rslt {
EvaluateResult::EvaluateResultSuccess(eval_rslt_success) => {
match eval_rslt_success.result {
RemoteValue::PrimitiveProtocolValue(
webdriverbidi::model::script::PrimitiveProtocolValue::BooleanValue(bool_value),
) => {
debug!("get_document_focus result: {:?}", bool_value);
Ok(bool_value.value)
}
remote_val => Err(anyhow::anyhow!(
"Received EvaluateResultSuccess but not a boolean value, actual remote value: {:?}",
remote_val
)),
}
}
eval_rslt => Err(anyhow::anyhow!(
"Received unexpected EvaluateResult: {:?}",
eval_rslt
)),
}
}