1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Element handle retrieval methods.
use tracing::{debug, instrument};
use viewpoint_cdp::protocol::dom::{BackendNodeId, ResolveNodeParams, ResolveNodeResult};
use viewpoint_cdp::protocol::runtime::EvaluateParams;
use viewpoint_js::js;
use super::super::Locator;
use super::super::Selector;
use super::super::element::ElementHandle;
use crate::error::LocatorError;
impl<'a> Locator<'a> {
/// Get a raw element handle for the first matching element.
///
/// The returned [`ElementHandle`] provides lower-level access to the DOM element
/// and can be used for advanced operations that aren't covered by the Locator API.
///
/// **Note:** Unlike locators, element handles are bound to the specific element
/// at the time of creation. If the element is removed from the DOM, the handle
/// becomes stale.
///
/// # Example
///
/// ```no_run
/// use viewpoint_core::Page;
///
/// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
/// let handle = page.locator("button").element_handle().await?;
/// let box_model = handle.box_model().await?;
/// println!("Element at: {:?}", box_model);
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns an error if the element cannot be found.
#[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
pub async fn element_handle(&self) -> Result<ElementHandle<'a>, LocatorError> {
self.wait_for_actionable().await?;
debug!("Getting element handle");
// Handle Ref selector - lookup in ref map and resolve via CDP
if let Selector::Ref(ref_str) = &self.selector {
let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
return self.element_handle_by_backend_id(backend_node_id).await;
}
// Handle BackendNodeId selector
if let Selector::BackendNodeId(backend_node_id) = &self.selector {
return self.element_handle_by_backend_id(*backend_node_id).await;
}
// Use Runtime.evaluate to get the element object ID
let selector_expr = self.selector.to_js_expression();
let js = js! {
(function() {
const elements = @{selector_expr};
if (elements.length === 0) return null;
return elements[0];
})()
};
let params = EvaluateParams {
expression: js,
object_group: Some("viewpoint-element-handle".to_string()),
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(false),
await_promise: Some(false),
};
let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
.page
.connection()
.send_command(
"Runtime.evaluate",
Some(params),
Some(self.page.session_id()),
)
.await?;
if let Some(exception) = result.exception_details {
return Err(LocatorError::EvaluationError(exception.text));
}
let object_id = result
.result
.object_id
.ok_or_else(|| LocatorError::NotFound(format!("{:?}", self.selector)))?;
Ok(ElementHandle {
object_id,
page: self.page,
})
}
/// Get an element handle by backend node ID.
pub(super) async fn element_handle_by_backend_id(
&self,
backend_node_id: BackendNodeId,
) -> Result<ElementHandle<'a>, LocatorError> {
// Resolve the backend node ID to a RemoteObject
let result: ResolveNodeResult = self
.page
.connection()
.send_command(
"DOM.resolveNode",
Some(ResolveNodeParams {
node_id: None,
backend_node_id: Some(backend_node_id),
object_group: Some("viewpoint-element-handle".to_string()),
execution_context_id: None,
}),
Some(self.page.session_id()),
)
.await
.map_err(|_| {
LocatorError::NotFound(format!(
"Could not resolve backend node ID {backend_node_id}: element may no longer exist"
))
})?;
let object_id = result.object.object_id.ok_or_else(|| {
LocatorError::NotFound(format!(
"No object ID for backend node ID {backend_node_id}"
))
})?;
Ok(ElementHandle {
object_id,
page: self.page,
})
}
}