viewpoint_core/page/locator/debug/
mod.rs1use std::time::Duration;
6
7use serde::Deserialize;
8use tracing::{debug, instrument};
9use viewpoint_cdp::protocol::dom::{BackendNodeId, ResolveNodeParams, ResolveNodeResult};
10use viewpoint_js::js;
11
12use super::Locator;
13use super::Selector;
14use crate::error::LocatorError;
15
16impl Locator<'_> {
17 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
42 pub async fn highlight(&self) -> Result<(), LocatorError> {
43 self.highlight_for(Duration::from_secs(2)).await
44 }
45
46 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
56 pub async fn highlight_for(&self, duration: Duration) -> Result<(), LocatorError> {
57 self.wait_for_actionable().await?;
58
59 debug!(?duration, "Highlighting element");
60
61 if let Selector::Ref(ref_str) = &self.selector {
63 let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
64 return self
65 .highlight_by_backend_id(backend_node_id, duration)
66 .await;
67 }
68
69 if let Selector::BackendNodeId(backend_node_id) = &self.selector {
71 return self
72 .highlight_by_backend_id(*backend_node_id, duration)
73 .await;
74 }
75
76 let selector_expr = self.selector.to_js_expression();
78 let highlight_js = js! {
79 (function() {
80 const elements = @{selector_expr};
81 if (elements.length === 0) return { found: false };
82
83 const el = elements[0];
84 const originalOutline = el.style.outline;
85 const originalOutlineOffset = el.style.outlineOffset;
86 const originalTransition = el.style.transition;
87
88 el.style.transition = "outline 0.2s ease-in-out";
90 el.style.outline = "3px solid #ff00ff";
91 el.style.outlineOffset = "2px";
92
93 el.__viewpoint_original_outline = originalOutline;
95 el.__viewpoint_original_outline_offset = originalOutlineOffset;
96 el.__viewpoint_original_transition = originalTransition;
97
98 return { found: true };
99 })()
100 };
101
102 let result = self.evaluate_js(&highlight_js).await?;
103 let found = result
104 .get("found")
105 .and_then(serde_json::Value::as_bool)
106 .unwrap_or(false);
107 if !found {
108 return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
109 }
110
111 tokio::time::sleep(duration).await;
113
114 let cleanup_js = js! {
116 (function() {
117 const elements = @{selector_expr};
118 if (elements.length === 0) return;
119
120 const el = elements[0];
121 el.style.outline = el.__viewpoint_original_outline || "";
122 el.style.outlineOffset = el.__viewpoint_original_outline_offset || "";
123 el.style.transition = el.__viewpoint_original_transition || "";
124
125 delete el.__viewpoint_original_outline;
126 delete el.__viewpoint_original_outline_offset;
127 delete el.__viewpoint_original_transition;
128 })()
129 };
130
131 let _ = self.evaluate_js(&cleanup_js).await;
133
134 Ok(())
135 }
136
137 async fn highlight_by_backend_id(
139 &self,
140 backend_node_id: BackendNodeId,
141 duration: Duration,
142 ) -> Result<(), LocatorError> {
143 let result: ResolveNodeResult = self
145 .page
146 .connection()
147 .send_command(
148 "DOM.resolveNode",
149 Some(ResolveNodeParams {
150 node_id: None,
151 backend_node_id: Some(backend_node_id),
152 object_group: Some("viewpoint-highlight".to_string()),
153 execution_context_id: None,
154 }),
155 Some(self.page.session_id()),
156 )
157 .await
158 .map_err(|_| {
159 LocatorError::NotFound(format!(
160 "Could not resolve backend node ID {backend_node_id}: element may no longer exist"
161 ))
162 })?;
163
164 let object_id = result.object.object_id.ok_or_else(|| {
165 LocatorError::NotFound(format!(
166 "No object ID for backend node ID {backend_node_id}"
167 ))
168 })?;
169
170 #[derive(Debug, Deserialize)]
172 struct CallResult {
173 result: viewpoint_cdp::protocol::runtime::RemoteObject,
174 #[serde(rename = "exceptionDetails")]
175 exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
176 }
177
178 let js_highlight = js! {
179 (function() {
180 const el = this;
181 const originalOutline = el.style.outline;
182 const originalOutlineOffset = el.style.outlineOffset;
183 const originalTransition = el.style.transition;
184
185 el.style.transition = "outline 0.2s ease-in-out";
187 el.style.outline = "3px solid #ff00ff";
188 el.style.outlineOffset = "2px";
189
190 el.__viewpoint_original_outline = originalOutline;
192 el.__viewpoint_original_outline_offset = originalOutlineOffset;
193 el.__viewpoint_original_transition = originalTransition;
194
195 return { found: true };
196 })
197 };
198 let js_highlight = js_highlight.trim_start_matches('(').trim_end_matches(')');
200
201 let call_result: CallResult = self
202 .page
203 .connection()
204 .send_command(
205 "Runtime.callFunctionOn",
206 Some(serde_json::json!({
207 "objectId": object_id,
208 "functionDeclaration": js_highlight,
209 "returnByValue": true
210 })),
211 Some(self.page.session_id()),
212 )
213 .await?;
214
215 if let Some(exception) = call_result.exception_details {
216 let _ = self
217 .page
218 .connection()
219 .send_command::<_, serde_json::Value>(
220 "Runtime.releaseObject",
221 Some(serde_json::json!({ "objectId": object_id })),
222 Some(self.page.session_id()),
223 )
224 .await;
225 return Err(LocatorError::EvaluationError(exception.text));
226 }
227
228 tokio::time::sleep(duration).await;
230
231 let js_remove_highlight = js! {
233 (function() {
234 const el = this;
235 el.style.outline = el.__viewpoint_original_outline || "";
236 el.style.outlineOffset = el.__viewpoint_original_outline_offset || "";
237 el.style.transition = el.__viewpoint_original_transition || "";
238
239 delete el.__viewpoint_original_outline;
240 delete el.__viewpoint_original_outline_offset;
241 delete el.__viewpoint_original_transition;
242 })
243 };
244 let js_remove_highlight = js_remove_highlight
246 .trim_start_matches('(')
247 .trim_end_matches(')');
248
249 let _ = self
250 .page
251 .connection()
252 .send_command::<_, CallResult>(
253 "Runtime.callFunctionOn",
254 Some(serde_json::json!({
255 "objectId": object_id,
256 "functionDeclaration": js_remove_highlight,
257 "returnByValue": true
258 })),
259 Some(self.page.session_id()),
260 )
261 .await;
262
263 let _ = self
265 .page
266 .connection()
267 .send_command::<_, serde_json::Value>(
268 "Runtime.releaseObject",
269 Some(serde_json::json!({ "objectId": object_id })),
270 Some(self.page.session_id()),
271 )
272 .await;
273
274 Ok(())
275 }
276}