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(®ex_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}