Skip to main content

playwright_rs/protocol/
selectors.rs

1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// Selectors — custom selector engine registration
5//
6// Architecture Reference:
7// - Python: playwright-python/playwright/_impl/_selectors.py
8// - JavaScript: playwright/packages/playwright-core/src/client/selectors.ts
9// - Docs: https://playwright.dev/docs/api/class-selectors
10//
11// IMPORTANT: Unlike most Playwright objects, Selectors is NOT a ChannelOwner.
12// It does not have its own GUID or server-side representation. Instead, it is
13// a pure client-side coordinator that:
14// 1. Tracks registered selector engines and test ID attribute
15// 2. Applies state to all registered BrowserContext objects via their channels
16// 3. Stores engine definitions so they can be re-applied to new contexts
17//
18// This matches the Python and JavaScript implementations exactly.
19
20//! Selectors — register custom selector engines and configure test ID attribute.
21//!
22//! Selectors can be used to install custom selector engines. Custom selector engines
23//! are consulted when Playwright evaluates CSS, XPath, and other selectors.
24//!
25//! Unlike most Playwright objects, `Selectors` is a pure client-side coordinator;
26//! it does not correspond to a server-side protocol object. It keeps track of
27//! registered engines and the test ID attribute, and propagates changes to all
28//! active browser contexts.
29//!
30//! # Example
31//!
32//! ```ignore
33//! use playwright_rs::protocol::Playwright;
34//!
35//! #[tokio::main]
36//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
37//!     let playwright = Playwright::launch().await?;
38//!
39//!     // Access the shared Selectors instance
40//!     let selectors = playwright.selectors();
41//!
42//!     // Register a custom selector engine
43//!     let script = r#"
44//!         {
45//!             query(root, selector) {
46//!                 return root.querySelector(selector);
47//!             },
48//!             queryAll(root, selector) {
49//!                 return Array.from(root.querySelectorAll(selector));
50//!             }
51//!         }
52//!     "#;
53//!     selectors.register("tag", script, None).await?;
54//!
55//!     // Change the attribute used by get_by_test_id()
56//!     selectors.set_test_id_attribute("data-custom-id").await?;
57//!
58//!     playwright.shutdown().await?;
59//!     Ok(())
60//! }
61//! ```
62//!
63//! See: <https://playwright.dev/docs/api/class-selectors>
64
65use crate::error::{Error, Result};
66use crate::server::channel::Channel;
67use parking_lot::Mutex;
68use std::collections::HashSet;
69use std::sync::Arc;
70
71/// A registered selector engine definition.
72#[derive(Clone, Debug)]
73struct SelectorEngine {
74    name: String,
75    script: String,
76    content_script: bool,
77}
78
79/// Inner shared state for `Selectors`.
80struct SelectorsInner {
81    /// Registered selector engine definitions (kept so they can be re-applied to new contexts).
82    engines: Vec<SelectorEngine>,
83    /// Names of registered engines (for duplicate detection).
84    engine_names: HashSet<String>,
85    /// Custom test ID attribute name, if overridden.
86    test_id_attribute: Option<String>,
87    /// Active browser contexts that need to receive selector updates.
88    contexts: Vec<Channel>,
89}
90
91/// Selectors — manages custom selector engines and test ID attribute configuration.
92///
93/// An instance of Selectors is available via [`crate::protocol::Playwright::selectors()`].
94///
95/// Selector engines registered here are applied to all browser contexts. Register
96/// engines **before** creating pages that will use them.
97///
98/// See: <https://playwright.dev/docs/api/class-selectors>
99#[derive(Clone)]
100pub struct Selectors {
101    inner: Arc<Mutex<SelectorsInner>>,
102}
103
104impl Selectors {
105    /// Creates a new, empty Selectors coordinator.
106    pub fn new() -> Self {
107        Self {
108            inner: Arc::new(Mutex::new(SelectorsInner {
109                engines: Vec::new(),
110                engine_names: HashSet::new(),
111                test_id_attribute: None,
112                contexts: Vec::new(),
113            })),
114        }
115    }
116
117    /// Registers a context's channel so it receives selector updates.
118    ///
119    /// Called by BrowserContext when it is created, so that:
120    /// 1. All previously registered engines are applied to it immediately.
121    /// 2. Future `register()` / `set_test_id_attribute()` calls reach it.
122    pub async fn add_context(&self, channel: Channel) -> Result<()> {
123        let (engines_snapshot, attr_snapshot) = {
124            let mut inner = self.inner.lock();
125            inner.contexts.push(channel.clone());
126            (inner.engines.clone(), inner.test_id_attribute.clone())
127        };
128
129        // Re-apply all previously registered engines to this new context.
130        for engine in &engines_snapshot {
131            let params = serde_json::json!({
132                "selectorEngine": {
133                    "name": engine.name,
134                    "source": engine.script,
135                    "contentScript": engine.content_script,
136                }
137            });
138            channel
139                .send_no_result("registerSelectorEngine", params)
140                .await?;
141        }
142
143        // Apply the current test ID attribute, if any.
144        if let Some(attr) = attr_snapshot {
145            channel
146                .send_no_result(
147                    "setTestIdAttributeName",
148                    serde_json::json!({ "testIdAttributeName": attr }),
149                )
150                .await?;
151        }
152
153        Ok(())
154    }
155
156    /// Removes a context's channel when it is closed.
157    ///
158    /// Called by BrowserContext on close to avoid sending messages to dead channels.
159    pub fn remove_context(&self, channel: &Channel) {
160        let mut inner = self.inner.lock();
161        inner.contexts.retain(|c| c.guid() != channel.guid());
162    }
163
164    /// Registers a custom selector engine.
165    ///
166    /// The script must evaluate to an object with `query` and `queryAll` methods:
167    ///
168    /// ```text
169    /// {
170    ///     query(root, selector) { return root.querySelector(selector); },
171    ///     queryAll(root, selector) { return Array.from(root.querySelectorAll(selector)); }
172    /// }
173    /// ```
174    ///
175    /// After registration, use the engine with `page.locator("name=selector")`.
176    ///
177    /// # Arguments
178    ///
179    /// * `name` - Name to assign to the selector engine.
180    /// * `script` - JavaScript string that evaluates to a selector engine factory.
181    /// * `content_script` - Whether to run the engine in isolated content script mode.
182    ///   Defaults to `false`.
183    ///
184    /// # Errors
185    ///
186    /// Returns error if:
187    /// - A selector engine with the same name is already registered
188    /// - Any context rejects the registration (invalid script, etc.)
189    ///
190    /// See: <https://playwright.dev/docs/api/class-selectors#selectors-register>
191    pub async fn register(
192        &self,
193        name: &str,
194        script: &str,
195        content_script: Option<bool>,
196    ) -> Result<()> {
197        let content_script = content_script.unwrap_or(false);
198
199        let channels_snapshot = {
200            let mut inner = self.inner.lock();
201
202            if inner.engine_names.contains(name) {
203                return Err(Error::ProtocolError(format!(
204                    "Selector engine '{name}' is already registered"
205                )));
206            }
207
208            inner.engine_names.insert(name.to_string());
209            inner.engines.push(SelectorEngine {
210                name: name.to_string(),
211                script: script.to_string(),
212                content_script,
213            });
214
215            inner.contexts.clone()
216        };
217
218        // The protocol expects { "selectorEngine": { "name": ..., "source": ..., "contentScript": ... } }
219        let params = serde_json::json!({
220            "selectorEngine": {
221                "name": name,
222                "source": script,
223                "contentScript": content_script,
224            }
225        });
226
227        // Broadcast to all active contexts.
228        for channel in &channels_snapshot {
229            channel
230                .send_no_result("registerSelectorEngine", params.clone())
231                .await?;
232        }
233
234        Ok(())
235    }
236
237    /// Returns the current test ID attribute name used by `get_by_test_id()` locators.
238    ///
239    /// Defaults to `"data-testid"`.
240    pub fn test_id_attribute(&self) -> String {
241        self.inner
242            .lock()
243            .test_id_attribute
244            .clone()
245            .unwrap_or_else(|| "data-testid".to_string())
246    }
247
248    /// Sets the attribute used by `get_by_test_id()` locators.
249    ///
250    /// By default, Playwright uses `data-testid`. Calling this method changes the
251    /// attribute name for all current and future contexts.
252    ///
253    /// # Arguments
254    ///
255    /// * `attribute` - The attribute name to use as the test ID (e.g., `"data-custom-id"`).
256    ///
257    /// # Errors
258    ///
259    /// Returns error if any context rejects the update.
260    ///
261    /// See: <https://playwright.dev/docs/api/class-selectors#selectors-set-test-id-attribute>
262    pub async fn set_test_id_attribute(&self, attribute: &str) -> Result<()> {
263        let channels_snapshot = {
264            let mut inner = self.inner.lock();
265            inner.test_id_attribute = Some(attribute.to_string());
266            inner.contexts.clone()
267        };
268
269        let params = serde_json::json!({ "testIdAttributeName": attribute });
270
271        for channel in &channels_snapshot {
272            channel
273                .send_no_result("setTestIdAttributeName", params.clone())
274                .await?;
275        }
276
277        Ok(())
278    }
279}
280
281impl Default for Selectors {
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287impl std::fmt::Debug for Selectors {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        let inner = self.inner.lock();
290        f.debug_struct("Selectors")
291            .field("engines", &inner.engines)
292            .field("test_id_attribute", &inner.test_id_attribute)
293            .finish()
294    }
295}