viewpoint_core/page/locator/evaluation/
scroll.rs1use serde::Deserialize;
4use tracing::{debug, instrument};
5use viewpoint_cdp::protocol::dom::{BackendNodeId, ResolveNodeParams, ResolveNodeResult};
6use viewpoint_js::js;
7
8use super::super::Locator;
9use super::super::Selector;
10use crate::error::LocatorError;
11
12impl Locator<'_> {
13 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
32 pub async fn scroll_into_view_if_needed(&self) -> Result<(), LocatorError> {
33 let _info = self.wait_for_actionable().await?;
34
35 debug!("Scrolling element into view");
36
37 if let Selector::Ref(ref_str) = &self.selector {
39 let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
40 return self.scroll_into_view_by_backend_id(backend_node_id).await;
41 }
42
43 if let Selector::BackendNodeId(backend_node_id) = &self.selector {
45 return self.scroll_into_view_by_backend_id(*backend_node_id).await;
46 }
47
48 let selector_expr = self.selector.to_js_expression();
49 let js = js! {
50 (function() {
51 const elements = @{selector_expr};
52 if (elements.length === 0) return { found: false };
53
54 const el = elements[0];
55 el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
56 return { found: true };
57 })()
58 };
59
60 let result = self.evaluate_js(&js).await?;
61 let found = result
62 .get("found")
63 .and_then(serde_json::Value::as_bool)
64 .unwrap_or(false);
65 if !found {
66 return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
67 }
68
69 Ok(())
70 }
71
72 pub(super) async fn scroll_into_view_by_backend_id(
74 &self,
75 backend_node_id: BackendNodeId,
76 ) -> Result<(), LocatorError> {
77 let result: ResolveNodeResult = self
79 .page
80 .connection()
81 .send_command(
82 "DOM.resolveNode",
83 Some(ResolveNodeParams {
84 node_id: None,
85 backend_node_id: Some(backend_node_id),
86 object_group: Some("viewpoint-scroll".to_string()),
87 execution_context_id: None,
88 }),
89 Some(self.page.session_id()),
90 )
91 .await
92 .map_err(|_| {
93 LocatorError::NotFound(format!(
94 "Could not resolve backend node ID {backend_node_id}: element may no longer exist"
95 ))
96 })?;
97
98 let object_id = result.object.object_id.ok_or_else(|| {
99 LocatorError::NotFound(format!(
100 "No object ID for backend node ID {backend_node_id}"
101 ))
102 })?;
103
104 #[derive(Debug, Deserialize)]
106 struct CallResult {
107 result: viewpoint_cdp::protocol::runtime::RemoteObject,
108 #[serde(rename = "exceptionDetails")]
109 exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
110 }
111
112 let js_fn = js! {
113 (function() {
114 this.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
115 return { found: true };
116 })
117 };
118 let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
120
121 let call_result: CallResult = self
122 .page
123 .connection()
124 .send_command(
125 "Runtime.callFunctionOn",
126 Some(serde_json::json!({
127 "objectId": object_id,
128 "functionDeclaration": js_fn,
129 "returnByValue": true
130 })),
131 Some(self.page.session_id()),
132 )
133 .await?;
134
135 let _ = self
137 .page
138 .connection()
139 .send_command::<_, serde_json::Value>(
140 "Runtime.releaseObject",
141 Some(serde_json::json!({ "objectId": object_id })),
142 Some(self.page.session_id()),
143 )
144 .await;
145
146 if let Some(exception) = call_result.exception_details {
147 return Err(LocatorError::EvaluationError(exception.text));
148 }
149
150 Ok(())
151 }
152}