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}