use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use failure::{Error, Fail, Fallible};
use log::*;
use serde;
use element::Element;
use point::Point;
use crate::browser::Transport;
use crate::protocol::dom::Node;
use crate::protocol::page::methods::Navigate;
use crate::protocol::target::TargetId;
use crate::protocol::target::TargetInfo;
use crate::protocol::{dom, input, page, profiler, target};
use crate::protocol::{network, Event, RemoteError};
use crate::{protocol, util};
use super::transport::SessionId;
pub mod element;
mod keys;
mod point;
#[derive(Debug)]
pub enum RequestInterceptionDecision {
Continue,
Response(String),
}
pub type RequestInterceptor = Box<
dyn Fn(
Arc<Transport>,
SessionId,
protocol::network::events::RequestInterceptedEventParams,
) -> RequestInterceptionDecision
+ Send
+ Sync,
>;
#[rustfmt::skip]
pub type ResponseHandler = Box<
Fn(
protocol::network::events::ResponseReceivedEventParams,
&dyn Fn() -> Result<
protocol::network::methods::GetResponseBodyReturnObject,
failure::Error,
>,
) + Send
+ Sync,
>;
pub struct Tab {
target_id: TargetId,
transport: Arc<Transport>,
session_id: SessionId,
navigating: Arc<AtomicBool>,
target_info: Arc<Mutex<TargetInfo>>,
request_interceptor: Arc<Mutex<RequestInterceptor>>,
response_handler: Arc<Mutex<Option<ResponseHandler>>>,
}
#[derive(Debug, Fail)]
#[fail(display = "No element found")]
pub struct NoElementFound {}
#[derive(Debug, Fail)]
#[fail(display = "Navigate failed: {}", error_text)]
pub struct NavigationFailed {
error_text: String,
}
impl NoElementFound {
pub fn map(error: Error) -> Error {
match error.downcast::<RemoteError>() {
Ok(remote_error) => {
match remote_error.message.as_ref() {
"Could not find node with given id" => Self {}.into(),
_ => remote_error.into(),
}
}
Err(original_error) => original_error,
}
}
}
impl<'a> Tab {
pub fn new(target_info: TargetInfo, transport: Arc<Transport>) -> Fallible<Self> {
let target_id = target_info.target_id.clone();
let session_id = transport
.call_method_on_browser(target::methods::AttachToTarget {
target_id: &target_id,
flatten: None,
})?
.session_id
.into();
debug!("New tab attached with session ID: {:?}", session_id);
let target_info_mutex = Arc::new(Mutex::new(target_info));
let tab = Self {
target_id,
transport,
session_id,
navigating: Arc::new(AtomicBool::new(false)),
target_info: target_info_mutex,
request_interceptor: Arc::new(Mutex::new(Box::new(
|_transport, _session_id, _interception| RequestInterceptionDecision::Continue,
))),
response_handler: Arc::new(Mutex::new(None)),
};
tab.call_method(page::methods::Enable {})?;
tab.call_method(page::methods::SetLifecycleEventsEnabled { enabled: true })?;
tab.start_event_handler_thread();
Ok(tab)
}
pub fn update_target_info(&self, target_info: TargetInfo) {
let mut info = self.target_info.lock().unwrap();
*info = target_info;
}
pub fn get_target_id(&self) -> &TargetId {
&self.target_id
}
pub fn get_target_info(&self) -> Fallible<TargetInfo> {
Ok(self
.call_method(target::methods::GetTargetInfo {
target_id: self.get_target_id(),
})?
.target_info)
}
pub fn get_browser_context_id(&self) -> Fallible<Option<String>> {
Ok(self.get_target_info()?.browser_context_id)
}
pub fn get_url(&self) -> String {
let info = self.target_info.lock().unwrap();
info.url.clone()
}
fn start_event_handler_thread(&self) {
let transport: Arc<Transport> = Arc::clone(&self.transport);
let incoming_events_rx = self
.transport
.listen_to_target_events(self.session_id.clone());
let navigating = Arc::clone(&self.navigating);
let interceptor_mutex = Arc::clone(&self.request_interceptor);
let response_handler_mutex = self.response_handler.clone();
let session_id = self.session_id.clone();
thread::spawn(move || {
for event in incoming_events_rx {
match event {
Event::Lifecycle(lifecycle_event) => {
let event_name = lifecycle_event.params.name.as_ref();
trace!("Lifecycle event: {}", event_name);
match event_name {
"networkAlmostIdle" => {
navigating.store(false, Ordering::SeqCst);
}
"init" => {
navigating.store(true, Ordering::SeqCst);
}
_ => {}
}
}
Event::RequestIntercepted(interception_event) => {
let id = interception_event.params.interception_id.clone();
let interceptor = interceptor_mutex.lock().unwrap();
let decision = interceptor(
Arc::clone(&transport),
session_id.clone(),
interception_event.params,
);
match decision {
RequestInterceptionDecision::Continue => {
let method = network::methods::ContinueInterceptedRequest {
interception_id: &id,
..Default::default()
};
transport
.call_method_on_target(session_id.clone(), method)
.expect("couldn't continue intercepted request");
}
RequestInterceptionDecision::Response(response_str) => {
let method = network::methods::ContinueInterceptedRequest {
interception_id: &id,
raw_response: Some(&response_str),
..Default::default()
};
transport
.call_method_on_target(session_id.clone(), method)
.expect("couldn't continue intercepted request");
}
}
}
Event::ResponseReceived(ev) => {
if let Some(handler) = response_handler_mutex.lock().unwrap().as_ref() {
let request_id = ev.params.request_id.clone();
let retrieve_body = || {
let method = network::methods::GetResponseBody {
request_id: &request_id,
};
transport.call_method_on_target(session_id.clone(), method)
};
handler(ev.params, &retrieve_body);
}
}
_ => {
let mut raw_event = format!("{:?}", event);
raw_event.truncate(50);
trace!("Unhandled event: {}", raw_event);
}
}
}
info!("finished tab's event handling loop");
});
}
pub fn call_method<C>(&self, method: C) -> Fallible<C::ReturnObject>
where
C: protocol::Method + serde::Serialize + std::fmt::Debug,
{
trace!("Calling method: {:?}", method);
let result = self
.transport
.call_method_on_target(self.session_id.clone(), method);
let mut result_string = format!("{:?}", result);
result_string.truncate(70);
trace!("Got result: {:?}", result_string);
result
}
pub fn wait_until_navigated(&self) -> Fallible<&Self> {
let navigating = Arc::clone(&self.navigating);
util::Wait::with_timeout(Duration::from_secs(20)).until(|| {
if navigating.load(Ordering::SeqCst) {
None
} else {
Some(true)
}
})?;
debug!("A tab finished navigating");
Ok(self)
}
pub fn navigate_to(&self, url: &str) -> Fallible<&Self> {
let return_object = self.call_method(Navigate { url })?;
if let Some(error_text) = return_object.error_text {
return Err(NavigationFailed { error_text }.into());
}
let navigating = Arc::clone(&self.navigating);
navigating.store(true, Ordering::SeqCst);
info!("Navigating a tab to {}", url);
Ok(self)
}
pub fn wait_for_element(&self, selector: &str) -> Fallible<Element<'_>> {
self.wait_for_element_with_custom_timeout(selector, std::time::Duration::from_secs(3))
}
pub fn wait_for_element_with_custom_timeout(
&self,
selector: &str,
timeout: std::time::Duration,
) -> Fallible<Element<'_>> {
debug!("Waiting for element with selector: {}", selector);
util::Wait::with_timeout(timeout).strict_until(
|| self.find_element(selector),
Error::downcast::<NoElementFound>,
)
}
pub fn wait_for_elements(&self, selector: &str) -> Fallible<Vec<Element<'_>>> {
debug!("Waiting for element with selector: {}", selector);
util::Wait::with_timeout(Duration::from_secs(3)).strict_until(
|| self.find_elements(selector),
Error::downcast::<NoElementFound>,
)
}
pub fn find_element(&self, selector: &str) -> Fallible<Element<'_>> {
trace!("Looking up element via selector: {}", selector);
let root_node_id = self.get_document()?.node_id;
let node_id = self
.call_method(dom::methods::QuerySelector {
node_id: root_node_id,
selector,
})
.map_err(NoElementFound::map)?
.node_id;
Element::new(&self, node_id)
}
pub fn get_document(&self) -> Fallible<Node> {
Ok(self
.call_method(dom::methods::GetDocument {
depth: Some(0),
pierce: Some(false),
})?
.root)
}
pub fn find_elements(&self, selector: &str) -> Fallible<Vec<Element<'_>>> {
trace!("Looking up elements via selector: {}", selector);
let root_node_id = self.get_document()?.node_id;
let node_ids = self
.call_method(dom::methods::QuerySelectorAll {
node_id: root_node_id,
selector,
})
.map_err(NoElementFound::map)?
.node_ids;
if node_ids.is_empty() {
return Err(NoElementFound {}.into());
}
node_ids
.into_iter()
.map(|node_id| Element::new(&self, node_id))
.collect()
}
pub fn describe_node(&self, node_id: dom::NodeId) -> Fallible<dom::Node> {
let node = self
.call_method(dom::methods::DescribeNode {
node_id: Some(node_id),
backend_node_id: None,
depth: Some(100),
})?
.node;
Ok(node)
}
pub fn type_str(&self, string_to_type: &str) -> Fallible<&Self> {
for c in string_to_type.split("") {
if c == "" {
continue;
}
self.press_key(c)?;
}
Ok(self)
}
pub fn press_key(&self, key: &str) -> Fallible<&Self> {
let definition = keys::get_key_definition(key)?;
let text = definition.text.or_else(|| {
if definition.key.len() == 1 {
Some(definition.key)
} else {
None
}
});
let key_down_event_type = if text.is_some() {
"keyDown"
} else {
"rawKeyDown"
};
let key = Some(definition.key);
let code = Some(definition.code);
self.call_method(input::methods::DispatchKeyEvent {
event_type: key_down_event_type,
key,
text,
code: Some(definition.code),
windows_virtual_key_code: definition.key_code,
native_virtual_key_code: definition.key_code,
})?;
self.call_method(input::methods::DispatchKeyEvent {
event_type: "keyUp",
key,
text,
code,
windows_virtual_key_code: definition.key_code,
native_virtual_key_code: definition.key_code,
})?;
Ok(self)
}
pub fn move_mouse_to_point(&self, point: Point) -> Fallible<&Self> {
if point.x == 0.0 && point.y == 0.0 {
warn!("Midpoint of element shouldn't be 0,0. Something is probably wrong.")
}
self.call_method(input::methods::DispatchMouseEvent {
event_type: "mouseMoved",
x: point.x,
y: point.y,
..Default::default()
})?;
Ok(self)
}
pub fn click_point(&self, point: Point) -> Fallible<&Self> {
trace!("Clicking point: {:?}", point);
if point.x == 0.0 && point.y == 0.0 {
warn!("Midpoint of element shouldn't be 0,0. Something is probably wrong.")
}
self.move_mouse_to_point(point)?;
self.call_method(input::methods::DispatchMouseEvent {
event_type: "mousePressed",
x: point.x,
y: point.y,
button: Some("left"),
click_count: Some(1),
})?;
self.call_method(input::methods::DispatchMouseEvent {
event_type: "mouseReleased",
x: point.x,
y: point.y,
button: Some("left"),
click_count: Some(1),
})?;
Ok(self)
}
pub fn capture_screenshot(
&self,
format: page::ScreenshotFormat,
clip: Option<page::Viewport>,
from_surface: bool,
) -> Fallible<Vec<u8>> {
let (format, quality) = match format {
page::ScreenshotFormat::JPEG(quality) => {
(page::InternalScreenshotFormat::JPEG, quality)
}
page::ScreenshotFormat::PNG => (page::InternalScreenshotFormat::PNG, None),
};
let data = self
.call_method(page::methods::CaptureScreenshot {
format,
clip,
quality,
from_surface,
})?
.data;
base64::decode(&data).map_err(Into::into)
}
pub fn print_to_pdf(&self, options: Option<page::PrintToPdfOptions>) -> Fallible<Vec<u8>> {
let data = self
.call_method(page::methods::PrintToPdf { options })?
.data;
base64::decode(&data).map_err(Into::into)
}
pub fn reload(&self, ignore_cache: bool, script_to_evaluate: Option<&str>) -> Fallible<&Self> {
self.call_method(page::methods::Reload {
ignore_cache,
script_to_evaluate,
})?;
Ok(self)
}
pub fn enable_profiler(&self) -> Fallible<&Self> {
self.call_method(profiler::methods::Enable {})?;
Ok(self)
}
pub fn disable_profiler(&self) -> Fallible<&Self> {
self.call_method(profiler::methods::Disable {})?;
Ok(self)
}
pub fn start_js_coverage(&self) -> Fallible<&Self> {
self.call_method(profiler::methods::StartPreciseCoverage {
call_count: Some(true),
detailed: Some(true),
})?;
Ok(self)
}
pub fn stop_js_coverage(&self) -> Fallible<&Self> {
self.call_method(profiler::methods::StopPreciseCoverage {})?;
Ok(self)
}
pub fn take_precise_js_coverage(&self) -> Fallible<Vec<profiler::ScriptCoverage>> {
let script_coverages = self
.call_method(profiler::methods::TakePreciseCoverage {})?
.result;
Ok(script_coverages)
}
pub fn enable_request_interception(
&self,
patterns: &[network::methods::RequestPattern],
interceptor: RequestInterceptor,
) -> Fallible<()> {
let mut current_interceptor = self.request_interceptor.lock().unwrap();
*current_interceptor = interceptor;
self.call_method(network::methods::SetRequestInterception {
patterns: &patterns,
})?;
Ok(())
}
pub fn continue_intercepted_request(
&self,
interception_id: &str,
modified_response: Option<&str>,
) -> Fallible<()> {
self.call_method(network::methods::ContinueInterceptedRequest {
interception_id,
error_reason: None,
raw_response: modified_response,
url: None,
method: None,
post_data: None,
headers: None,
auth_challenge_response: None,
})?;
Ok(())
}
pub fn enable_response_handling(&self, handler: ResponseHandler) -> Fallible<()> {
self.call_method(network::methods::Enable {})?;
*(self.response_handler.lock().unwrap()) = Some(handler);
Ok(())
}
pub fn enable_debugger(&self) -> Fallible<()> {
self.call_method(protocol::debugger::methods::Enable {})?;
Ok(())
}
pub fn disable_debugger(&self) -> Fallible<()> {
self.call_method(protocol::debugger::methods::Disable {})?;
Ok(())
}
pub fn get_script_source(&self, script_id: &str) -> Fallible<String> {
Ok(self
.call_method(protocol::debugger::methods::GetScriptSource { script_id })?
.script_source)
}
}
impl Drop for Tab {
fn drop(&mut self) {
info!("dropping tab");
info!("dropped tab");
}
}