use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use crate::error::{Result, ZendriverError};
use crate::tab::Tab;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WindowState {
Normal,
Minimized,
Maximized,
Fullscreen,
}
impl WindowState {
#[must_use]
pub fn as_cdp(&self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Minimized => "minimized",
Self::Maximized => "maximized",
Self::Fullscreen => "fullscreen",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WindowBounds {
pub left: Option<i64>,
pub top: Option<i64>,
pub width: Option<i64>,
pub height: Option<i64>,
pub state: Option<WindowState>,
}
impl WindowBounds {
fn to_cdp(&self) -> Value {
let mut bounds = Map::new();
if let Some(v) = self.left {
bounds.insert("left".to_string(), json!(v));
}
if let Some(v) = self.top {
bounds.insert("top".to_string(), json!(v));
}
if let Some(v) = self.width {
bounds.insert("width".to_string(), json!(v));
}
if let Some(v) = self.height {
bounds.insert("height".to_string(), json!(v));
}
if let Some(state) = self.state {
bounds.insert(
"windowState".to_string(),
Value::String(state.as_cdp().into()),
);
}
Value::Object(bounds)
}
fn from_cdp(v: &Value) -> Self {
Self {
left: v.get("left").and_then(Value::as_i64),
top: v.get("top").and_then(Value::as_i64),
width: v.get("width").and_then(Value::as_i64),
height: v.get("height").and_then(Value::as_i64),
state: v
.get("windowState")
.and_then(Value::as_str)
.and_then(|s| match s {
"normal" => Some(WindowState::Normal),
"minimized" => Some(WindowState::Minimized),
"maximized" => Some(WindowState::Maximized),
"fullscreen" => Some(WindowState::Fullscreen),
_ => None,
}),
}
}
}
impl Tab {
async fn window_id(&self) -> Result<i64> {
let target_id = self.target_id().to_string();
let res = self
.inner
.session
.connection()
.call_raw(
"Browser.getWindowForTarget",
json!({ "targetId": target_id }),
None,
)
.await?;
res.get("windowId").and_then(Value::as_i64).ok_or_else(|| {
ZendriverError::Navigation("Browser.getWindowForTarget returned no windowId".into())
})
}
pub async fn window_bounds(&self) -> Result<WindowBounds> {
let target_id = self.target_id().to_string();
let res = self
.inner
.session
.connection()
.call_raw(
"Browser.getWindowForTarget",
json!({ "targetId": target_id }),
None,
)
.await?;
let bounds = res.get("bounds").ok_or_else(|| {
ZendriverError::Navigation("Browser.getWindowForTarget returned no bounds".into())
})?;
Ok(WindowBounds::from_cdp(bounds))
}
pub async fn set_window_bounds(&self, bounds: WindowBounds) -> Result<()> {
let window_id = self.window_id().await?;
self.inner
.session
.connection()
.call_raw(
"Browser.setWindowBounds",
json!({ "windowId": window_id, "bounds": bounds.to_cdp() }),
None,
)
.await?;
Ok(())
}
pub async fn set_window_size(&self, width: i64, height: i64) -> Result<()> {
self.set_window_bounds(WindowBounds {
width: Some(width),
height: Some(height),
..Default::default()
})
.await
}
pub async fn maximize(&self) -> Result<()> {
self.set_window_state(WindowState::Maximized).await
}
pub async fn minimize(&self) -> Result<()> {
self.set_window_state(WindowState::Minimized).await
}
pub async fn fullscreen(&self) -> Result<()> {
self.set_window_state(WindowState::Fullscreen).await
}
async fn set_window_state(&self, state: WindowState) -> Result<()> {
self.set_window_bounds(WindowBounds {
state: Some(state),
..Default::default()
})
.await
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
use zendriver_transport::SessionHandle;
use zendriver_transport::testing::MockConnection;
#[tokio::test]
async fn window_bounds_dispatches_get_window_for_target() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S42");
let tab = Tab::new_for_test(sess);
let fut = tokio::spawn({
let t = tab.clone();
async move { t.window_bounds().await }
});
let id = mock.expect_cmd("Browser.getWindowForTarget").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["targetId"], "test-target-S42");
assert!(sent.get("sessionId").is_none());
mock.reply(
id,
json!({
"windowId": 7,
"bounds": { "left": 0, "top": 0, "width": 1280, "height": 800, "windowState": "normal" },
}),
)
.await;
let bounds = fut.await.unwrap().unwrap();
assert_eq!(bounds.width, Some(1280));
assert_eq!(bounds.height, Some(800));
assert_eq!(bounds.state, Some(WindowState::Normal));
conn.shutdown();
}
#[tokio::test]
async fn maximize_sends_only_window_state() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let fut = tokio::spawn({
let t = tab.clone();
async move { t.maximize().await }
});
let lookup_id = mock.expect_cmd("Browser.getWindowForTarget").await;
mock.reply(lookup_id, json!({ "windowId": 3, "bounds": {} }))
.await;
let set_id = mock.expect_cmd("Browser.setWindowBounds").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["windowId"], 3);
let bounds = &sent["params"]["bounds"];
assert_eq!(bounds["windowState"], "maximized");
assert!(bounds.get("width").is_none());
assert!(bounds.get("height").is_none());
assert!(bounds.get("left").is_none());
assert!(bounds.get("top").is_none());
assert!(sent.get("sessionId").is_none());
mock.reply(set_id, json!({})).await;
fut.await.unwrap().unwrap();
conn.shutdown();
}
#[tokio::test]
async fn set_window_size_sends_dimensions() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let fut = tokio::spawn({
let t = tab.clone();
async move { t.set_window_size(1024, 768).await }
});
let lookup_id = mock.expect_cmd("Browser.getWindowForTarget").await;
mock.reply(lookup_id, json!({ "windowId": 9, "bounds": {} }))
.await;
let set_id = mock.expect_cmd("Browser.setWindowBounds").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["windowId"], 9);
let bounds = &sent["params"]["bounds"];
assert_eq!(bounds["width"], 1024);
assert_eq!(bounds["height"], 768);
assert!(bounds.get("windowState").is_none());
mock.reply(set_id, json!({})).await;
fut.await.unwrap().unwrap();
conn.shutdown();
}
}