Skip to main content

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}