playwright_cdp/web_storage.rs
1//! `WebStorage` — `localStorage` / `sessionStorage` access via the CDP
2//! `DOMStorage` domain.
3//!
4//! A [`WebStorage`] is bound to a page session, a security origin, and a choice
5//! of local vs. session storage. All operations map directly onto CDP's
6//! `DOMStorage.*` commands with a `storageId` of `{ securityOrigin,
7//! isLocalStorage }`:
8//!
9//! | Method | CDP command |
10//! |---------------------------|----------------------------|
11//! | [`WebStorage::get`] | `DOMStorage.getDOMStorageItems` (client-side filter) |
12//! | [`WebStorage::set`] | `DOMStorage.setDOMStorageItem` |
13//! | [`WebStorage::remove`] | `DOMStorage.removeDOMStorageItem` |
14//! | [`WebStorage::clear`] | `DOMStorage.clear` |
15//! | [`WebStorage::keys`] | `DOMStorage.getDOMStorageItems` |
16//! | [`WebStorage::size`] | `DOMStorage.getDOMStorageItems` |
17//! | [`WebStorage::has`] | `DOMStorage.getDOMStorageItems` (client-side filter) |
18//!
19//! Mirrors the shape of Playwright's `page.evaluate`-backed storage helpers,
20//! but talks CDP directly.
21
22use crate::cdp::session::CdpSession;
23use crate::error::{Error, Result};
24use serde_json::{json, Value};
25use std::sync::Arc;
26
27/// A handle to one of a page's web storages (`localStorage` or
28/// `sessionStorage`) for a given origin.
29///
30/// Cheaply cloneable; all clones share the same page session, origin, and
31/// storage kind.
32#[derive(Clone)]
33pub struct WebStorage {
34 inner: Arc<WebStorageInner>,
35}
36
37struct WebStorageInner {
38 /// The page-level CDP session (DOMStorage lives here).
39 session: CdpSession,
40 /// The security origin the storage is scoped to (e.g. `https://example.com`).
41 security_origin: String,
42 /// `true` for `localStorage`, `false` for `sessionStorage`.
43 is_local_storage: bool,
44}
45
46impl WebStorage {
47 pub(crate) fn new(session: CdpSession, security_origin: String, is_local_storage: bool) -> Self {
48 Self {
49 inner: Arc::new(WebStorageInner {
50 session,
51 security_origin,
52 is_local_storage,
53 }),
54 }
55 }
56
57 /// Build a `localStorage` handle for the given origin on `session`.
58 pub(crate) fn local_storage(session: CdpSession, security_origin: String) -> Self {
59 Self::new(session, security_origin, true)
60 }
61
62 /// Build a `sessionStorage` handle for the given origin on `session`.
63 pub(crate) fn session_storage(session: CdpSession, security_origin: String) -> Self {
64 Self::new(session, security_origin, false)
65 }
66
67 /// The CDP `storageId` object for this handle.
68 fn storage_id(&self) -> Value {
69 json!({
70 "securityOrigin": self.inner.security_origin,
71 "isLocalStorage": self.inner.is_local_storage,
72 })
73 }
74
75 /// Fetch all `[key, value]` pairs from storage via
76 /// `DOMStorage.getDOMStorageItems`.
77 async fn items(&self) -> Result<Vec<(String, String)>> {
78 let resp: Value = self
79 .inner
80 .session
81 .send(
82 "DOMStorage.getDOMStorageItems",
83 json!({ "storageId": self.storage_id() }),
84 )
85 .await?;
86 let arr = resp
87 .get("entries")
88 .and_then(|v| v.as_array())
89 .ok_or_else(|| Error::ProtocolError("getDOMStorageItems missing 'entries'".into()))?;
90 let mut out = Vec::with_capacity(arr.len());
91 for entry in arr {
92 let mut it = entry.as_array().into_iter().flatten();
93 let key = it
94 .next()
95 .and_then(|v| v.as_str())
96 .unwrap_or("")
97 .to_string();
98 let value = it
99 .next()
100 .and_then(|v| v.as_str())
101 .unwrap_or("")
102 .to_string();
103 out.push((key, value));
104 }
105 Ok(out)
106 }
107
108 /// The value for `key`, or `None` if absent.
109 pub async fn get(&self, key: &str) -> Result<Option<String>> {
110 let items = self.items().await?;
111 Ok(items
112 .into_iter()
113 .find_map(|(k, v)| if k == key { Some(v) } else { None }))
114 }
115
116 /// Set `key` to `value`, overwriting any existing value. Issues
117 /// `DOMStorage.setDOMStorageItem`.
118 pub async fn set(&self, key: &str, value: &str) -> Result<()> {
119 self.inner
120 .session
121 .send(
122 "DOMStorage.setDOMStorageItem",
123 json!({
124 "storageId": self.storage_id(),
125 "key": key,
126 "value": value,
127 }),
128 )
129 .await?;
130 Ok(())
131 }
132
133 /// Remove `key` (no-op if absent). Issues `DOMStorage.removeDOMStorageItem`.
134 pub async fn remove(&self, key: &str) -> Result<()> {
135 self.inner
136 .session
137 .send(
138 "DOMStorage.removeDOMStorageItem",
139 json!({
140 "storageId": self.storage_id(),
141 "key": key,
142 }),
143 )
144 .await?;
145 Ok(())
146 }
147
148 /// Remove all entries. Issues `DOMStorage.clear`.
149 pub async fn clear(&self) -> Result<()> {
150 self.inner
151 .session
152 .send(
153 "DOMStorage.clear",
154 json!({ "storageId": self.storage_id() }),
155 )
156 .await?;
157 Ok(())
158 }
159
160 /// All keys currently in storage.
161 pub async fn keys(&self) -> Result<Vec<String>> {
162 Ok(self.items().await?.into_iter().map(|(k, _)| k).collect())
163 }
164
165 /// The number of entries in storage.
166 pub async fn size(&self) -> Result<usize> {
167 Ok(self.items().await?.len())
168 }
169
170 /// Whether `key` is present.
171 pub async fn has(&self, key: &str) -> Result<bool> {
172 Ok(self.items().await?.iter().any(|(k, _)| k == key))
173 }
174
175 /// The security origin this storage is scoped to.
176 pub fn security_origin(&self) -> &str {
177 &self.inner.security_origin
178 }
179
180 /// `true` if this is `localStorage`, `false` for `sessionStorage`.
181 pub fn is_local_storage(&self) -> bool {
182 self.inner.is_local_storage
183 }
184}