playwright_rs/protocol/browser_type.rs
1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// BrowserType - Represents a browser type (Chromium, Firefox, WebKit)
5//
6// Reference:
7// - Python: playwright-python/playwright/_impl/_browser_type.py
8// - Protocol: protocol.yml (BrowserType interface)
9
10use crate::api::{ConnectOptions, ConnectOverCdpOptions, LaunchOptions};
11use crate::error::Result;
12use crate::protocol::har_options::OptionHar;
13use crate::protocol::{Browser, BrowserContext, BrowserContextOptions};
14use crate::server::channel::Channel;
15use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
16use crate::server::connection::{ConnectionExt, ConnectionLike};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::any::Any;
20use std::sync::Arc;
21
22/// BrowserType represents a browser engine (Chromium, Firefox, or WebKit).
23///
24/// Each Playwright instance provides three BrowserType objects accessible via:
25/// - `playwright.chromium()`
26/// - `playwright.firefox()`
27/// - `playwright.webkit()`
28///
29/// BrowserType provides three main modes:
30/// 1. **Launch**: Creates a new browser instance
31/// 2. **Launch Persistent Context**: Creates browser + context with persistent storage
32/// 3. **Connect**: Connects to an existing remote browser instance
33///
34/// # Example
35///
36/// ```no_run
37/// # use playwright_rs::protocol::Playwright;
38/// # use playwright_rs::api::LaunchOptions;
39/// # use playwright_rs::protocol::BrowserContextOptions;
40/// # #[tokio::main]
41/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
42/// let playwright = Playwright::launch().await?;
43/// let chromium = playwright.chromium();
44///
45/// // Verify browser type info
46/// assert_eq!(chromium.name(), "chromium");
47/// assert!(!chromium.executable_path().is_empty());
48///
49/// // === Standard Launch ===
50/// // Launch with default options
51/// let browser1 = chromium.launch().await?;
52/// assert_eq!(browser1.name(), "chromium");
53/// assert!(!browser1.version().is_empty());
54/// browser1.close().await?;
55///
56/// // === Remote Connection ===
57/// // Connect to a remote browser (e.g., started with `npx playwright launch-server`)
58/// // let browser3 = chromium.connect("ws://localhost:3000", None).await?;
59/// // browser3.close().await?;
60///
61/// // === CDP Connection (Chromium only) ===
62/// // Connect to a Chrome instance with remote debugging enabled
63/// // let browser4 = chromium.connect_over_cdp("http://localhost:9222", None).await?;
64/// // browser4.close().await?;
65///
66/// // === Persistent Context Launch ===
67/// // Launch with persistent storage (cookies, local storage, etc.)
68/// let context = chromium
69/// .launch_persistent_context("/tmp/user-data")
70/// .await?;
71/// let page = context.new_page().await?;
72/// page.goto("https://example.com", None).await?;
73/// context.close().await?; // Closes browser too
74///
75/// // === App Mode (Standalone Window) ===
76/// // Launch as a standalone application window
77/// let app_options = BrowserContextOptions::builder()
78/// .args(vec!["--app=https://example.com".to_string()])
79/// .headless(true) // Set to true for CI, but app mode is typically headed
80/// .build();
81///
82/// let app_context = chromium
83/// .launch_persistent_context_with_options("/tmp/app-data", app_options)
84/// .await?;
85/// // Browser opens directly to URL without address bar
86/// app_context.close().await?;
87/// # Ok(())
88/// # }
89/// ```
90///
91/// See: <https://playwright.dev/docs/api/class-browsertype>
92#[derive(Clone)]
93pub struct BrowserType {
94 /// Base ChannelOwner implementation
95 base: ChannelOwnerImpl,
96 /// Browser name ("chromium", "firefox", or "webkit")
97 name: String,
98 /// Path to browser executable
99 executable_path: String,
100}
101
102impl BrowserType {
103 /// Creates a new BrowserType object from protocol initialization.
104 ///
105 /// Called by the object factory when server sends __create__ message.
106 ///
107 /// # Arguments
108 /// * `parent` - Parent (Connection for root objects, or another ChannelOwner)
109 /// * `type_name` - Protocol type name ("BrowserType")
110 /// * `guid` - Unique GUID from server (e.g., "browserType@chromium")
111 /// * `initializer` - Initial state with name and executablePath
112 pub fn new(
113 parent: ParentOrConnection,
114 type_name: String,
115 guid: Arc<str>,
116 initializer: Value,
117 ) -> Result<Self> {
118 let base = ChannelOwnerImpl::new(parent, type_name, guid, initializer.clone());
119
120 // Extract fields from initializer
121 let name = initializer["name"]
122 .as_str()
123 .ok_or_else(|| {
124 crate::error::Error::ProtocolError(
125 "BrowserType initializer missing 'name'".to_string(),
126 )
127 })?
128 .to_string();
129
130 let executable_path = initializer["executablePath"]
131 .as_str()
132 .unwrap_or_default() // executablePath might be optional/empty for remote connection objects
133 .to_string();
134
135 Ok(Self {
136 base,
137 name,
138 executable_path,
139 })
140 }
141
142 /// Returns the browser name ("chromium", "firefox", or "webkit").
143 pub fn name(&self) -> &str {
144 &self.name
145 }
146
147 /// Returns the path to the browser executable.
148 pub fn executable_path(&self) -> &str {
149 &self.executable_path
150 }
151
152 /// Launches a browser instance with default options.
153 ///
154 /// This is equivalent to calling `launch_with_options(LaunchOptions::default())`.
155 ///
156 /// # Errors
157 ///
158 /// Returns error if:
159 /// - Browser executable not found
160 /// - Launch timeout (default three minutes, [`DEFAULT_LAUNCH_TIMEOUT_MS`](crate::DEFAULT_LAUNCH_TIMEOUT_MS))
161 /// - Browser process fails to start
162 ///
163 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
164 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
165 pub async fn launch(&self) -> Result<Browser> {
166 self.launch_with_options(LaunchOptions::default()).await
167 }
168
169 /// Launches a browser instance with custom options.
170 ///
171 /// # Arguments
172 ///
173 /// * `options` - Launch options (headless, args, etc.)
174 ///
175 /// # Errors
176 ///
177 /// Returns error if:
178 /// - Browser executable not found
179 /// - Launch timeout
180 /// - Invalid options
181 /// - Browser process fails to start
182 ///
183 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
184 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
185 pub async fn launch_with_options(&self, options: LaunchOptions) -> Result<Browser> {
186 // Add Windows CI-specific browser args to prevent hanging
187 let options = {
188 #[cfg(windows)]
189 {
190 let mut options = options;
191 // Check if we're in a CI environment (GitHub Actions, Jenkins, etc.)
192 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
193
194 if is_ci {
195 tracing::debug!(
196 "[playwright-rust] Detected Windows CI environment, adding stability flags"
197 );
198
199 // Get existing args or create empty vec
200 let mut args = options.args.unwrap_or_default();
201
202 // Add Windows CI stability flags if not already present
203 let ci_flags = vec![
204 "--no-sandbox", // Disable sandboxing (often problematic in CI)
205 "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
206 "--disable-gpu", // Disable GPU hardware acceleration
207 "--disable-web-security", // Avoid CORS issues in CI
208 "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
209 ];
210
211 for flag in ci_flags {
212 if !args.iter().any(|a| a == flag) {
213 args.push(flag.to_string());
214 }
215 }
216
217 // Update options with enhanced args
218 options.args = Some(args);
219 }
220 options
221 }
222
223 #[cfg(not(windows))]
224 {
225 options
226 }
227 };
228
229 // Normalize options for protocol transmission
230 let params = options.normalize();
231
232 // Send launch RPC to server
233 let response: LaunchResponse = self.base.channel().send("launch", params).await?;
234
235 // Get browser object from registry and downcast
236 let browser: Browser = self
237 .connection()
238 .get_typed::<Browser>(&response.browser.guid)
239 .await?;
240
241 Ok(browser)
242 }
243
244 /// Launches a browser with persistent storage using default options.
245 ///
246 /// Returns a persistent browser context. Closing this context will automatically
247 /// close the browser.
248 ///
249 /// This method is useful for:
250 /// - Preserving authentication state across sessions
251 /// - Testing with real user profiles
252 /// - Creating standalone applications with app mode
253 /// - Simulating real user behavior with cookies and storage
254 ///
255 /// # Arguments
256 ///
257 /// * `user_data_dir` - Path to a user data directory (stores cookies, local storage)
258 ///
259 /// # Errors
260 ///
261 /// Returns error if:
262 /// - Browser executable not found
263 /// - Launch timeout (default three minutes, [`DEFAULT_LAUNCH_TIMEOUT_MS`](crate::DEFAULT_LAUNCH_TIMEOUT_MS))
264 /// - Browser process fails to start
265 /// - User data directory cannot be created
266 ///
267 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
268 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
269 pub async fn launch_persistent_context(
270 &self,
271 user_data_dir: impl Into<String>,
272 ) -> Result<BrowserContext> {
273 self.launch_persistent_context_with_options(user_data_dir, BrowserContextOptions::default())
274 .await
275 }
276
277 /// Launches a browser with persistent storage and custom options.
278 ///
279 /// Returns a persistent browser context with the specified configuration.
280 /// Closing this context will automatically close the browser.
281 ///
282 /// This method accepts both launch options (headless, args, etc.) and context
283 /// options (viewport, locale, etc.) in a single BrowserContextOptions struct.
284 ///
285 /// # Arguments
286 ///
287 /// * `user_data_dir` - Path to a user data directory (stores cookies, local storage)
288 /// * `options` - Combined launch and context options
289 ///
290 /// # Errors
291 ///
292 /// Returns error if:
293 /// - Browser executable not found
294 /// - Launch timeout
295 /// - Invalid options
296 /// - Browser process fails to start
297 /// - User data directory cannot be created
298 ///
299 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
300 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
301 pub async fn launch_persistent_context_with_options(
302 &self,
303 user_data_dir: impl Into<String>,
304 mut options: BrowserContextOptions,
305 ) -> Result<BrowserContext> {
306 // Add Windows CI-specific browser args to prevent hanging
307 #[cfg(windows)]
308 {
309 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
310
311 if is_ci {
312 tracing::debug!(
313 "[playwright-rust] Detected Windows CI environment, adding stability flags"
314 );
315
316 // Get existing args or create empty vec
317 let mut args = options.args.unwrap_or_default();
318
319 // Add Windows CI stability flags if not already present
320 let ci_flags = vec![
321 "--no-sandbox", // Disable sandboxing (often problematic in CI)
322 "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
323 "--disable-gpu", // Disable GPU hardware acceleration
324 "--disable-web-security", // Avoid CORS issues in CI
325 "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
326 ];
327
328 for flag in ci_flags {
329 if !args.iter().any(|a| a == flag) {
330 args.push(flag.to_string());
331 }
332 }
333
334 // Update options with enhanced args
335 options.args = Some(args);
336 }
337 }
338
339 // `recordHar` is not a driver parameter. As in the other language
340 // bindings, the HAR starts once the context exists and is written
341 // when it closes.
342 let option_har = options.record_har.take().map(OptionHar::new).transpose()?;
343
344 // Handle storage_state_path: read file and convert to inline storage_state
345 if let Some(path) = &options.storage_state_path {
346 let file_content = tokio::fs::read_to_string(path).await.map_err(|e| {
347 crate::error::Error::ProtocolError(format!(
348 "Failed to read storage state file '{}': {}",
349 path, e
350 ))
351 })?;
352
353 let storage_state: crate::protocol::StorageState = serde_json::from_str(&file_content)
354 .map_err(|e| {
355 crate::error::Error::ProtocolError(format!(
356 "Failed to parse storage state file '{}': {}",
357 path, e
358 ))
359 })?;
360
361 options.storage_state = Some(storage_state);
362 options.storage_state_path = None; // Clear path since we've converted to inline
363 }
364
365 // Convert options to JSON with userDataDir
366 let mut params = serde_json::to_value(&options).map_err(|e| {
367 crate::error::Error::ProtocolError(format!(
368 "Failed to serialize context options: {}",
369 e
370 ))
371 })?;
372
373 // Add userDataDir to params
374 params["userDataDir"] = serde_json::json!(user_data_dir.into());
375
376 // Set default timeout if not specified (required in Playwright 1.56.1+)
377 if params.get("timeout").is_none() {
378 params["timeout"] = serde_json::json!(crate::DEFAULT_LAUNCH_TIMEOUT_MS);
379 }
380
381 // Convert bool ignoreDefaultArgs to ignoreAllDefaultArgs
382 // (matches playwright-python's parameter normalization)
383 if let Some(ignore) = params.get("ignoreDefaultArgs")
384 && let Some(b) = ignore.as_bool()
385 {
386 if b {
387 params["ignoreAllDefaultArgs"] = serde_json::json!(true);
388 }
389 if let Some(obj) = params.as_object_mut() {
390 obj.remove("ignoreDefaultArgs");
391 }
392 }
393
394 // Send launchPersistentContext RPC to server
395 let response: LaunchPersistentContextResponse = self
396 .base
397 .channel()
398 .send("launchPersistentContext", params)
399 .await?;
400
401 // Get context object from registry and downcast
402 let context: BrowserContext = self
403 .connection()
404 .get_typed::<BrowserContext>(&response.context.guid)
405 .await?;
406
407 // Register with Selectors coordinator
408 let selectors = self.connection().selectors();
409 if let Err(e) = selectors.add_context(context.channel().clone()).await {
410 tracing::warn!("Failed to register BrowserContext with Selectors: {}", e);
411 }
412 if let Some(har) = option_har {
413 context.start_option_har(har).await?;
414 }
415
416 Ok(context)
417 }
418 /// Connects to an existing browser instance.
419 ///
420 /// # Arguments
421 /// * `ws_endpoint` - A WebSocket endpoint to connect to.
422 /// * `options` - Connection options.
423 ///
424 /// # Errors
425 /// Returns error if connection fails or handshake fails.
426 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name(), url = %ws_endpoint))]
427 pub async fn connect(
428 &self,
429 ws_endpoint: &str,
430 options: impl Into<Option<ConnectOptions>>,
431 ) -> Result<Browser> {
432 use crate::server::connection::Connection;
433 use crate::server::transport::WebSocketTransport;
434
435 let options = options.into();
436 let options = options.unwrap_or_default();
437
438 // Get timeout (default 30 seconds, 0 = no timeout)
439 let timeout_ms = options.timeout.unwrap_or(30000.0);
440
441 // 1. Connect to WebSocket
442 tracing::debug!("Connecting to remote browser at {}", ws_endpoint);
443
444 let connect_future = WebSocketTransport::connect(ws_endpoint, options.headers);
445 let (transport, message_rx) = if timeout_ms > 0.0 {
446 let timeout = std::time::Duration::from_millis(timeout_ms as u64);
447 tokio::time::timeout(timeout, connect_future)
448 .await
449 .map_err(|_| {
450 crate::error::Error::Timeout(format!(
451 "Connection to {} timed out after {} ms",
452 ws_endpoint, timeout_ms
453 ))
454 })??
455 } else {
456 connect_future.await?
457 };
458 let (sender, receiver) = transport.into_parts();
459
460 // 2. Create Connection
461 let connection = Arc::new(Connection::new(sender, receiver, message_rx));
462
463 // 3. Start message loop
464 let conn_for_loop = Arc::clone(&connection);
465 crate::task::spawn_detached(async move {
466 conn_for_loop.run().await;
467 });
468
469 // 4. Initialize Playwright
470 // This exchanges the "initialize" message and returns the root Playwright object
471 let playwright_obj = connection.initialize_playwright().await?;
472
473 // 5. Get pre-launched browser from initializer
474 // The server sends a "preLaunchedBrowser" field in the Playwright object's initializer
475 let initializer = playwright_obj.initializer();
476
477 let browser_guid = initializer["preLaunchedBrowser"]["guid"]
478 .as_str()
479 .ok_or_else(|| {
480 crate::error::Error::ProtocolError(
481 "Remote server did not return a pre-launched browser. Ensure server was launched in server mode.".to_string()
482 )
483 })?;
484
485 // 6. Get the existing Browser object and downcast
486 let browser: Browser = connection.get_typed::<Browser>(browser_guid).await?;
487
488 Ok(browser)
489 }
490
491 /// Connects to a browser over the Chrome DevTools Protocol.
492 ///
493 /// This method is only supported for Chromium. It connects to an existing Chrome
494 /// instance that exposes a CDP endpoint (e.g., `--remote-debugging-port`), or to
495 /// CDP-compatible services like browserless.
496 ///
497 /// Unlike `connect()`, which uses Playwright's proprietary WebSocket protocol,
498 /// this method connects directly via CDP. The Playwright server manages the CDP
499 /// connection internally.
500 ///
501 /// # Arguments
502 /// * `endpoint_url` - A CDP endpoint URL (e.g., `http://localhost:9222` or
503 /// `ws://localhost:9222/devtools/browser/...`)
504 /// * `options` - Optional connection options.
505 ///
506 /// # Errors
507 /// Returns error if:
508 /// - Called on a non-Chromium browser type
509 /// - Connection to the CDP endpoint fails
510 /// - Connection timeout
511 ///
512 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp>
513 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name(), url = %endpoint_url))]
514 pub async fn connect_over_cdp(
515 &self,
516 endpoint_url: &str,
517 options: impl Into<Option<ConnectOverCdpOptions>>,
518 ) -> Result<Browser> {
519 let options = options.into();
520 // connect_over_cdp is Chromium-only
521 if self.name() != "chromium" {
522 return Err(crate::error::Error::ProtocolError(
523 "Connecting over CDP is only supported in Chromium.".to_string(),
524 ));
525 }
526
527 let options = options.unwrap_or_default();
528
529 // Convert headers from HashMap to array of {name, value} objects
530 let headers_array = options.headers.map(|h| {
531 h.into_iter()
532 .map(|(name, value)| HeaderEntry { name, value })
533 .collect::<Vec<_>>()
534 });
535
536 let params = ConnectOverCdpParams {
537 endpoint_url: endpoint_url.to_string(),
538 headers: headers_array,
539 slow_mo: options.slow_mo,
540 no_defaults: options.no_defaults,
541 artifacts_dir: options.artifacts_dir,
542 timeout: options.timeout.unwrap_or(crate::DEFAULT_LAUNCH_TIMEOUT_MS),
543 };
544
545 // Send connectOverCDP RPC to the local Playwright server
546 let response: ConnectOverCdpResponse =
547 self.base.channel().send("connectOverCDP", params).await?;
548
549 // Get browser object from registry and downcast
550 let browser: Browser = self
551 .connection()
552 .get_typed::<Browser>(&response.browser.guid)
553 .await?;
554
555 Ok(browser)
556 }
557}
558
559/// Response from BrowserType.launch() protocol call
560#[derive(Debug, Deserialize, Serialize)]
561struct LaunchResponse {
562 browser: BrowserRef,
563}
564
565/// Response from BrowserType.launchPersistentContext() protocol call
566#[derive(Debug, Deserialize, Serialize)]
567struct LaunchPersistentContextResponse {
568 context: ContextRef,
569}
570
571/// Reference to a Browser object in the protocol
572#[derive(Debug, Deserialize, Serialize)]
573struct BrowserRef {
574 #[serde(
575 serialize_with = "crate::server::connection::serialize_arc_str",
576 deserialize_with = "crate::server::connection::deserialize_arc_str"
577 )]
578 guid: Arc<str>,
579}
580
581/// Reference to a BrowserContext object in the protocol
582#[derive(Debug, Deserialize, Serialize)]
583struct ContextRef {
584 #[serde(
585 serialize_with = "crate::server::connection::serialize_arc_str",
586 deserialize_with = "crate::server::connection::deserialize_arc_str"
587 )]
588 guid: Arc<str>,
589}
590
591/// Parameters for BrowserType.connectOverCDP() protocol call
592#[derive(Debug, Serialize)]
593struct ConnectOverCdpParams {
594 #[serde(rename = "endpointURL")]
595 endpoint_url: String,
596 #[serde(skip_serializing_if = "Option::is_none")]
597 headers: Option<Vec<HeaderEntry>>,
598 #[serde(rename = "slowMo", skip_serializing_if = "Option::is_none")]
599 slow_mo: Option<f64>,
600 #[serde(rename = "noDefaults", skip_serializing_if = "Option::is_none")]
601 no_defaults: Option<bool>,
602 #[serde(rename = "artifactsDir", skip_serializing_if = "Option::is_none")]
603 artifacts_dir: Option<String>,
604 timeout: f64,
605}
606
607/// A single HTTP header as {name, value} for the connectOverCDP protocol
608#[derive(Debug, Serialize)]
609struct HeaderEntry {
610 name: String,
611 value: String,
612}
613
614/// Response from BrowserType.connectOverCDP() protocol call
615#[derive(Debug, Deserialize)]
616struct ConnectOverCdpResponse {
617 browser: BrowserRef,
618 #[serde(rename = "defaultContext")]
619 #[allow(dead_code)]
620 default_context: Option<ContextRef>,
621}
622
623impl ChannelOwner for BrowserType {
624 fn guid(&self) -> &str {
625 self.base.guid()
626 }
627
628 fn type_name(&self) -> &str {
629 self.base.type_name()
630 }
631
632 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
633 self.base.parent()
634 }
635
636 fn connection(&self) -> Arc<dyn ConnectionLike> {
637 self.base.connection()
638 }
639
640 fn initializer(&self) -> &Value {
641 self.base.initializer()
642 }
643
644 fn channel(&self) -> &Channel {
645 self.base.channel()
646 }
647
648 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
649 self.base.dispose(reason)
650 }
651
652 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
653 self.base.adopt(child)
654 }
655
656 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
657 self.base.add_child(guid, child)
658 }
659
660 fn remove_child(&self, guid: &str) {
661 self.base.remove_child(guid)
662 }
663
664 fn on_event(&self, method: &str, params: Value) {
665 self.base.on_event(method, params)
666 }
667
668 fn was_collected(&self) -> bool {
669 self.base.was_collected()
670 }
671
672 fn as_any(&self) -> &dyn Any {
673 self
674 }
675}
676
677impl std::fmt::Debug for BrowserType {
678 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679 f.debug_struct("BrowserType")
680 .field("guid", &self.guid())
681 .field("name", &self.name)
682 .field("executable_path", &self.executable_path)
683 .finish()
684 }
685}
686
687// Note: BrowserType testing is done via integration tests since it requires:
688// - A real Connection with object registry
689// - Protocol messages from the server
690// See: crates/playwright-core/tests/connection_integration.rs
691
692#[cfg(test)]
693mod connect_over_cdp_params_tests {
694 use super::ConnectOverCdpParams;
695
696 #[test]
697 fn serializes_artifacts_dir_as_camelcase_when_set() {
698 let params = ConnectOverCdpParams {
699 endpoint_url: "http://localhost:9222".to_string(),
700 headers: None,
701 slow_mo: None,
702 no_defaults: None,
703 artifacts_dir: Some("/tmp/artifacts".to_string()),
704 timeout: 30000.0,
705 };
706 let v = serde_json::to_value(¶ms).unwrap();
707 assert_eq!(v["artifactsDir"], "/tmp/artifacts");
708 assert_eq!(v["endpointURL"], "http://localhost:9222");
709 }
710
711 #[test]
712 fn omits_artifacts_dir_when_unset() {
713 let params = ConnectOverCdpParams {
714 endpoint_url: "http://localhost:9222".to_string(),
715 headers: None,
716 slow_mo: None,
717 no_defaults: None,
718 artifacts_dir: None,
719 timeout: 30000.0,
720 };
721 let v = serde_json::to_value(¶ms).unwrap();
722 assert!(v.get("artifactsDir").is_none());
723 }
724}