Skip to content

Commit eb396df

Browse files
committed
Add support for Chrome Devtools Protocol
1 parent b555a7d commit eb396df

5 files changed

Lines changed: 391 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ For synchronous support, use the [thirtyfour_sync](https://docs.rs/thirtyfour_sy
2525
- Shadow DOM support
2626
- Alert support
2727
- Capture / Save screenshot of browser or individual element as PNG
28+
- Chrome DevTools Protocol (CDP) support
2829

2930
## Why 'thirtyfour' ?
3031

examples/chrome_devtools.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Requires chromedriver running on port 4444:
2+
//!
3+
//! chromedriver --port=4444
4+
//!
5+
//! Run as follows:
6+
//!
7+
//! cargo run --example chrome_devtools
8+
9+
use thirtyfour::extensions::chrome::{ChromeDevTools, NetworkConditions};
10+
use thirtyfour::prelude::*;
11+
12+
#[tokio::main]
13+
async fn main() -> color_eyre::Result<()> {
14+
// The use of color_eyre gives much nicer error reports, including making
15+
// it much easier to locate where the error occurred.
16+
color_eyre::install()?;
17+
18+
let caps = DesiredCapabilities::chrome();
19+
let driver = WebDriver::new("http://localhost:4444", &caps).await?;
20+
21+
// Use Chrome Devtools Protocol (CDP).
22+
let dev_tools = ChromeDevTools::new(driver.session());
23+
let mut conditions = NetworkConditions::new();
24+
conditions.download_throughput = 20;
25+
conditions.upload_throughput = 10;
26+
dev_tools.set_network_conditions(&conditions).await?;
27+
let conditions = dev_tools.get_network_conditions().await?;
28+
assert_eq!(conditions.download_throughput, 20);
29+
assert_eq!(conditions.upload_throughput, 10);
30+
println!("Conditions: {:?}", conditions);
31+
32+
// Execute CDP command.
33+
let version_info = dev_tools.execute_cdp("Browser.getVersion").await?;
34+
println!("Chrome Version: {:?}", version_info);
35+
36+
Ok(())
37+
}

src/extensions/chrome/devtools.rs

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
use crate::common::command::FormatRequestData;
2+
use crate::common::connection_common::convert_json;
3+
use crate::error::WebDriverResult;
4+
use crate::extensions::chrome::networkconditions::NetworkConditions;
5+
use crate::{RequestData, RequestMethod, SessionId, WebDriverSession};
6+
use serde_json::{json, Value};
7+
8+
pub enum ChromeCommand {
9+
LaunchApp(String),
10+
GetNetworkConditions,
11+
SetNetworkConditions(NetworkConditions),
12+
ExecuteCdpCommand(String, Value),
13+
GetSinks,
14+
GetIssueMessage,
15+
SetSinkToUse(String),
16+
StartTabMirroring(String),
17+
StopCasting(String),
18+
}
19+
20+
impl FormatRequestData for ChromeCommand {
21+
fn format_request(&self, session_id: &SessionId) -> RequestData {
22+
match self {
23+
ChromeCommand::LaunchApp(app_id) => RequestData::new(
24+
RequestMethod::Post,
25+
format!("/session/{}/chromium/launch_app", session_id),
26+
)
27+
.add_body(json!({ "id": app_id })),
28+
ChromeCommand::GetNetworkConditions => RequestData::new(
29+
RequestMethod::Get,
30+
format!("/session/{}/chromium/network_conditions", session_id),
31+
),
32+
ChromeCommand::SetNetworkConditions(conditions) => RequestData::new(
33+
RequestMethod::Post,
34+
format!("/session/{}/chromium/network_conditions", session_id),
35+
)
36+
.add_body(json!({ "network_conditions": conditions })),
37+
ChromeCommand::ExecuteCdpCommand(command, params) => RequestData::new(
38+
RequestMethod::Post,
39+
format!("/session/{}/goog/cdp/execute", session_id),
40+
)
41+
.add_body(json!({ "cmd": command, "params": params })),
42+
ChromeCommand::GetSinks => RequestData::new(
43+
RequestMethod::Get,
44+
format!("/session/{}/goog/cast/get_sinks", session_id),
45+
),
46+
ChromeCommand::GetIssueMessage => RequestData::new(
47+
RequestMethod::Get,
48+
format!("/session/{}/goog/cast/get_issue_message", session_id),
49+
),
50+
ChromeCommand::SetSinkToUse(sink_name) => RequestData::new(
51+
RequestMethod::Post,
52+
format!("/session/{}/goog/cast/set_sink_to_use", session_id),
53+
)
54+
.add_body(json!({ "sinkName": sink_name })),
55+
ChromeCommand::StartTabMirroring(sink_name) => RequestData::new(
56+
RequestMethod::Post,
57+
format!("/session/{}/goog/cast/start_tab_mirroring", session_id),
58+
)
59+
.add_body(json!({ "sinkName": sink_name })),
60+
ChromeCommand::StopCasting(sink_name) => RequestData::new(
61+
RequestMethod::Post,
62+
format!("/session/{}/goog/cast/stop_casting", session_id),
63+
)
64+
.add_body(json!({ "sinkName": sink_name })),
65+
}
66+
}
67+
}
68+
69+
/// The ChromeDevTools struct allows you to interact with Chromium-based browsers via
70+
/// the Chrome Devtools Protocol (CDP).
71+
///
72+
/// You can find documentation for the available commands here:
73+
/// [https://chromedevtools.github.io/devtools-protocol/](https://chromedevtools.github.io/devtools-protocol/])
74+
///
75+
/// # Example
76+
/// ```rust
77+
/// # use thirtyfour::prelude::*;
78+
/// # use thirtyfour::support::block_on;
79+
/// # use thirtyfour::extensions::chrome::ChromeDevTools;
80+
/// #
81+
/// # fn main() -> WebDriverResult<()> {
82+
/// # block_on(async {
83+
/// let caps = DesiredCapabilities::chrome();
84+
/// let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;
85+
///
86+
/// // Create a ChromeDevTools struct like this.
87+
/// let dev_tools = ChromeDevTools::new(driver.session());
88+
/// dev_tools.execute_cdp("Network.clearBrowserCache").await?;
89+
/// # Ok(())
90+
/// # })
91+
/// # }
92+
/// ```
93+
#[derive(Debug, Clone)]
94+
pub struct ChromeDevTools<'a> {
95+
pub session: &'a WebDriverSession,
96+
}
97+
98+
impl<'a> ChromeDevTools<'a> {
99+
/// Create a new ChromeDevTools struct.
100+
///
101+
/// # Example:
102+
/// ```rust
103+
/// # use thirtyfour::prelude::*;
104+
/// # use thirtyfour::support::block_on;
105+
/// use thirtyfour::extensions::chrome::ChromeDevTools;
106+
/// #
107+
/// # fn main() -> WebDriverResult<()> {
108+
/// # block_on(async {
109+
/// # let caps = DesiredCapabilities::chrome();
110+
/// # let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;
111+
/// let dev_tools = ChromeDevTools::new(driver.session());
112+
/// # Ok(())
113+
/// # })
114+
/// # }
115+
/// ```
116+
pub fn new(session: &'a WebDriverSession) -> Self {
117+
Self {
118+
session,
119+
}
120+
}
121+
122+
/// Convenience method to execute a ChromeCommand.
123+
async fn cmd(&self, command: ChromeCommand) -> WebDriverResult<serde_json::Value> {
124+
self.session.execute(Box::new(command)).await
125+
}
126+
127+
/// Launch the Chrome app with the specified id.
128+
pub async fn launch_app(&self, app_id: &str) -> WebDriverResult<()> {
129+
self.cmd(ChromeCommand::LaunchApp(app_id.to_string())).await?;
130+
Ok(())
131+
}
132+
133+
/// Get the current network conditions. You must set the conditions first.
134+
///
135+
/// # Example:
136+
/// ```rust
137+
/// # use thirtyfour::prelude::*;
138+
/// # use thirtyfour::support::block_on;
139+
/// use thirtyfour::extensions::chrome::{ChromeDevTools, NetworkConditions};
140+
/// #
141+
/// # fn main() -> WebDriverResult<()> {
142+
/// # block_on(async {
143+
/// # let caps = DesiredCapabilities::chrome();
144+
/// # let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;
145+
/// // Create ChromeDevTools struct.
146+
/// let dev_tools = ChromeDevTools::new(driver.session());
147+
///
148+
/// // First we need to set the network conditions.
149+
/// let mut conditions = NetworkConditions::new();
150+
/// conditions.download_throughput = 20;
151+
/// dev_tools.set_network_conditions(&conditions).await?;
152+
///
153+
/// // Now we can get the network conditions.
154+
/// let conditions_out = dev_tools.get_network_conditions().await?;
155+
/// assert_eq!(conditions_out.download_throughput, conditions.download_throughput);
156+
/// # Ok(())
157+
/// # })
158+
/// # }
159+
/// ```
160+
pub async fn get_network_conditions(&self) -> WebDriverResult<NetworkConditions> {
161+
let v = self.cmd(ChromeCommand::GetNetworkConditions).await?;
162+
convert_json(&v["value"])
163+
}
164+
165+
/// Set the network conditions.
166+
///
167+
/// # Example:
168+
/// ```rust
169+
/// # use thirtyfour::prelude::*;
170+
/// # use thirtyfour::support::block_on;
171+
/// use thirtyfour::extensions::chrome::{ChromeDevTools, NetworkConditions};
172+
///
173+
/// # fn main() -> WebDriverResult<()> {
174+
/// # block_on(async {
175+
/// # let caps = DesiredCapabilities::chrome();
176+
/// # let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;
177+
/// // Create ChromeDevTools struct.
178+
/// let dev_tools = ChromeDevTools::new(driver.session());
179+
///
180+
/// // Now we can set the network conditions. You do not need to set all parameters.
181+
/// let mut conditions = NetworkConditions::new();
182+
/// conditions.download_throughput = 20;
183+
/// conditions.upload_throughput = 10;
184+
/// conditions.offline = false;
185+
/// conditions.latency = 200;
186+
///
187+
/// dev_tools.set_network_conditions(&conditions).await?;
188+
/// # let conditions_out = dev_tools.get_network_conditions().await?;
189+
/// # assert_eq!(conditions_out.download_throughput, conditions.download_throughput);
190+
/// # assert_eq!(conditions_out.upload_throughput, conditions.upload_throughput);
191+
/// # assert_eq!(conditions_out.latency, conditions.latency);
192+
/// # assert_eq!(conditions_out.offline, conditions.offline);
193+
/// # Ok(())
194+
/// # })
195+
/// # }
196+
/// ```
197+
pub async fn set_network_conditions(
198+
&self,
199+
conditions: &NetworkConditions,
200+
) -> WebDriverResult<()> {
201+
self.cmd(ChromeCommand::SetNetworkConditions(conditions.clone())).await?;
202+
Ok(())
203+
}
204+
205+
/// Execute the specified command without parameters.
206+
/// For commands that require parameters, use `execute_cdp_with_params()` instead.
207+
///
208+
/// You can find documentation for the available commands here:
209+
/// [https://chromedevtools.github.io/devtools-protocol/](https://chromedevtools.github.io/devtools-protocol/])
210+
///
211+
/// # Example:
212+
/// ```rust
213+
/// # use thirtyfour::prelude::*;
214+
/// # use thirtyfour::support::block_on;
215+
/// use thirtyfour::extensions::chrome::ChromeDevTools;
216+
///
217+
/// #
218+
/// # fn main() -> WebDriverResult<()> {
219+
/// # block_on(async {
220+
/// # let caps = DesiredCapabilities::chrome();
221+
/// # let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;
222+
/// let dev_tools = ChromeDevTools::new(driver.session());
223+
/// dev_tools.execute_cdp("Network.clearBrowserCache").await?;
224+
///
225+
/// // execute_cdp() can also return values as well.
226+
/// let version_info = dev_tools.execute_cdp("Browser.getVersion").await?;
227+
/// let user_agent = version_info["userAgent"].as_str().unwrap();
228+
/// # Ok(())
229+
/// # })
230+
/// # }
231+
/// ```
232+
pub async fn execute_cdp(&self, cmd: &str) -> WebDriverResult<Value> {
233+
self.execute_cdp_with_params(cmd, json!({})).await
234+
}
235+
236+
/// Execute the specified command with the specified parameter(s).
237+
///
238+
/// You can find documentation for the available commands here:
239+
/// [https://chromedevtools.github.io/devtools-protocol/](https://chromedevtools.github.io/devtools-protocol/])
240+
///
241+
/// # Example:
242+
/// ```rust
243+
/// # use thirtyfour::prelude::*;
244+
/// # use thirtyfour::support::block_on;
245+
/// use thirtyfour::extensions::chrome::ChromeDevTools;
246+
/// use serde_json::json;
247+
/// #
248+
/// # fn main() -> WebDriverResult<()> {
249+
/// # block_on(async {
250+
/// # let caps = DesiredCapabilities::chrome();
251+
/// # let driver = WebDriver::new("http://localhost:4444/wd/hub", &caps).await?;
252+
/// let dev_tools = ChromeDevTools::new(driver.session());
253+
/// dev_tools.execute_cdp_with_params("Network.setCacheDisabled", json!({"cacheDisabled": true})).await?;
254+
/// # Ok(())
255+
/// # })
256+
/// # }
257+
/// ```
258+
pub async fn execute_cdp_with_params(
259+
&self,
260+
cmd: &str,
261+
cmd_args: Value,
262+
) -> WebDriverResult<Value> {
263+
let v = self.cmd(ChromeCommand::ExecuteCdpCommand(cmd.to_string(), cmd_args)).await?;
264+
Ok(v["value"].clone())
265+
}
266+
267+
/// Get the list of sinks available for cast.
268+
pub async fn get_sinks(&self) -> WebDriverResult<Value> {
269+
let v = self.cmd(ChromeCommand::GetSinks).await?;
270+
Ok(v["value"].clone())
271+
}
272+
273+
/// Get the issue message for any issue in a cast session.
274+
pub async fn get_issue_message(&self) -> WebDriverResult<Value> {
275+
let v = self.cmd(ChromeCommand::GetIssueMessage).await?;
276+
Ok(v["value"].clone())
277+
}
278+
279+
/// Set the specified sink as the cast session receiver target.
280+
pub async fn set_sink_to_use(&self, sink_name: &str) -> WebDriverResult<()> {
281+
self.cmd(ChromeCommand::SetSinkToUse(sink_name.to_string())).await?;
282+
Ok(())
283+
}
284+
285+
/// Start a tab mirroring session on the specified receiver target.
286+
pub async fn start_tab_mirroring(&self, sink_name: &str) -> WebDriverResult<()> {
287+
self.cmd(ChromeCommand::StartTabMirroring(sink_name.to_string())).await?;
288+
Ok(())
289+
}
290+
291+
/// Stop the existing cast session on the specified receiver target.
292+
pub async fn stop_casting(&self, sink_name: &str) -> WebDriverResult<()> {
293+
self.cmd(ChromeCommand::StopCasting(sink_name.to_string())).await?;
294+
Ok(())
295+
}
296+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4+
#[serde(rename = "lowercase")]
5+
pub enum ConnectionType {
6+
None,
7+
Cellular2G,
8+
Cellular3G,
9+
Cellular4G,
10+
Bluetooth,
11+
Ethernet,
12+
Wifi,
13+
Wimax,
14+
Other,
15+
}
16+
17+
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
18+
#[serde(rename = "camelCase")]
19+
pub struct NetworkConditions {
20+
pub offline: bool,
21+
pub latency: u32,
22+
pub download_throughput: i32,
23+
pub upload_throughput: i32,
24+
#[serde(skip_serializing_if = "Option::is_none")]
25+
pub connection_type: Option<ConnectionType>,
26+
}
27+
28+
impl Default for NetworkConditions {
29+
fn default() -> Self {
30+
Self {
31+
offline: false,
32+
latency: 0,
33+
download_throughput: -1,
34+
upload_throughput: -1,
35+
connection_type: None,
36+
}
37+
}
38+
}
39+
40+
impl NetworkConditions {
41+
pub fn new() -> Self {
42+
Self::default()
43+
}
44+
}

0 commit comments

Comments
 (0)