use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::{Rc, Weak};
use std::time::Duration;
use serde_json::Value;
use super::accessibility::Accessibility;
use super::browser_context::BrowserContext;
use super::coverage::Coverage;
use super::event_emitter::{EventEmitter, EventEmitterInner};
use super::frame::Frame;
use super::keyboard::Keyboard;
use super::mouse::Mouse;
use super::touchscreen::Touchscreen;
use super::tracing::Tracing;
use crate::api::browser::Browser as HighLevelBrowser;
use crate::connection::Connection;
use crate::error::CdpError;
#[derive(Debug, Clone)]
pub struct Worker {
pub target_id: String,
pub url: String,
pub type_str: String,
}
impl Worker {
pub fn new(
target_id: impl Into<String>,
url: impl Into<String>,
type_str: impl Into<String>,
) -> Self {
Self {
target_id: target_id.into(),
url: url.into(),
type_str: type_str.into(),
}
}
pub fn target_id(&self) -> &str {
&self.target_id
}
pub fn url(&self) -> &str {
&self.url
}
pub fn type_str(&self) -> &str {
&self.type_str
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Viewport {
pub width: u32,
pub height: u32,
pub device_scale_factor: f64,
pub is_mobile: bool,
pub has_touch: bool,
pub is_landscape: bool,
}
impl Default for Viewport {
fn default() -> Self {
Self {
width: 1280,
height: 720,
device_scale_factor: 1.0,
is_mobile: false,
has_touch: false,
is_landscape: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TargetInfo {
pub target_id: String,
pub type_str: String,
pub title: String,
pub url: String,
pub attached: bool,
pub opener_id: Option<String>,
pub browser_context_id: Option<String>,
}
pub struct Page {
target_id: String,
is_service_worker: Cell<bool>,
main_frame: Rc<Frame>,
frames_map: RefCell<HashMap<String, Rc<Frame>>>,
workers: RefCell<Vec<Worker>>,
viewport: RefCell<Option<Viewport>>,
target_info: RefCell<TargetInfo>,
mouse: Mouse,
keyboard: Keyboard,
touchscreen: Touchscreen,
coverage: Coverage,
tracing: Tracing,
accessibility: Accessibility,
closed: RefCell<bool>,
default_timeout: RefCell<Option<Duration>>,
default_nav_timeout: RefCell<Option<Duration>>,
opener: RefCell<Option<Weak<Page>>>,
context: Weak<BrowserContext>,
connection: RefCell<Option<Rc<RefCell<Connection>>>>,
events: Rc<EventEmitterInner>,
}
impl std::fmt::Debug for Page {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Page")
.field("target_id", &self.target_id)
.field("is_service_worker", &self.is_service_worker.get())
.field("frame_count", &self.frames_map.borrow().len())
.field("worker_count", &self.workers.borrow().len())
.field("closed", &self.closed.borrow())
.field("has_connection", &self.connection.borrow().is_some())
.finish()
}
}
impl Page {
pub fn new(target_id: impl Into<String>, context: Weak<BrowserContext>) -> Self {
Self::new_with_connection(target_id, context, None)
}
pub fn new_with_connection(
target_id: impl Into<String>,
context: Weak<BrowserContext>,
connection: Option<Rc<RefCell<Connection>>>,
) -> Self {
let target_id = target_id.into();
let ctx_clone = context.clone();
let main_frame = Rc::new(Frame::new("MAIN", true, Weak::new()));
let mut frames = HashMap::new();
frames.insert("MAIN".to_string(), main_frame.clone());
Self {
target_id,
is_service_worker: Cell::new(false),
main_frame,
frames_map: RefCell::new(frames),
workers: RefCell::new(Vec::new()),
viewport: RefCell::new(None),
target_info: RefCell::new(TargetInfo::default()),
mouse: Mouse::new(),
keyboard: Keyboard::new(),
touchscreen: Touchscreen::new(),
coverage: Coverage::new(),
tracing: Tracing::new(),
accessibility: Accessibility::new(),
closed: RefCell::new(false),
default_timeout: RefCell::new(None),
default_nav_timeout: RefCell::new(None),
opener: RefCell::new(None),
context: ctx_clone,
connection: RefCell::new(connection),
events: Rc::new(EventEmitterInner::new()),
}
}
pub fn is_closed(&self) -> bool {
*self.closed.borrow()
}
pub fn set_closed(&self, v: bool) {
*self.closed.borrow_mut() = v;
}
pub fn target_id(&self) -> &str {
&self.target_id
}
pub fn is_service_worker(&self) -> bool {
self.is_service_worker.get()
}
pub fn set_is_service_worker(&self, v: bool) {
self.is_service_worker.set(v);
}
pub fn main_frame(&self) -> Rc<Frame> {
self.main_frame.clone()
}
pub fn frames(&self) -> Vec<Rc<Frame>> {
self.frames_map.borrow().values().cloned().collect()
}
pub fn frames_count(&self) -> usize {
self.frames_map.borrow().len()
}
pub fn frame_by_id(&self, id: &str) -> Option<Rc<Frame>> {
self.frames_map.borrow().get(id).cloned()
}
pub fn add_frame(&self, id: impl Into<String>, frame: Rc<Frame>) {
self.frames_map.borrow_mut().insert(id.into(), frame);
}
pub fn remove_frame(&self, id: &str) {
self.frames_map.borrow_mut().remove(id);
}
pub fn workers(&self) -> Vec<Worker> {
self.workers.borrow().clone()
}
pub fn workers_count(&self) -> usize {
self.workers.borrow().len()
}
pub fn add_worker(&self, w: Worker) {
self.workers.borrow_mut().push(w);
}
pub fn viewport(&self) -> Option<Viewport> {
*self.viewport.borrow()
}
pub fn viewport_size(&self) -> Option<(u32, u32)> {
self.viewport.borrow().map(|v| (v.width, v.height))
}
pub fn set_viewport(&self, v: Viewport) {
*self.viewport.borrow_mut() = Some(v);
}
pub fn target_info(&self) -> TargetInfo {
self.target_info.borrow().clone()
}
pub fn set_target_info(&self, info: TargetInfo) {
*self.target_info.borrow_mut() = info;
}
pub fn target(&self) -> std::cell::Ref<'_, TargetInfo> {
self.target_info.borrow()
}
pub fn mouse(&self) -> &Mouse {
&self.mouse
}
pub fn keyboard(&self) -> &Keyboard {
&self.keyboard
}
pub fn touchscreen(&self) -> &Touchscreen {
&self.touchscreen
}
pub fn coverage(&self) -> &Coverage {
&self.coverage
}
pub fn tracing(&self) -> &Tracing {
&self.tracing
}
pub fn accessibility(&self) -> &Accessibility {
&self.accessibility
}
pub fn default_timeout(&self) -> Option<Duration> {
*self.default_timeout.borrow()
}
pub fn default_navigation_timeout(&self) -> Option<Duration> {
*self.default_nav_timeout.borrow()
}
pub fn set_default_timeout(&self, ms: u64) {
*self.default_timeout.borrow_mut() = Some(Duration::from_millis(ms));
}
pub fn set_default_navigation_timeout(&self, ms: u64) {
*self.default_nav_timeout.borrow_mut() = Some(Duration::from_millis(ms));
}
pub fn clear_default_timeout(&self) {
*self.default_timeout.borrow_mut() = None;
}
pub fn clear_default_navigation_timeout(&self) {
*self.default_nav_timeout.borrow_mut() = None;
}
pub fn opener(&self) -> Option<Rc<Page>> {
self.opener.borrow().as_ref().and_then(|w| w.upgrade())
}
pub fn set_opener(&self, opener: Weak<Page>) {
*self.opener.borrow_mut() = Some(opener);
}
pub fn browser_context(&self) -> Option<Rc<BrowserContext>> {
self.context.upgrade()
}
pub fn browser(&self) -> Option<Rc<HighLevelBrowser>> {
self.context.upgrade().map(|c| c.browser())
}
pub fn url(&self) -> String {
self.main_frame.url()
}
pub fn set_url(&self, url: impl Into<String>) {
self.main_frame.set_url(url);
}
pub fn events_inner(&self) -> &Rc<EventEmitterInner> {
&self.events
}
pub fn has_connection(&self) -> bool {
self.connection.borrow().is_some()
}
pub fn set_connection(&self, conn: Rc<RefCell<Connection>>) {
*self.connection.borrow_mut() = Some(conn);
}
pub fn goto(&self, url: &str) -> crate::error::Result<Value> {
self.send_cdp_command("Page.navigate", serde_json::json!({"url": url}))
}
pub fn evaluate(&self, expression: &str) -> crate::error::Result<Value> {
self.send_cdp_command(
"Runtime.evaluate",
serde_json::json!({
"expression": expression,
"returnByValue": true,
}),
)
}
pub fn screenshot(&self) -> crate::error::Result<Value> {
self.send_cdp_command(
"Page.captureScreenshot",
serde_json::json!({"format": "png"}),
)
}
pub fn screenshot_with_format(&self, format: &str) -> crate::error::Result<Value> {
self.send_cdp_command(
"Page.captureScreenshot",
serde_json::json!({"format": format}),
)
}
pub fn close(&self) -> crate::error::Result<Value> {
let result = self.send_cdp_command("Page.close", serde_json::json!({}));
if result.is_ok() {
*self.closed.borrow_mut() = true;
}
result
}
pub fn title(&self) -> crate::error::Result<String> {
let result = self.evaluate("document.title")?;
Ok(result
.get("result")
.and_then(|r| r.get("value"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string())
}
pub fn set_viewport_cdp(&self, viewport: Viewport) -> crate::error::Result<Value> {
let result = self.send_cdp_command(
"Emulation.setDeviceMetricsOverride",
serde_json::json!({
"width": viewport.width,
"height": viewport.height,
"deviceScaleFactor": viewport.device_scale_factor,
"mobile": viewport.is_mobile,
}),
);
if result.is_ok() {
*self.viewport.borrow_mut() = Some(viewport);
}
result
}
pub fn send_cdp_command(&self, method: &str, params: Value) -> crate::error::Result<Value> {
match &*self.connection.borrow() {
Some(conn) => conn.borrow_mut().send_command(method, params),
None => Err(CdpError::ConnectionClosed),
}
}
pub fn send_cdp_command_with_session(
&self,
method: &str,
params: Value,
session_id: &str,
) -> crate::error::Result<Value> {
match &*self.connection.borrow() {
Some(conn) => {
conn.borrow_mut()
.send_command_with_session(method, params, Some(session_id))
}
None => Err(CdpError::ConnectionClosed),
}
}
pub fn recv_cdp_event(&self) -> crate::error::Result<Option<crate::transport::CdpEvent>> {
match &*self.connection.borrow() {
Some(conn) => conn.borrow_mut().recv_event(),
None => Err(CdpError::ConnectionClosed),
}
}
}
impl EventEmitter for Page {
delegate_event_emitter!(self, events);
}
#[cfg(test)]
mod tests {
use super::super::event_emitter::EventHandler;
use super::*;
fn make_browser_ctx() -> Rc<BrowserContext> {
let browser = Rc::new(HighLevelBrowser::new_for_test("ws://x"));
HighLevelBrowser::new_context_for_test(&browser)
}
fn make_page_with_ctx(ctx: Rc<BrowserContext>) -> Rc<Page> {
Rc::new(Page::new("TARGET-1", Rc::downgrade(&ctx)))
}
#[test]
fn new_page_initial_state() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert_eq!(p.target_id(), "TARGET-1");
assert!(!p.is_closed());
assert!(!p.is_service_worker());
assert_eq!(p.frames_count(), 1);
assert_eq!(p.workers_count(), 0);
assert!(p.viewport().is_none());
assert!(p.default_timeout().is_none());
assert!(p.default_navigation_timeout().is_none());
assert!(p.opener().is_none());
assert!(!p.has_connection());
}
#[test]
fn is_closed_flag() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
p.set_closed(true);
assert!(p.is_closed());
}
#[test]
fn main_frame_is_first_frame() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let mf = p.main_frame();
assert!(mf.is_main_frame());
assert_eq!(p.frames().len(), 1);
}
#[test]
fn add_remove_frames() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let f2 = Rc::new(Frame::new("F2", false, Rc::downgrade(&p)));
p.add_frame("F2", f2);
assert_eq!(p.frames_count(), 2);
assert!(p.frame_by_id("F2").is_some());
p.remove_frame("F2");
assert_eq!(p.frames_count(), 1);
assert!(p.frame_by_id("F2").is_none());
}
#[test]
fn add_workers() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
p.add_worker(Worker::new("W1", "https://example.com/w.js", "worker"));
assert_eq!(p.workers_count(), 1);
assert_eq!(p.workers()[0].target_id(), "W1");
}
#[test]
fn viewport_round_trip() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert!(p.viewport().is_none());
p.set_viewport(Viewport {
width: 1920,
height: 1080,
..Default::default()
});
let v = p.viewport().unwrap();
assert_eq!(v.width, 1920);
assert_eq!(v.height, 1080);
assert_eq!(p.viewport_size(), Some((1920, 1080)));
}
#[test]
fn default_timeouts() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
p.set_default_timeout(5000);
p.set_default_navigation_timeout(10000);
assert_eq!(p.default_timeout(), Some(Duration::from_millis(5000)));
assert_eq!(
p.default_navigation_timeout(),
Some(Duration::from_millis(10000))
);
p.clear_default_timeout();
assert!(p.default_timeout().is_none());
}
#[test]
fn target_info_round_trip() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let info = TargetInfo {
target_id: "TARGET-1".into(),
type_str: "page".into(),
title: "Hello".into(),
url: "https://example.com".into(),
attached: true,
opener_id: None,
browser_context_id: None,
};
p.set_target_info(info);
let got = p.target_info();
assert_eq!(got.title, "Hello");
assert_eq!(got.url, "https://example.com");
let r = p.target();
assert_eq!(r.type_str, "page");
}
#[test]
fn input_devices_local() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
p.mouse().set_position(10.0, 20.0);
assert_eq!(p.mouse().current_x(), 10.0);
p.keyboard().set_modifier(1, true); assert!(p.keyboard().is_shift_pressed());
p.touchscreen()
.add_touch(crate::api::touchscreen::TouchPoint::default());
assert_eq!(p.touchscreen().touch_count(), 1);
}
#[test]
fn coverage_local() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert!(!p.coverage().is_started());
p.coverage().set_js_started(true);
assert!(p.coverage().is_started());
}
#[test]
fn tracing_local() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert!(!p.tracing().is_started());
p.tracing().set_started(true);
assert!(p.tracing().is_started());
}
#[test]
fn accessibility_local() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert!(!p.accessibility().has_snapshot());
p.accessibility()
.add_node(crate::api::accessibility::AXNode {
node_id: "1".into(),
role: "button".into(),
..Default::default()
});
assert_eq!(p.accessibility().node_count(), 1);
}
#[test]
fn opener_link() {
let ctx = make_browser_ctx();
let opener = make_page_with_ctx(ctx.clone());
let child = Rc::new(Page::new("TARGET-2", Rc::downgrade(&ctx)));
child.set_opener(Rc::downgrade(&opener));
assert_eq!(
child.opener().map(|p| p.target_id().to_string()),
Some("TARGET-1".into())
);
}
#[test]
fn browser_context_lookup() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx.clone());
let got = p.browser_context();
assert!(got.is_some());
assert!(Rc::ptr_eq(&got.unwrap(), &ctx));
}
#[test]
fn url_delegates_to_main_frame() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert_eq!(p.url(), "");
p.set_url("https://example.com");
assert_eq!(p.url(), "https://example.com");
}
#[test]
fn event_emitter_via_trait() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let c = counter.clone();
let h: EventHandler = std::sync::Arc::new(move |_| {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
});
p.on("load", h);
p.emit("load", &[]);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(p.listener_count("load"), 1);
}
#[test]
fn event_emitter_off() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let h: EventHandler = std::sync::Arc::new(|_| {});
let id = p.on("x", h);
assert_eq!(p.listener_count("x"), 1);
p.off("x", id);
assert_eq!(p.listener_count("x"), 0);
}
#[test]
fn event_emitter_remove_all() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let h1: EventHandler = std::sync::Arc::new(|_| {});
let h2: EventHandler = std::sync::Arc::new(|_| {});
p.on("a", h1);
p.on("b", h2);
p.remove_all_listeners(Some("a"));
assert_eq!(p.listener_count("a"), 0);
assert_eq!(p.listener_count("b"), 1);
p.remove_all_listeners(None);
assert_eq!(p.listener_count("b"), 0);
}
#[test]
fn cdp_command_without_connection_returns_error() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let err = p.goto("https://example.com").unwrap_err();
assert!(matches!(err, CdpError::ConnectionClosed));
}
#[test]
fn cdp_evaluate_without_connection_returns_error() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let err = p.evaluate("1+1").unwrap_err();
assert!(matches!(err, CdpError::ConnectionClosed));
}
#[test]
fn cdp_screenshot_without_connection_returns_error() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let err = p.screenshot().unwrap_err();
assert!(matches!(err, CdpError::ConnectionClosed));
}
#[test]
fn cdp_close_without_connection_returns_error() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
let err = p.close().unwrap_err();
assert!(matches!(err, CdpError::ConnectionClosed));
assert!(!p.is_closed());
}
#[test]
fn page_with_connection_has_connection() {
let ctx = make_browser_ctx();
use crate::transport::{InMemoryBridge, InMemoryBridgeResponse, InMemoryTransport};
use std::sync::Arc;
struct MockBridge;
impl InMemoryBridge for MockBridge {
fn dispatch_command(
&self,
_m: &str,
_p: Value,
_s: Option<&str>,
) -> InMemoryBridgeResponse {
InMemoryBridgeResponse::Ok(serde_json::json!({"result": 42}))
}
}
let bridge: Arc<dyn InMemoryBridge> = Arc::new(MockBridge);
let transport = InMemoryTransport::new(bridge);
let conn = Rc::new(RefCell::new(Connection::from_transport(Box::new(
transport,
))));
let p = Rc::new(Page::new_with_connection(
"TARGET-1",
Rc::downgrade(&ctx),
Some(conn),
));
assert!(p.has_connection());
}
#[test]
fn set_connection_on_existing_page() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert!(!p.has_connection());
use crate::transport::{InMemoryBridge, InMemoryBridgeResponse, InMemoryTransport};
use std::sync::Arc;
struct MockBridge;
impl InMemoryBridge for MockBridge {
fn dispatch_command(
&self,
_m: &str,
_p: Value,
_s: Option<&str>,
) -> InMemoryBridgeResponse {
InMemoryBridgeResponse::Ok(Value::Null)
}
}
let bridge: Arc<dyn InMemoryBridge> = Arc::new(MockBridge);
let transport = InMemoryTransport::new(bridge);
let conn = Rc::new(RefCell::new(Connection::from_transport(Box::new(
transport,
))));
p.set_connection(conn);
assert!(p.has_connection());
}
#[test]
fn set_is_service_worker_actually_sets_value() {
let ctx = make_browser_ctx();
let p = make_page_with_ctx(ctx);
assert!(!p.is_service_worker());
p.set_is_service_worker(true);
assert!(p.is_service_worker());
p.set_is_service_worker(false);
assert!(!p.is_service_worker());
}
}