firefox_webdriver/browser/
selector.rs

1//! Element locator strategies.
2//!
3//! Provides Selenium-like `By` selectors for finding elements.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use firefox_webdriver::By;
9//!
10//! // CSS selector (default)
11//! let btn = tab.find_element(By::Css("#submit")).await?;
12//!
13//! // By ID (shorthand for CSS #id)
14//! let form = tab.find_element(By::Id("login-form")).await?;
15//!
16//! // By text content
17//! let link = tab.find_element(By::Text("Click here")).await?;
18//!
19//! // By partial text
20//! let link = tab.find_element(By::PartialText("Click")).await?;
21//!
22//! // By XPath
23//! let btn = tab.find_element(By::XPath("//button[@type='submit']")).await?;
24//!
25//! // By tag name
26//! let inputs = tab.find_elements(By::Tag("input")).await?;
27//! ```
28
29use serde::{Deserialize, Serialize};
30
31// ============================================================================
32// By Enum
33// ============================================================================
34
35/// Element locator strategy (like Selenium's `By`).
36///
37/// Supports multiple strategies for finding elements in the DOM.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(tag = "strategy", content = "value")]
40pub enum By {
41    /// CSS selector (most common).
42    ///
43    /// # Example
44    /// ```ignore
45    /// By::Css("#login-button")
46    /// By::Css("button.primary")
47    /// By::Css("[data-testid='submit']")
48    /// ```
49    #[serde(rename = "css")]
50    Css(String),
51
52    /// XPath expression.
53    ///
54    /// # Example
55    /// ```ignore
56    /// By::XPath("//button[@type='submit']")
57    /// By::XPath("//div[contains(@class, 'modal')]")
58    /// By::XPath("//a[text()='Login']")
59    /// ```
60    #[serde(rename = "xpath")]
61    XPath(String),
62
63    /// Exact text content match.
64    ///
65    /// Finds element where `textContent.trim() === value`.
66    ///
67    /// # Example
68    /// ```ignore
69    /// By::Text("Submit")
70    /// By::Text("Click here to continue")
71    /// ```
72    #[serde(rename = "text")]
73    Text(String),
74
75    /// Partial text content match.
76    ///
77    /// Finds element where `textContent.includes(value)`.
78    ///
79    /// # Example
80    /// ```ignore
81    /// By::PartialText("Submit")
82    /// By::PartialText("continue")
83    /// ```
84    #[serde(rename = "partialText")]
85    PartialText(String),
86
87    /// Element ID (shorthand for `#id` CSS selector).
88    ///
89    /// # Example
90    /// ```ignore
91    /// By::Id("username")  // equivalent to By::Css("#username")
92    /// ```
93    #[serde(rename = "id")]
94    Id(String),
95
96    /// Tag name.
97    ///
98    /// # Example
99    /// ```ignore
100    /// By::Tag("button")
101    /// By::Tag("input")
102    /// ```
103    #[serde(rename = "tag")]
104    Tag(String),
105
106    /// Name attribute.
107    ///
108    /// # Example
109    /// ```ignore
110    /// By::Name("email")  // equivalent to By::Css("[name='email']")
111    /// ```
112    #[serde(rename = "name")]
113    Name(String),
114
115    /// Class name (single class).
116    ///
117    /// # Example
118    /// ```ignore
119    /// By::Class("btn-primary")  // equivalent to By::Css(".btn-primary")
120    /// ```
121    #[serde(rename = "class")]
122    Class(String),
123
124    /// Link text (for `<a>` elements).
125    ///
126    /// # Example
127    /// ```ignore
128    /// By::LinkText("Home")
129    /// ```
130    #[serde(rename = "linkText")]
131    LinkText(String),
132
133    /// Partial link text (for `<a>` elements).
134    ///
135    /// # Example
136    /// ```ignore
137    /// By::PartialLinkText("Read more")
138    /// ```
139    #[serde(rename = "partialLinkText")]
140    PartialLinkText(String),
141}
142
143impl By {
144    /// Creates a CSS selector.
145    #[inline]
146    pub fn css(selector: impl Into<String>) -> Self {
147        Self::Css(selector.into())
148    }
149
150    /// Creates an XPath selector.
151    #[inline]
152    pub fn xpath(expr: impl Into<String>) -> Self {
153        Self::XPath(expr.into())
154    }
155
156    /// Creates a text content selector.
157    #[inline]
158    pub fn text(text: impl Into<String>) -> Self {
159        Self::Text(text.into())
160    }
161
162    /// Creates a partial text content selector.
163    #[inline]
164    pub fn partial_text(text: impl Into<String>) -> Self {
165        Self::PartialText(text.into())
166    }
167
168    /// Creates an ID selector.
169    #[inline]
170    pub fn id(id: impl Into<String>) -> Self {
171        Self::Id(id.into())
172    }
173
174    /// Creates a tag name selector.
175    #[inline]
176    pub fn tag(tag: impl Into<String>) -> Self {
177        Self::Tag(tag.into())
178    }
179
180    /// Creates a name attribute selector.
181    #[inline]
182    pub fn name(name: impl Into<String>) -> Self {
183        Self::Name(name.into())
184    }
185
186    /// Creates a class name selector.
187    #[inline]
188    pub fn class(class: impl Into<String>) -> Self {
189        Self::Class(class.into())
190    }
191
192    /// Creates a link text selector.
193    #[inline]
194    pub fn link_text(text: impl Into<String>) -> Self {
195        Self::LinkText(text.into())
196    }
197
198    /// Creates a partial link text selector.
199    #[inline]
200    pub fn partial_link_text(text: impl Into<String>) -> Self {
201        Self::PartialLinkText(text.into())
202    }
203
204    /// Returns the strategy name for the protocol.
205    #[must_use]
206    pub fn strategy(&self) -> &'static str {
207        match self {
208            Self::Css(_) => "css",
209            Self::XPath(_) => "xpath",
210            Self::Text(_) => "text",
211            Self::PartialText(_) => "partialText",
212            Self::Id(_) => "id",
213            Self::Tag(_) => "tag",
214            Self::Name(_) => "name",
215            Self::Class(_) => "class",
216            Self::LinkText(_) => "linkText",
217            Self::PartialLinkText(_) => "partialLinkText",
218        }
219    }
220
221    /// Returns the selector value.
222    #[must_use]
223    pub fn value(&self) -> &str {
224        match self {
225            Self::Css(v)
226            | Self::XPath(v)
227            | Self::Text(v)
228            | Self::PartialText(v)
229            | Self::Id(v)
230            | Self::Tag(v)
231            | Self::Name(v)
232            | Self::Class(v)
233            | Self::LinkText(v)
234            | Self::PartialLinkText(v) => v,
235        }
236    }
237}
238
239// ============================================================================
240// From implementations for ergonomics
241// ============================================================================
242
243impl From<&str> for By {
244    /// Converts a string to CSS selector (default).
245    fn from(s: &str) -> Self {
246        Self::Css(s.to_string())
247    }
248}
249
250impl From<String> for By {
251    /// Converts a string to CSS selector (default).
252    fn from(s: String) -> Self {
253        Self::Css(s)
254    }
255}
256
257// ============================================================================
258// Tests
259// ============================================================================
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_by_css() {
267        let by = By::Css("#login".to_string());
268        assert_eq!(by.strategy(), "css");
269        assert_eq!(by.value(), "#login");
270    }
271
272    #[test]
273    fn test_by_id() {
274        let by = By::Id("username".to_string());
275        assert_eq!(by.strategy(), "id");
276        assert_eq!(by.value(), "username");
277    }
278
279    #[test]
280    fn test_by_xpath() {
281        let by = By::XPath("//button".to_string());
282        assert_eq!(by.strategy(), "xpath");
283        assert_eq!(by.value(), "//button");
284    }
285
286    #[test]
287    fn test_by_text() {
288        let by = By::Text("Submit".to_string());
289        assert_eq!(by.strategy(), "text");
290        assert_eq!(by.value(), "Submit");
291    }
292
293    #[test]
294    fn test_from_str() {
295        let by: By = "#login".into();
296        assert!(matches!(by, By::Css(_)));
297    }
298
299    #[test]
300    fn test_builder_methods() {
301        assert!(matches!(By::css("#id"), By::Css(_)));
302        assert!(matches!(By::xpath("//div"), By::XPath(_)));
303        assert!(matches!(By::text("hello"), By::Text(_)));
304        assert!(matches!(By::id("myid"), By::Id(_)));
305    }
306}