Skip to main content

playwright_rs/protocol/browser_context/
state.rs

1use super::BrowserContext;
2use crate::error::{Error, Result};
3use crate::server::channel_owner::ChannelOwner;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Context state: cookies, storage state, headers, permissions, geolocation, credentials, offline.
8///
9/// Restoring a session cookie through the storage state:
10///
11/// ```no_run
12/// # use playwright_rs::Playwright;
13/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
14/// # let pw = Playwright::launch().await?;
15/// # let browser = pw.chromium().launch().await?;
16/// # let context = browser.new_context().await?;
17/// use playwright_rs::protocol::{Cookie, StorageState};
18///
19/// let state = StorageState::default().cookies(vec![
20///     Cookie::new("session", "token123")
21///         .domain("example.com")
22///         .path("/")
23///         .http_only(true)
24///         .secure(true)
25///         .same_site("Lax"),
26/// ]);
27/// context.set_storage_state(state).await?;
28/// # Ok(())
29/// # }
30/// ```
31impl BrowserContext {
32    /// Returns storage state for this browser context.
33    ///
34    /// Contains current cookies and local storage snapshots.
35    ///
36    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
37    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
38    pub async fn storage_state(
39        &self,
40        options: impl Into<Option<StorageStateOptions>>,
41    ) -> Result<StorageState> {
42        let params = serde_json::to_value(options.into().unwrap_or_default())
43            .map_err(|e| Error::ProtocolError(format!("Failed to serialize options: {e}")))?;
44        let response: StorageState = self.channel().send("storageState", params).await?;
45        Ok(response)
46    }
47
48    /// Replaces this context's storage state in-place via the driver's
49    /// `setStorageState`, matching `browserContext.setStorageState()` in the
50    /// JS/Python APIs. Useful for restoring authentication state without
51    /// recreating the context.
52    ///
53    /// This is a **replace**, not a merge, and the driver is thorough about
54    /// it. Beyond installing the cookies, origins and passkeys carried by
55    /// `state`, it also:
56    ///
57    /// - clears the HTTP cache;
58    /// - clears storage (localStorage, sessionStorage, IndexedDB, service
59    ///   workers) for **every origin the context has visited**, not only the
60    ///   origins listed in `state`;
61    /// - when `state` carries no `credentials`, disposes an installed
62    ///   virtual authenticator along with its passkeys. Capture the state
63    ///   with [`StorageStateOptions::credentials`] if the context being
64    ///   restored into should keep WebAuthn working.
65    ///
66    /// No client-visible page is opened: the driver uses an internal page,
67    /// navigated to each origin, to apply origin-scoped state.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the state fails to serialize or the driver
72    /// rejects it, or if the context has closed.
73    ///
74    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-storage-state>
75    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
76    pub async fn set_storage_state(&self, state: StorageState) -> Result<()> {
77        // Delegates to the driver rather than reconstructing the state
78        // client-side. The previous implementation cleared cookies, re-added
79        // them, then opened a throwaway page per origin to replay
80        // localStorage through `evaluate`. That could only ever restore what
81        // it knew how to replay, so WebAuthn passkeys and
82        // IndexedDB were silently dropped, and every origin cost a page
83        // navigation.
84        let storage_state = serde_json::to_value(&state)
85            .map_err(|e| Error::ProtocolError(format!("Failed to serialize storage state: {e}")))?;
86
87        self.channel()
88            .send_no_result(
89                "setStorageState",
90                serde_json::json!({ "storageState": storage_state }),
91            )
92            .await
93    }
94
95    /// Adds cookies into this browser context.
96    ///
97    /// All pages within this context will have these cookies installed. Cookies can be granularly specified
98    /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
99    ///
100    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
101    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = cookies.len()))]
102    pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
103        self.channel()
104            .send_no_result(
105                "addCookies",
106                serde_json::json!({
107                    "cookies": cookies
108                }),
109            )
110            .await
111    }
112
113    /// Returns cookies for this browser context, optionally filtered by URLs.
114    ///
115    /// If `urls` is `None` or empty, all cookies are returned.
116    ///
117    /// # Arguments
118    ///
119    /// * `urls` - Optional list of URLs to filter cookies by
120    ///
121    /// # Errors
122    ///
123    /// Returns error if:
124    /// - Context has been closed
125    /// - Communication with browser process fails
126    ///
127    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies>
128    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = tracing::field::Empty))]
129    pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
130        let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
131        #[derive(serde::Deserialize)]
132        struct CookiesResponse {
133            cookies: Vec<Cookie>,
134        }
135        let response: CookiesResponse = self
136            .channel()
137            .send("cookies", serde_json::json!({ "urls": url_list }))
138            .await?;
139        tracing::Span::current().record("count", response.cookies.len());
140        Ok(response.cookies)
141    }
142
143    /// Clears cookies from this browser context, with optional filters.
144    ///
145    /// When called with no options, all cookies are removed. Use `ClearCookiesOptions`
146    /// to filter which cookies to clear by name, domain, or path.
147    ///
148    /// # Arguments
149    ///
150    /// * `options` - Optional filters for which cookies to clear
151    ///
152    /// # Errors
153    ///
154    /// Returns error if:
155    /// - Context has been closed
156    /// - Communication with browser process fails
157    ///
158    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
159    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
160    pub async fn clear_cookies(
161        &self,
162        options: impl Into<Option<ClearCookiesOptions>>,
163    ) -> Result<()> {
164        let options = options.into();
165        self.channel()
166            .send_no_result("clearCookies", options.unwrap_or_default())
167            .await
168    }
169
170    /// Sets extra HTTP headers that will be sent with every request from this context.
171    ///
172    /// These headers are merged with per-page extra headers set with `page.set_extra_http_headers()`.
173    /// If the page has specific headers that conflict, page-level headers take precedence.
174    ///
175    /// # Arguments
176    ///
177    /// * `headers` - Map of header names to values. All header names are lowercased.
178    ///
179    /// # Errors
180    ///
181    /// Returns error if:
182    /// - Context has been closed
183    /// - Communication with browser process fails
184    ///
185    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers>
186    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = headers.len()))]
187    pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
188        // Playwright protocol expects an array of {name, value} objects
189        let headers_array = crate::protocol::route_params::header_array(headers);
190        self.channel()
191            .send_no_result(
192                "setExtraHTTPHeaders",
193                serde_json::json!({ "headers": headers_array }),
194            )
195            .await
196    }
197
198    /// Grants browser permissions to the context.
199    ///
200    /// Permissions are granted for all pages in the context. The optional `origin`
201    /// in `GrantPermissionsOptions` restricts the grant to a specific URL origin.
202    ///
203    /// Common permissions: `"geolocation"`, `"notifications"`, `"camera"`,
204    /// `"microphone"`, `"clipboard-read"`, `"clipboard-write"`.
205    ///
206    /// # Arguments
207    ///
208    /// * `permissions` - List of permission strings to grant
209    /// * `options` - Optional options, including `origin` to restrict the grant
210    ///
211    /// # Errors
212    ///
213    /// Returns error if:
214    /// - Permission name is not recognised
215    /// - Context has been closed
216    /// - Communication with browser process fails
217    ///
218    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
219    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
220    pub async fn grant_permissions(
221        &self,
222        permissions: &[&str],
223        options: impl Into<Option<GrantPermissionsOptions>>,
224    ) -> Result<()> {
225        let options = options.into();
226        let mut params = serde_json::json!({ "permissions": permissions });
227        if let Some(opts) = options
228            && let Some(origin) = opts.origin
229        {
230            params["origin"] = serde_json::Value::String(origin);
231        }
232        self.channel()
233            .send_no_result("grantPermissions", params)
234            .await
235    }
236
237    /// Clears all permission overrides for this browser context.
238    ///
239    /// Reverts all permissions previously set with `grant_permissions()` back to
240    /// the browser default state.
241    ///
242    /// # Errors
243    ///
244    /// Returns error if:
245    /// - Context has been closed
246    /// - Communication with browser process fails
247    ///
248    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-permissions>
249    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
250    pub async fn clear_permissions(&self) -> Result<()> {
251        self.channel()
252            .send_no_result("clearPermissions", serde_json::json!({}))
253            .await
254    }
255
256    /// Sets or clears the geolocation for all pages in this context.
257    ///
258    /// Pass `Some(Geolocation { ... })` to set a specific location, or `None` to
259    /// clear the override and let the browser handle location requests naturally.
260    ///
261    /// Note: Geolocation access requires the `"geolocation"` permission to be granted
262    /// via `grant_permissions()` for navigator.geolocation to succeed.
263    ///
264    /// # Arguments
265    ///
266    /// * `geolocation` - Location to set, or `None` to clear
267    ///
268    /// # Errors
269    ///
270    /// Returns error if:
271    /// - Latitude or longitude is out of range
272    /// - Context has been closed
273    /// - Communication with browser process fails
274    ///
275    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation>
276    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
277    pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
278        // Playwright protocol: omit the "geolocation" key entirely to clear;
279        // passing null causes a validation error on the server side.
280        let params = match geolocation {
281            Some(geo) => serde_json::json!({ "geolocation": geo }),
282            None => serde_json::json!({}),
283        };
284        self.channel()
285            .send_no_result("setGeolocation", params)
286            .await
287    }
288
289    /// Replaces the credentials used for HTTP authentication.
290    ///
291    /// Each request uses the first entry whose `origin` matches it; an entry
292    /// without an origin matches anything. Pass an empty vector to clear.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error if the context is closed or the driver rejects the
297    /// credentials.
298    ///
299    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-http-credentials>
300    pub async fn set_http_credentials(&self, credentials: Vec<HttpCredentials>) -> Result<()> {
301        self.channel()
302            .send_no_result(
303                "setHTTPCredentials",
304                serde_json::json!({ "httpCredentials": credentials }),
305            )
306            .await
307    }
308
309    /// Toggles the offline mode for this browser context.
310    ///
311    /// When `true`, all network requests from pages in this context will fail with
312    /// a network error. Set to `false` to restore network connectivity.
313    ///
314    /// # Arguments
315    ///
316    /// * `offline` - `true` to go offline, `false` to go back online
317    ///
318    /// # Errors
319    ///
320    /// Returns error if:
321    /// - Context has been closed
322    /// - Communication with browser process fails
323    ///
324    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline>
325    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), offline))]
326    pub async fn set_offline(&self, offline: bool) -> Result<()> {
327        self.channel()
328            .send_no_result("setOffline", serde_json::json!({ "offline": offline }))
329            .await
330    }
331}
332
333/// Geolocation coordinates.
334///
335/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct Geolocation {
338    /// Latitude between -90 and 90
339    pub latitude: f64,
340    /// Longitude between -180 and 180
341    pub longitude: f64,
342    /// Optional accuracy in meters (default: 0)
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub accuracy: Option<f64>,
345}
346
347/// When to send HTTP credentials.
348///
349/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-http-credentials>
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(rename_all = "lowercase")]
352#[non_exhaustive]
353pub enum HttpCredentialsSend {
354    /// Send the `Authorization` header up front rather than waiting to be
355    /// challenged.
356    ///
357    /// **Only honored by [`APIRequestContext`](crate::protocol::APIRequestContext)
358    /// fetches**, matching upstream: browser navigation stays reactive on
359    /// every engine, so this is a no-op for page loads. Reach for it when a
360    /// server answers `403` instead of `401`, which leaves nothing to react
361    /// to.
362    Always,
363    /// Send it only after the server answers `401`. The default.
364    Unauthorized,
365}
366
367/// Credentials for HTTP authentication.
368///
369/// A context can hold several: the first whose `origin` matches the request
370/// is used, and an entry without an origin matches any request.
371///
372/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-http-credentials>
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[non_exhaustive]
375pub struct HttpCredentials {
376    /// Username to authenticate with.
377    pub username: String,
378    /// Password to authenticate with.
379    pub password: String,
380    /// Restrict these credentials to one origin (scheme, host, and port,
381    /// e.g. `https://example.com`). Without it they match any request.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub origin: Option<String>,
384    /// When to send the header. Defaults to after a `401`, and only an
385    /// [`APIRequestContext`](crate::protocol::APIRequestContext) honors
386    /// anything else; see [`HttpCredentialsSend`].
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub send: Option<HttpCredentialsSend>,
389}
390
391impl HttpCredentials {
392    /// Credentials matching any origin.
393    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
394        Self {
395            username: username.into(),
396            password: password.into(),
397            origin: None,
398            send: None,
399        }
400    }
401
402    /// Restrict these credentials to one origin.
403    pub fn origin(mut self, origin: impl Into<String>) -> Self {
404        self.origin = Some(origin.into());
405        self
406    }
407
408    /// Choose when the `Authorization` header is sent.
409    pub fn send(mut self, send: HttpCredentialsSend) -> Self {
410        self.send = Some(send);
411        self
412    }
413}
414
415/// Cookie information for storage state.
416///
417/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
418#[derive(Debug, Clone, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420#[non_exhaustive]
421pub struct Cookie {
422    /// Cookie name
423    pub name: String,
424    /// Cookie value
425    pub value: String,
426    /// The URL a cookie being added applies to, in place of `domain` and
427    /// `path`: the driver derives both, and `secure`, from it. Never set on
428    /// a cookie read back from a context.
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    pub url: Option<String>,
431    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
432    #[serde(skip_serializing_if = "String::is_empty")]
433    pub domain: String,
434    /// Cookie path
435    #[serde(skip_serializing_if = "String::is_empty")]
436    pub path: String,
437    /// Unix timestamp in seconds; -1 for session cookies
438    pub expires: f64,
439    /// HTTP-only flag
440    pub http_only: bool,
441    /// Secure flag
442    pub secure: bool,
443    /// SameSite attribute ("Strict", "Lax", "None")
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub same_site: Option<String>,
446}
447
448impl Cookie {
449    /// Create a session cookie (no expiry) with the given name and value.
450    /// Set a [`url`](Self::url), or a [`domain`](Self::domain) (the path
451    /// defaults to `/`), before adding it.
452    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
453        Self {
454            name: name.into(),
455            value: value.into(),
456            url: None,
457            domain: String::new(),
458            path: "/".to_string(),
459            expires: -1.0,
460            http_only: false,
461            secure: false,
462            same_site: None,
463        }
464    }
465    /// The URL the cookie applies to, in place of a domain and path. Clears
466    /// both, since the driver rejects a cookie that has a URL and either, and
467    /// setting a domain or path afterwards clears the URL in turn.
468    pub fn url(mut self, url: impl Into<String>) -> Self {
469        self.url = Some(url.into());
470        self.domain.clear();
471        self.path.clear();
472        self
473    }
474    /// Cookie domain (e.g. "example.com").
475    pub fn domain(mut self, domain: impl Into<String>) -> Self {
476        self.domain = domain.into();
477        self.url = None;
478        self
479    }
480    /// Cookie path.
481    pub fn path(mut self, path: impl Into<String>) -> Self {
482        self.path = path.into();
483        self.url = None;
484        self
485    }
486    /// Expiry as Unix time in seconds (-1 for a session cookie).
487    pub fn expires(mut self, expires: f64) -> Self {
488        self.expires = expires;
489        self
490    }
491    /// Mark the cookie HttpOnly.
492    pub fn http_only(mut self, http_only: bool) -> Self {
493        self.http_only = http_only;
494        self
495    }
496    /// Mark the cookie Secure.
497    pub fn secure(mut self, secure: bool) -> Self {
498        self.secure = secure;
499        self
500    }
501    /// SameSite attribute ("Strict", "Lax", or "None").
502    pub fn same_site(mut self, same_site: impl Into<String>) -> Self {
503        self.same_site = Some(same_site.into());
504        self
505    }
506}
507
508/// Local storage item for storage state.
509///
510/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[non_exhaustive]
513pub struct LocalStorageItem {
514    /// Storage key
515    pub name: String,
516    /// Storage value
517    pub value: String,
518}
519
520impl LocalStorageItem {
521    /// A single localStorage key/value pair.
522    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
523        Self {
524            name: name.into(),
525            value: value.into(),
526        }
527    }
528}
529
530/// Origin with local storage items for storage state.
531///
532/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
533#[derive(Debug, Clone, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535#[non_exhaustive]
536pub struct Origin {
537    /// Origin URL (e.g., `https://example.com`)
538    pub origin: String,
539    /// Local storage items for this origin
540    pub local_storage: Vec<LocalStorageItem>,
541    /// IndexedDB contents for this origin, as the driver's opaque payload.
542    ///
543    /// Populated when the state was captured with
544    /// [`StorageStateOptions::indexed_db`], and passed back verbatim on
545    /// restore. Kept as raw JSON rather than modelled: the shape is an
546    /// implementation detail of the driver's snapshot format, and the only
547    /// supported operation is carrying it back unchanged.
548    #[serde(rename = "indexedDB", default, skip_serializing_if = "Option::is_none")]
549    pub indexed_db: Option<serde_json::Value>,
550    /// This origin's private file system, as the driver's opaque payload.
551    ///
552    /// Populated when the state was captured with
553    /// [`StorageStateOptions::opfs`], and passed back verbatim on restore.
554    /// Kept as raw JSON for the same reason as `indexed_db`: the shape is
555    /// the driver's snapshot format, and the only supported operation is
556    /// carrying it back unchanged.
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    pub opfs: Option<serde_json::Value>,
559}
560
561impl Origin {
562    /// Storage entries for one origin.
563    pub fn new(origin: impl Into<String>, local_storage: Vec<LocalStorageItem>) -> Self {
564        Self {
565            origin: origin.into(),
566            local_storage,
567            indexed_db: None,
568            opfs: None,
569        }
570    }
571}
572
573/// Storage state containing cookies and local storage.
574///
575/// Used to populate a browser context with saved authentication state,
576/// enabling session persistence across context instances.
577///
578/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
579#[derive(Debug, Clone, Default, Serialize, Deserialize)]
580#[non_exhaustive]
581pub struct StorageState {
582    /// List of cookies
583    pub cookies: Vec<Cookie>,
584    /// List of origins with local storage
585    pub origins: Vec<Origin>,
586    /// WebAuthn passkeys held by the context's virtual authenticator.
587    /// Only populated when the state was captured with
588    /// [`StorageStateOptions::credentials`]; omitted from the wire when empty
589    /// so a state captured without them is unchanged.
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub credentials: Option<Vec<crate::protocol::VirtualCredential>>,
592}
593
594impl StorageState {
595    /// Cookies to seed the context with.
596    pub fn cookies(mut self, cookies: Vec<Cookie>) -> Self {
597        self.cookies = cookies;
598        self
599    }
600    /// Per-origin storage (localStorage) to seed the context with.
601    pub fn origins(mut self, origins: Vec<Origin>) -> Self {
602        self.origins = origins;
603        self
604    }
605    /// WebAuthn passkeys to seed the context's virtual authenticator with.
606    pub fn credentials(mut self, credentials: Vec<crate::protocol::VirtualCredential>) -> Self {
607        self.credentials = Some(credentials);
608        self
609    }
610}
611
612/// Options for [`BrowserContext::storage_state`].
613///
614/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
615#[derive(Debug, Clone, Default, Serialize)]
616#[serde(rename_all = "camelCase")]
617#[non_exhaustive]
618pub struct StorageStateOptions {
619    /// Include IndexedDB contents in the captured state.
620    // camelCase would render this "indexedDb"; the protocol field is
621    // "indexedDB", and the driver drops unknown parameters silently, so the
622    // wrong casing is not an error but a no-op.
623    #[serde(rename = "indexedDB", skip_serializing_if = "Option::is_none")]
624    pub indexed_db: Option<bool>,
625    /// Include the virtual authenticator's WebAuthn passkeys.
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub credentials: Option<bool>,
628    /// Include each origin's private file system in the captured state.
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub opfs: Option<bool>,
631}
632
633impl StorageStateOptions {
634    /// Include IndexedDB contents in the captured state.
635    pub fn indexed_db(mut self, include: bool) -> Self {
636        self.indexed_db = Some(include);
637        self
638    }
639    /// Include the virtual authenticator's WebAuthn passkeys.
640    pub fn credentials(mut self, include: bool) -> Self {
641        self.credentials = Some(include);
642        self
643    }
644    /// Include each origin's private file system in the captured state.
645    pub fn opfs(mut self, include: bool) -> Self {
646        self.opfs = Some(include);
647        self
648    }
649}
650
651/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
652///
653/// All fields are optional; when provided they act as AND-combined filters.
654/// Each of name, domain, and path filters by an exact string or by a regular
655/// expression, whichever was set last. A regular expression is matched by the
656/// driver as JavaScript, so its source is JavaScript regex syntax and its
657/// flags are JavaScript flags (`"i"`, `"m"`, `"s"`, `"u"`).
658///
659/// Used by both
660/// [`BrowserContext::clear_cookies`](crate::protocol::BrowserContext::clear_cookies)
661/// and
662/// [`APIRequestContext::clear_cookies`](crate::protocol::APIRequestContext::clear_cookies).
663///
664/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
665#[derive(Debug, Clone, Default, Serialize)]
666#[serde(rename_all = "camelCase")]
667#[non_exhaustive]
668pub struct ClearCookiesOptions {
669    /// Filter by cookie name (exact match).
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub name: Option<String>,
672    /// Filter by cookie name with a regular expression: its source.
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub name_regex_source: Option<String>,
675    /// Flags for [`name_regex_source`](Self::name_regex_source).
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub name_regex_flags: Option<String>,
678    /// Filter by cookie domain.
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub domain: Option<String>,
681    /// Filter by cookie domain with a regular expression: its source.
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub domain_regex_source: Option<String>,
684    /// Flags for [`domain_regex_source`](Self::domain_regex_source).
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub domain_regex_flags: Option<String>,
687    /// Filter by cookie path.
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub path: Option<String>,
690    /// Filter by cookie path with a regular expression: its source.
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub path_regex_source: Option<String>,
693    /// Flags for [`path_regex_source`](Self::path_regex_source).
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub path_regex_flags: Option<String>,
696}
697
698impl ClearCookiesOptions {
699    /// Only clear cookies with this name.
700    pub fn name(mut self, name: impl Into<String>) -> Self {
701        self.name = Some(name.into());
702        self.name_regex_source = None;
703        self.name_regex_flags = None;
704        self
705    }
706    /// Only clear cookies whose name matches the JavaScript regular
707    /// expression `source` with `flags` (`""` for none).
708    pub fn name_regex(mut self, source: impl Into<String>, flags: impl Into<String>) -> Self {
709        self.name = None;
710        self.name_regex_source = Some(source.into());
711        self.name_regex_flags = Some(flags.into());
712        self
713    }
714    /// Only clear cookies for this domain.
715    pub fn domain(mut self, domain: impl Into<String>) -> Self {
716        self.domain = Some(domain.into());
717        self.domain_regex_source = None;
718        self.domain_regex_flags = None;
719        self
720    }
721    /// Only clear cookies whose domain matches the JavaScript regular
722    /// expression `source` with `flags` (`""` for none).
723    pub fn domain_regex(mut self, source: impl Into<String>, flags: impl Into<String>) -> Self {
724        self.domain = None;
725        self.domain_regex_source = Some(source.into());
726        self.domain_regex_flags = Some(flags.into());
727        self
728    }
729    /// Only clear cookies for this path.
730    pub fn path(mut self, path: impl Into<String>) -> Self {
731        self.path = Some(path.into());
732        self.path_regex_source = None;
733        self.path_regex_flags = None;
734        self
735    }
736    /// Only clear cookies whose path matches the JavaScript regular
737    /// expression `source` with `flags` (`""` for none).
738    pub fn path_regex(mut self, source: impl Into<String>, flags: impl Into<String>) -> Self {
739        self.path = None;
740        self.path_regex_source = Some(source.into());
741        self.path_regex_flags = Some(flags.into());
742        self
743    }
744}
745
746/// Options for `BrowserContext::grant_permissions()`.
747///
748/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
749#[derive(Debug, Clone, Default)]
750#[non_exhaustive]
751pub struct GrantPermissionsOptions {
752    /// Optional origin to restrict the permission grant to.
753    ///
754    /// For example `"https://example.com"`.
755    pub origin: Option<String>,
756}
757
758impl GrantPermissionsOptions {
759    /// Restrict the grant to the given origin.
760    pub fn origin(mut self, origin: impl Into<String>) -> Self {
761        self.origin = Some(origin.into());
762        self
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769
770    #[test]
771    fn a_cookie_for_a_url_sends_the_url_instead_of_a_domain_and_path() {
772        let sent =
773            serde_json::to_value(Cookie::new("a", "b").url("http://localhost:8080/app")).unwrap();
774        assert_eq!(sent["url"], "http://localhost:8080/app");
775        assert!(sent.get("domain").is_none(), "{sent}");
776        assert!(sent.get("path").is_none(), "{sent}");
777
778        let scoped = serde_json::to_value(
779            Cookie::new("a", "b")
780                .url("http://localhost:8080/app")
781                .domain("example.com")
782                .path("/"),
783        )
784        .unwrap();
785        assert_eq!(
786            (scoped["domain"].as_str(), scoped["path"].as_str()),
787            (Some("example.com"), Some("/"))
788        );
789        assert!(scoped.get("url").is_none(), "{scoped}");
790    }
791
792    #[test]
793    fn clear_cookies_filters_send_a_string_or_a_regex_under_the_driver_spelling() {
794        use crate::protocol_spec::{
795            API_REQUEST_CONTEXT, BROWSER_CONTEXT, command_parameters, keys,
796        };
797
798        let options = ClearCookiesOptions::default()
799            .name_regex("^sess", "")
800            .domain_regex(r"one\.com$", "i")
801            .path("/app");
802        let sent = serde_json::to_value(&options).unwrap();
803        assert_eq!(
804            sent,
805            serde_json::json!({
806                "nameRegexSource": "^sess",
807                "nameRegexFlags": "",
808                "domainRegexSource": "one\\.com$",
809                "domainRegexFlags": "i",
810                "path": "/app"
811            })
812        );
813        assert!(keys(&sent).is_subset(&command_parameters(BROWSER_CONTEXT, "clearCookies")));
814        assert!(keys(&sent).is_subset(&command_parameters(API_REQUEST_CONTEXT, "clearCookies")));
815    }
816
817    #[test]
818    fn a_clear_cookies_filter_is_a_string_or_a_regex_whichever_was_set_last() {
819        let regex_then_string = ClearCookiesOptions::default()
820            .name_regex("^sess", "")
821            .domain_regex("x", "")
822            .path_regex("y", "")
823            .name("sid")
824            .domain("example.com")
825            .path("/");
826        assert_eq!(
827            serde_json::to_value(&regex_then_string).unwrap(),
828            serde_json::json!({"name": "sid", "domain": "example.com", "path": "/"})
829        );
830        let string_then_regex = ClearCookiesOptions::default()
831            .name("sid")
832            .domain("example.com")
833            .path("/")
834            .name_regex("^sess", "")
835            .domain_regex("x", "")
836            .path_regex("y", "m");
837        assert_eq!(
838            serde_json::to_value(&string_then_regex).unwrap(),
839            serde_json::json!({
840                "nameRegexSource": "^sess", "nameRegexFlags": "",
841                "domainRegexSource": "x", "domainRegexFlags": "",
842                "pathRegexSource": "y", "pathRegexFlags": "m"
843            })
844        );
845    }
846
847    #[test]
848    fn storage_state_options_serialize_with_protocol_casing() {
849        // The driver validates "indexedDB" and silently drops unknown keys,
850        // so serde's camelCase ("indexedDb") would make the flag a no-op
851        // that reports success.
852        let opts = StorageStateOptions::default()
853            .indexed_db(true)
854            .credentials(true);
855        let value = serde_json::to_value(&opts).unwrap();
856        assert_eq!(
857            value,
858            serde_json::json!({ "indexedDB": true, "credentials": true })
859        );
860    }
861
862    #[test]
863    fn http_credentials_serialize_with_protocol_casing() {
864        // The driver validates `send` against an enum, so a wrong spelling is
865        // a hard error rather than a silent drop; and an unset field must not
866        // serialize as null.
867        let bare = HttpCredentials::new("user", "secret");
868        assert_eq!(
869            serde_json::to_value(&bare).unwrap(),
870            serde_json::json!({ "username": "user", "password": "secret" })
871        );
872
873        let full = HttpCredentials::new("user", "secret")
874            .origin("https://example.test")
875            .send(HttpCredentialsSend::Always);
876        assert_eq!(
877            serde_json::to_value(&full).unwrap(),
878            serde_json::json!({
879                "username": "user",
880                "password": "secret",
881                "origin": "https://example.test",
882                "send": "always",
883            })
884        );
885
886        assert_eq!(
887            serde_json::to_value(HttpCredentialsSend::Unauthorized).unwrap(),
888            serde_json::json!("unauthorized")
889        );
890    }
891
892    #[test]
893    fn storage_state_round_trips_credentials_content() {
894        // A captured state's value is in being restorable; this pins that a
895        // save/load through JSON preserves credential content, not just
896        // count.
897        // Exactly the driver's VirtualCredential schema: five required
898        // string fields and the signature counter.
899        let json = serde_json::json!({
900            "cookies": [],
901            "origins": [],
902            "credentials": [{
903                "id": "Y3JlZA",
904                "rpId": "example.com",
905                "userHandle": "dXNlcg",
906                "privateKey": "cGtleQ",
907                "publicKey": "cHVi",
908                "signCount": 43
909            }]
910        });
911        let state: StorageState = serde_json::from_value(json.clone()).unwrap();
912        let back = serde_json::to_value(&state).unwrap();
913        assert_eq!(back["credentials"], json["credentials"]);
914    }
915
916    #[test]
917    fn origin_round_trips_indexed_db_payload_verbatim() {
918        // The payload is the driver's opaque snapshot format; the contract
919        // is carrying it back unchanged, under the protocol's exact casing.
920        let json = serde_json::json!({
921            "origin": "https://example.com",
922            "localStorage": [],
923            "indexedDB": [{"name": "db", "version": 1, "stores": []}]
924        });
925        let origin: Origin = serde_json::from_value(json.clone()).unwrap();
926        let back = serde_json::to_value(&origin).unwrap();
927        assert_eq!(back["indexedDB"], json["indexedDB"]);
928        assert!(back.get("indexedDb").is_none());
929    }
930}