firefox_webdriver/browser/element.rs
1//! DOM element interaction and manipulation.
2//!
3//! Elements are identified by UUID and stored in the content script's
4//! internal `Map<UUID, Element>`.
5//!
6//! # Example
7//!
8//! ```ignore
9//! let element = tab.find_element("#submit-button").await?;
10//!
11//! // Get properties
12//! let text = element.get_text().await?;
13//! let value = element.get_value().await?;
14//!
15//! // Interact
16//! element.click().await?;
17//! element.type_text("Hello, World!").await?;
18//! ```
19
20// ============================================================================
21// Imports
22// ============================================================================
23
24use std::fmt;
25use std::sync::Arc;
26
27use serde_json::Value;
28
29use crate::error::{Error, Result};
30use crate::identifiers::{ElementId, FrameId, SessionId, TabId};
31use crate::protocol::{Command, ElementCommand, InputCommand, Request, Response};
32
33use super::Window;
34
35// ============================================================================
36// Types
37// ============================================================================
38
39/// Internal shared state for an element.
40pub(crate) struct ElementInner {
41 /// This element's unique ID.
42 pub id: ElementId,
43
44 /// Tab ID where this element exists.
45 pub tab_id: TabId,
46
47 /// Frame ID where this element exists.
48 pub frame_id: FrameId,
49
50 /// Session ID.
51 pub session_id: SessionId,
52
53 /// Parent window.
54 pub window: Option<Window>,
55}
56
57// ============================================================================
58// Element
59// ============================================================================
60
61/// A handle to a DOM element in a browser tab.
62///
63/// Elements are identified by a UUID stored in the extension's content script.
64/// Operations use generic dynamic property access (`element[method]()`).
65///
66/// # Example
67///
68/// ```ignore
69/// let element = tab.find_element("input[name='email']").await?;
70///
71/// // Set value and submit
72/// element.set_value("user@example.com").await?;
73/// element.type_text("\n").await?; // Press Enter
74/// ```
75#[derive(Clone)]
76pub struct Element {
77 /// Shared inner state.
78 pub(crate) inner: Arc<ElementInner>,
79}
80
81// ============================================================================
82// Element - Display
83// ============================================================================
84
85impl fmt::Debug for Element {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.debug_struct("Element")
88 .field("id", &self.inner.id)
89 .field("tab_id", &self.inner.tab_id)
90 .field("frame_id", &self.inner.frame_id)
91 .finish_non_exhaustive()
92 }
93}
94
95// ============================================================================
96// Element - Constructor
97// ============================================================================
98
99impl Element {
100 /// Creates a new element handle.
101 pub(crate) fn new(
102 id: ElementId,
103 tab_id: TabId,
104 frame_id: FrameId,
105 session_id: SessionId,
106 window: Option<Window>,
107 ) -> Self {
108 Self {
109 inner: Arc::new(ElementInner {
110 id,
111 tab_id,
112 frame_id,
113 session_id,
114 window,
115 }),
116 }
117 }
118}
119
120// ============================================================================
121// Element - Accessors
122// ============================================================================
123
124impl Element {
125 /// Returns this element's ID.
126 #[inline]
127 #[must_use]
128 pub fn id(&self) -> &ElementId {
129 &self.inner.id
130 }
131
132 /// Returns the tab ID where this element exists.
133 #[inline]
134 #[must_use]
135 pub fn tab_id(&self) -> TabId {
136 self.inner.tab_id
137 }
138
139 /// Returns the frame ID where this element exists.
140 #[inline]
141 #[must_use]
142 pub fn frame_id(&self) -> FrameId {
143 self.inner.frame_id
144 }
145}
146
147// ============================================================================
148// Element - Actions
149// ============================================================================
150
151impl Element {
152 /// Clicks the element.
153 ///
154 /// Uses `element.click()` internally.
155 pub async fn click(&self) -> Result<()> {
156 self.call_method("click", vec![]).await?;
157 Ok(())
158 }
159
160 /// Focuses the element.
161 pub async fn focus(&self) -> Result<()> {
162 self.call_method("focus", vec![]).await?;
163 Ok(())
164 }
165
166 /// Blurs (unfocuses) the element.
167 pub async fn blur(&self) -> Result<()> {
168 self.call_method("blur", vec![]).await?;
169 Ok(())
170 }
171
172 /// Clears the element's value.
173 ///
174 /// Sets `element.value = ""`.
175 pub async fn clear(&self) -> Result<()> {
176 self.set_property("value", Value::String(String::new()))
177 .await
178 }
179}
180
181// ============================================================================
182// Element - Properties
183// ============================================================================
184
185impl Element {
186 /// Gets the element's text content.
187 pub async fn get_text(&self) -> Result<String> {
188 let value = self.get_property("textContent").await?;
189 Ok(value.as_str().unwrap_or("").to_string())
190 }
191
192 /// Gets the element's inner HTML.
193 pub async fn get_inner_html(&self) -> Result<String> {
194 let value = self.get_property("innerHTML").await?;
195 Ok(value.as_str().unwrap_or("").to_string())
196 }
197
198 /// Gets the element's value (for input elements).
199 pub async fn get_value(&self) -> Result<String> {
200 let value = self.get_property("value").await?;
201 Ok(value.as_str().unwrap_or("").to_string())
202 }
203
204 /// Sets the element's value (for input elements).
205 pub async fn set_value(&self, value: &str) -> Result<()> {
206 self.set_property("value", Value::String(value.to_string()))
207 .await
208 }
209
210 /// Gets an attribute value.
211 ///
212 /// Returns `None` if the attribute doesn't exist.
213 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
214 let result = self
215 .call_method("getAttribute", vec![Value::String(name.to_string())])
216 .await?;
217 Ok(result.as_str().map(|s| s.to_string()))
218 }
219
220 /// Checks if the element is displayed.
221 ///
222 /// Returns `false` if `offsetParent` is null (element is hidden).
223 pub async fn is_displayed(&self) -> Result<bool> {
224 let offset_parent = self.get_property("offsetParent").await?;
225 Ok(!offset_parent.is_null())
226 }
227
228 /// Checks if the element is enabled.
229 ///
230 /// Returns `true` if `disabled` property is false or absent.
231 pub async fn is_enabled(&self) -> Result<bool> {
232 let disabled = self.get_property("disabled").await?;
233 Ok(!disabled.as_bool().unwrap_or(false))
234 }
235}
236
237// ============================================================================
238// Element - Keyboard Input
239// ============================================================================
240
241impl Element {
242 /// Types a single key with optional modifiers.
243 ///
244 /// Dispatches full keyboard event sequence: keydown → input → keypress → keyup.
245 ///
246 /// # Arguments
247 ///
248 /// * `key` - Key value (e.g., "a", "Enter")
249 /// * `code` - Key code (e.g., "KeyA", "Enter")
250 /// * `key_code` - Legacy keyCode number
251 /// * `printable` - Whether key produces visible output
252 /// * `ctrl` - Ctrl modifier
253 /// * `shift` - Shift modifier
254 /// * `alt` - Alt modifier
255 /// * `meta` - Meta modifier
256 #[allow(clippy::too_many_arguments)]
257 pub async fn type_key(
258 &self,
259 key: &str,
260 code: &str,
261 key_code: u32,
262 printable: bool,
263 ctrl: bool,
264 shift: bool,
265 alt: bool,
266 meta: bool,
267 ) -> Result<()> {
268 let command = Command::Input(InputCommand::TypeKey {
269 element_id: self.inner.id.clone(),
270 key: key.to_string(),
271 code: code.to_string(),
272 key_code,
273 printable,
274 ctrl,
275 shift,
276 alt,
277 meta,
278 });
279
280 self.send_command(command).await?;
281 Ok(())
282 }
283
284 /// Types a character with default key properties.
285 ///
286 /// Convenience method that uses `type_text` internally for reliability.
287 pub async fn type_char(&self, c: char) -> Result<()> {
288 self.type_text(&c.to_string()).await
289 }
290
291 /// Types a text string character by character.
292 ///
293 /// Each character goes through full keyboard event sequence.
294 /// This is slower but more realistic than `set_value`.
295 ///
296 /// # Example
297 ///
298 /// ```ignore
299 /// element.type_text("Hello, World!").await?;
300 /// ```
301 pub async fn type_text(&self, text: &str) -> Result<()> {
302 let command = Command::Input(InputCommand::TypeText {
303 element_id: self.inner.id.clone(),
304 text: text.to_string(),
305 });
306
307 self.send_command(command).await?;
308 Ok(())
309 }
310}
311
312// ============================================================================
313// Element - Mouse Input
314// ============================================================================
315
316impl Element {
317 /// Clicks the element using mouse events.
318 ///
319 /// Dispatches: mousemove → mousedown → mouseup → click.
320 /// This is more realistic than `click()` which uses `element.click()`.
321 ///
322 /// # Arguments
323 ///
324 /// * `button` - Mouse button (0=left, 1=middle, 2=right)
325 pub async fn mouse_click(&self, button: u8) -> Result<()> {
326 let command = Command::Input(InputCommand::MouseClick {
327 element_id: Some(self.inner.id.clone()),
328 x: None,
329 y: None,
330 button,
331 });
332
333 self.send_command(command).await?;
334 Ok(())
335 }
336
337 /// Moves mouse to the element center.
338 pub async fn mouse_move(&self) -> Result<()> {
339 let command = Command::Input(InputCommand::MouseMove {
340 element_id: Some(self.inner.id.clone()),
341 x: None,
342 y: None,
343 });
344
345 self.send_command(command).await?;
346 Ok(())
347 }
348
349 /// Presses mouse button down on the element (without release).
350 ///
351 /// Dispatches only mousedown event.
352 /// Use with `mouse_up()` for drag operations.
353 ///
354 /// # Arguments
355 ///
356 /// * `button` - Mouse button (0=left, 1=middle, 2=right)
357 pub async fn mouse_down(&self, button: u8) -> Result<()> {
358 let command = Command::Input(InputCommand::MouseDown {
359 element_id: Some(self.inner.id.clone()),
360 x: None,
361 y: None,
362 button,
363 });
364
365 self.send_command(command).await?;
366 Ok(())
367 }
368
369 /// Releases mouse button on the element.
370 ///
371 /// Dispatches only mouseup event.
372 /// Use with `mouse_down()` for drag operations.
373 ///
374 /// # Arguments
375 ///
376 /// * `button` - Mouse button (0=left, 1=middle, 2=right)
377 pub async fn mouse_up(&self, button: u8) -> Result<()> {
378 let command = Command::Input(InputCommand::MouseUp {
379 element_id: Some(self.inner.id.clone()),
380 x: None,
381 y: None,
382 button,
383 });
384
385 self.send_command(command).await?;
386 Ok(())
387 }
388}
389
390// ============================================================================
391// Element - Nested Search
392// ============================================================================
393
394impl Element {
395 /// Finds a child element by CSS selector.
396 ///
397 /// # Errors
398 ///
399 /// Returns [`Error::ElementNotFound`] if no matching element exists.
400 pub async fn find_element(&self, selector: &str) -> Result<Element> {
401 let command = Command::Element(ElementCommand::Find {
402 selector: selector.to_string(),
403 parent_id: Some(self.inner.id.clone()),
404 });
405
406 let response = self.send_command(command).await?;
407
408 let element_id = response
409 .result
410 .as_ref()
411 .and_then(|v| v.get("elementId"))
412 .and_then(|v| v.as_str())
413 .ok_or_else(|| {
414 Error::element_not_found(selector, self.inner.tab_id, self.inner.frame_id)
415 })?;
416
417 Ok(Element::new(
418 ElementId::new(element_id),
419 self.inner.tab_id,
420 self.inner.frame_id,
421 self.inner.session_id,
422 self.inner.window.clone(),
423 ))
424 }
425
426 /// Finds all child elements matching a CSS selector.
427 ///
428 /// Returns an empty vector if no elements match.
429 pub async fn find_elements(&self, selector: &str) -> Result<Vec<Element>> {
430 let command = Command::Element(ElementCommand::FindAll {
431 selector: selector.to_string(),
432 parent_id: Some(self.inner.id.clone()),
433 });
434
435 let response = self.send_command(command).await?;
436
437 let element_ids = response
438 .result
439 .as_ref()
440 .and_then(|v| v.get("elementIds"))
441 .and_then(|v| v.as_array())
442 .map(|arr| {
443 arr.iter()
444 .filter_map(|v| v.as_str())
445 .map(|id| {
446 Element::new(
447 ElementId::new(id),
448 self.inner.tab_id,
449 self.inner.frame_id,
450 self.inner.session_id,
451 self.inner.window.clone(),
452 )
453 })
454 .collect()
455 })
456 .unwrap_or_default();
457
458 Ok(element_ids)
459 }
460}
461
462// ============================================================================
463// Element - Generic Property Access
464// ============================================================================
465
466impl Element {
467 /// Gets a property value via `element[name]`.
468 ///
469 /// # Arguments
470 ///
471 /// * `name` - Property name (e.g., "value", "textContent")
472 pub async fn get_property(&self, name: &str) -> Result<Value> {
473 let command = Command::Element(ElementCommand::GetProperty {
474 element_id: self.inner.id.clone(),
475 name: name.to_string(),
476 });
477
478 let response = self.send_command(command).await?;
479
480 Ok(response
481 .result
482 .and_then(|v| v.get("value").cloned())
483 .unwrap_or(Value::Null))
484 }
485
486 /// Sets a property value via `element[name] = value`.
487 ///
488 /// # Arguments
489 ///
490 /// * `name` - Property name
491 /// * `value` - Value to set
492 pub async fn set_property(&self, name: &str, value: Value) -> Result<()> {
493 let command = Command::Element(ElementCommand::SetProperty {
494 element_id: self.inner.id.clone(),
495 name: name.to_string(),
496 value,
497 });
498
499 self.send_command(command).await?;
500 Ok(())
501 }
502
503 /// Calls a method via `element[name](...args)`.
504 ///
505 /// # Arguments
506 ///
507 /// * `name` - Method name
508 /// * `args` - Method arguments
509 pub async fn call_method(&self, name: &str, args: Vec<Value>) -> Result<Value> {
510 let command = Command::Element(ElementCommand::CallMethod {
511 element_id: self.inner.id.clone(),
512 name: name.to_string(),
513 args,
514 });
515
516 let response = self.send_command(command).await?;
517
518 Ok(response
519 .result
520 .and_then(|v| v.get("value").cloned())
521 .unwrap_or(Value::Null))
522 }
523}
524
525// ============================================================================
526// Element - Internal
527// ============================================================================
528
529impl Element {
530 /// Sends a command and returns the response.
531 async fn send_command(&self, command: Command) -> Result<Response> {
532 let window = self
533 .inner
534 .window
535 .as_ref()
536 .ok_or_else(|| Error::protocol("Element has no associated window"))?;
537
538 let request = Request::new(self.inner.tab_id, self.inner.frame_id, command);
539
540 window
541 .inner
542 .pool
543 .send(window.inner.session_id, request)
544 .await
545 }
546}
547
548// ============================================================================
549// Tests
550// ============================================================================
551
552#[cfg(test)]
553mod tests {
554 use super::Element;
555
556 #[test]
557 fn test_element_is_clone() {
558 fn assert_clone<T: Clone>() {}
559 assert_clone::<Element>();
560 }
561
562 #[test]
563 fn test_element_is_debug() {
564 fn assert_debug<T: std::fmt::Debug>() {}
565 assert_debug::<Element>();
566 }
567}