1use serde_json::Value;
2use tokio::sync::oneshot;
3
4use crate::error::NightFuryError;
5use crate::session::BrowserSession;
6use crate::worker::frame_helpers::frame_wrap_js;
7use crate::worker::helpers::{js_query_string, wait_for_selector};
8use crate::worker::WorkerState;
9
10#[non_exhaustive]
16pub enum QueryCmd {
17 GetText {
18 selector: String,
19 reply: oneshot::Sender<Result<String, String>>,
20 },
21 GetValue {
22 selector: String,
23 reply: oneshot::Sender<Result<String, String>>,
24 },
25 GetAttribute {
26 selector: String,
27 attribute: String,
28 reply: oneshot::Sender<Result<String, String>>,
29 },
30 GetHtml {
31 selector: String,
32 reply: oneshot::Sender<Result<String, String>>,
33 },
34 GetCount {
35 selector: String,
36 reply: oneshot::Sender<Result<usize, String>>,
37 },
38 GetBox {
39 selector: String,
40 reply: oneshot::Sender<Result<Value, String>>,
41 },
42 IsChecked {
43 selector: String,
44 reply: oneshot::Sender<Result<bool, String>>,
45 },
46 IsVisible {
47 selector: String,
48 reply: oneshot::Sender<Result<bool, String>>,
49 },
50 IsEnabled {
51 selector: String,
52 reply: oneshot::Sender<Result<bool, String>>,
53 },
54 WaitForSelector {
55 selector: String,
56 timeout_ms: u64,
57 reply: oneshot::Sender<Result<String, String>>,
58 },
59}
60
61impl QueryCmd {
66 pub(crate) async fn dispatch(self, state: &WorkerState) {
67 match self {
68 QueryCmd::GetText { selector, reply } => handle_get_text(state, selector, reply).await,
69 QueryCmd::GetValue { selector, reply } => {
70 handle_get_value(state, selector, reply).await
71 }
72 QueryCmd::GetAttribute {
73 selector,
74 attribute,
75 reply,
76 } => handle_get_attribute(state, selector, attribute, reply).await,
77 QueryCmd::GetHtml { selector, reply } => handle_get_html(state, selector, reply).await,
78 QueryCmd::GetCount { selector, reply } => {
79 handle_get_count(state, selector, reply).await
80 }
81 QueryCmd::GetBox { selector, reply } => handle_get_box(state, selector, reply).await,
82 QueryCmd::IsChecked { selector, reply } => {
83 handle_is_checked(state, selector, reply).await
84 }
85 QueryCmd::IsVisible { selector, reply } => {
86 handle_is_visible(state, selector, reply).await
87 }
88 QueryCmd::IsEnabled { selector, reply } => {
89 handle_is_enabled(state, selector, reply).await
90 }
91 QueryCmd::WaitForSelector {
92 selector,
93 timeout_ms,
94 reply,
95 } => {
96 let result = wait_for_selector(
97 &state.tabs[state.active_tab].page,
98 &selector,
99 timeout_ms,
100 &state.tabs[state.active_tab].active_frame.clone(),
101 )
102 .await;
103 let _ = reply.send(result);
104 }
105 }
106 }
107}
108
109async fn handle_get_text(
114 state: &WorkerState,
115 selector: String,
116 reply: oneshot::Sender<Result<String, String>>,
117) {
118 let result = js_query_string(
119 &state.tabs[state.active_tab].page,
120 &selector,
121 "innerText",
122 "innerText",
123 &state.tabs[state.active_tab].active_frame.clone(),
124 )
125 .await;
126 let _ = reply.send(result);
127}
128
129async fn handle_get_value(
130 state: &WorkerState,
131 selector: String,
132 reply: oneshot::Sender<Result<String, String>>,
133) {
134 let result = js_query_string(
135 &state.tabs[state.active_tab].page,
136 &selector,
137 "value",
138 "value",
139 &state.tabs[state.active_tab].active_frame.clone(),
140 )
141 .await;
142 let _ = reply.send(result);
143}
144
145async fn handle_get_attribute(
146 state: &WorkerState,
147 selector: String,
148 attribute: String,
149 reply: oneshot::Sender<Result<String, String>>,
150) {
151 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
152 let attr_json = serde_json::to_string(&attribute).unwrap_or_default();
153 let inner = format!(
154 "(function() {{ \
155 var el = document.querySelector({sel_json}); \
156 if (!el) return null; \
157 return el.getAttribute({attr_json}); \
158 }})()"
159 );
160 let js = frame_wrap_js(&state.tabs[state.active_tab].active_frame, &inner);
161 let result = state.tabs[state.active_tab]
162 .page
163 .evaluate_stealth(&js)
164 .await
165 .map_err(|e| format!("getAttribute eval failed: {e}"))
166 .and_then(|v| match v.as_ref() {
167 Some(serde_json::Value::String(s)) => Ok(s.clone()),
168 Some(serde_json::Value::Null) | None => {
169 Err(format!("attribute \"{attribute}\" not found on {selector}"))
170 }
171 Some(other) => Ok(other.to_string()),
172 });
173 let _ = reply.send(result);
174}
175
176async fn handle_get_html(
177 state: &WorkerState,
178 selector: String,
179 reply: oneshot::Sender<Result<String, String>>,
180) {
181 let result = async {
182 let active_frame = state.tabs[state.active_tab].active_frame.clone();
183 let page = &state.tabs[state.active_tab].page;
184 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
185 let inner = format!(
186 "(function() {{ \
187 var el = document.querySelector({sel_json}); \
188 if (!el) return null; \
189 return el.innerHTML; \
190 }})()"
191 );
192 let js = frame_wrap_js(&active_frame, &inner);
193 let val = page
194 .evaluate_stealth(&js)
195 .await
196 .map_err(|e| format!("get_html eval failed: {e}"))?;
197 match val.as_ref() {
198 Some(serde_json::Value::String(s)) => Ok(s.clone()),
199 Some(serde_json::Value::Null) | None => Err(format!("Element not found: {selector}")),
200 _ => Err("Unexpected value from get_html".to_string()),
201 }
202 }
203 .await;
204 let _ = reply.send(result);
205}
206
207async fn handle_get_count(
208 state: &WorkerState,
209 selector: String,
210 reply: oneshot::Sender<Result<usize, String>>,
211) {
212 let result = async {
213 let active_frame = state.tabs[state.active_tab].active_frame.clone();
214 let page = &state.tabs[state.active_tab].page;
215 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
216 let js = frame_wrap_js(
217 &active_frame,
218 &format!("document.querySelectorAll({sel_json}).length"),
219 );
220 let val = page
221 .evaluate_stealth(&js)
222 .await
223 .map_err(|e| format!("get_count eval failed: {e}"))?;
224 match val.as_ref() {
225 Some(serde_json::Value::Number(n)) => n
226 .as_u64()
227 .map(|v| v as usize)
228 .ok_or_else(|| "get_count returned non-integer".to_string()),
229 _ => Err("Unexpected value from get_count".to_string()),
230 }
231 }
232 .await;
233 let _ = reply.send(result);
234}
235
236async fn handle_get_box(
237 state: &WorkerState,
238 selector: String,
239 reply: oneshot::Sender<Result<Value, String>>,
240) {
241 let result = async {
242 let active_frame = state.tabs[state.active_tab].active_frame.clone();
243 let page = &state.tabs[state.active_tab].page;
244 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
245 let inner = format!(
246 "(function() {{ \
247 var el = document.querySelector({sel_json}); \
248 if (!el) return null; \
249 var r = el.getBoundingClientRect(); \
250 return {{ x: r.x, y: r.y, width: r.width, height: r.height }}; \
251 }})()"
252 );
253 let js = frame_wrap_js(&active_frame, &inner);
254 let val = page
255 .evaluate_stealth(&js)
256 .await
257 .map_err(|e| format!("get_box eval failed: {e}"))?;
258 match val.as_ref() {
259 Some(v @ serde_json::Value::Object(_)) => Ok(v.clone()),
260 Some(serde_json::Value::Null) | None => Err(format!("Element not found: {selector}")),
261 _ => Err("Unexpected value from get_box".to_string()),
262 }
263 }
264 .await;
265 let _ = reply.send(result);
266}
267
268async fn handle_is_checked(
269 state: &WorkerState,
270 selector: String,
271 reply: oneshot::Sender<Result<bool, String>>,
272) {
273 let result = async {
274 let active_frame = state.tabs[state.active_tab].active_frame.clone();
275 let page = &state.tabs[state.active_tab].page;
276 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
277 let inner = format!(
278 "(function() {{ \
279 var el = document.querySelector({sel_json}); \
280 if (!el) return null; \
281 return el.checked === true; \
282 }})()"
283 );
284 let js = frame_wrap_js(&active_frame, &inner);
285 let val = page
286 .evaluate_stealth(&js)
287 .await
288 .map_err(|e| format!("is_checked eval failed: {e}"))?;
289 match val.as_ref() {
290 Some(serde_json::Value::Bool(b)) => Ok(*b),
291 Some(serde_json::Value::Null) | None => Err(format!("Element not found: {selector}")),
292 _ => Err("Unexpected value from is_checked".to_string()),
293 }
294 }
295 .await;
296 let _ = reply.send(result);
297}
298
299async fn handle_is_visible(
300 state: &WorkerState,
301 selector: String,
302 reply: oneshot::Sender<Result<bool, String>>,
303) {
304 let result = async {
305 let active_frame = state.tabs[state.active_tab].active_frame.clone();
306 let page = &state.tabs[state.active_tab].page;
307 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
308 let inner = format!(
309 "(function() {{ \
310 var el = document.querySelector({sel_json}); \
311 if (!el) return null; \
312 var r = el.getBoundingClientRect(); \
313 var style = window.getComputedStyle(el); \
314 return r.width > 0 && r.height > 0 \
315 && style.visibility !== 'hidden' \
316 && style.display !== 'none' \
317 && parseFloat(style.opacity) > 0; \
318 }})()"
319 );
320 let js = frame_wrap_js(&active_frame, &inner);
321 let val = page
322 .evaluate_stealth(&js)
323 .await
324 .map_err(|e| format!("is_visible eval failed: {e}"))?;
325 match val.as_ref() {
326 Some(serde_json::Value::Bool(b)) => Ok(*b),
327 Some(serde_json::Value::Null) | None => Err(format!("Element not found: {selector}")),
328 _ => Err("Unexpected value from is_visible".to_string()),
329 }
330 }
331 .await;
332 let _ = reply.send(result);
333}
334
335async fn handle_is_enabled(
336 state: &WorkerState,
337 selector: String,
338 reply: oneshot::Sender<Result<bool, String>>,
339) {
340 let result = async {
341 let active_frame = state.tabs[state.active_tab].active_frame.clone();
342 let page = &state.tabs[state.active_tab].page;
343 let sel_json = serde_json::to_string(&selector).unwrap_or_default();
344 let inner = format!(
345 "(function() {{ \
346 var el = document.querySelector({sel_json}); \
347 if (!el) return null; \
348 return !el.disabled && el.getAttribute('aria-disabled') !== 'true'; \
349 }})()"
350 );
351 let js = frame_wrap_js(&active_frame, &inner);
352 let val = page
353 .evaluate_stealth(&js)
354 .await
355 .map_err(|e| format!("is_enabled eval failed: {e}"))?;
356 match val.as_ref() {
357 Some(serde_json::Value::Bool(b)) => Ok(*b),
358 Some(serde_json::Value::Null) | None => Err(format!("Element not found: {selector}")),
359 _ => Err("Unexpected value from is_enabled".to_string()),
360 }
361 }
362 .await;
363 let _ = reply.send(result);
364}
365
366impl BrowserSession {
371 pub async fn get_text_with_timeout(
373 &self,
374 selector: &str,
375 timeout: std::time::Duration,
376 ) -> Result<String, NightFuryError> {
377 let selector = selector.to_string();
378 self.retry_on_not_found(timeout, || self.get_text_inner(&selector))
379 .await
380 }
381
382 pub async fn get_text(&self, selector: &str) -> Result<String, NightFuryError> {
384 if let Some(timeout) = self.auto_wait() {
385 return self.get_text_with_timeout(selector, timeout).await;
386 }
387 self.get_text_inner(selector).await
388 }
389
390 async fn get_text_inner(&self, selector: &str) -> Result<String, NightFuryError> {
391 send_cmd!(
392 self,
393 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::GetText {
394 selector: selector.to_string(),
395 reply: tx,
396 }),
397 NightFuryError::ElementNotFound
398 )
399 }
400
401 pub async fn get_value_with_timeout(
403 &self,
404 selector: &str,
405 timeout: std::time::Duration,
406 ) -> Result<String, NightFuryError> {
407 let selector = selector.to_string();
408 self.retry_on_not_found(timeout, || self.get_value_inner(&selector))
409 .await
410 }
411
412 pub async fn get_value(&self, selector: &str) -> Result<String, NightFuryError> {
414 if let Some(timeout) = self.auto_wait() {
415 return self.get_value_with_timeout(selector, timeout).await;
416 }
417 self.get_value_inner(selector).await
418 }
419
420 async fn get_value_inner(&self, selector: &str) -> Result<String, NightFuryError> {
421 send_cmd!(
422 self,
423 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::GetValue {
424 selector: selector.to_string(),
425 reply: tx,
426 }),
427 NightFuryError::ElementNotFound
428 )
429 }
430
431 pub async fn get_attribute_with_timeout(
433 &self,
434 selector: &str,
435 attribute: &str,
436 timeout: std::time::Duration,
437 ) -> Result<String, NightFuryError> {
438 let selector = selector.to_string();
439 let attribute = attribute.to_string();
440 self.retry_on_not_found(timeout, || self.get_attribute_inner(&selector, &attribute))
441 .await
442 }
443
444 pub async fn get_attribute(
446 &self,
447 selector: &str,
448 attribute: &str,
449 ) -> Result<String, NightFuryError> {
450 if let Some(timeout) = self.auto_wait() {
451 return self
452 .get_attribute_with_timeout(selector, attribute, timeout)
453 .await;
454 }
455 self.get_attribute_inner(selector, attribute).await
456 }
457
458 async fn get_attribute_inner(
459 &self,
460 selector: &str,
461 attribute: &str,
462 ) -> Result<String, NightFuryError> {
463 send_cmd!(
464 self,
465 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::GetAttribute {
466 selector: selector.to_string(),
467 attribute: attribute.to_string(),
468 reply: tx,
469 }),
470 NightFuryError::ElementNotFound
471 )
472 }
473
474 pub async fn get_html_with_timeout(
476 &self,
477 selector: &str,
478 timeout: std::time::Duration,
479 ) -> Result<String, NightFuryError> {
480 let selector = selector.to_string();
481 self.retry_on_not_found(timeout, || self.get_html_inner(&selector))
482 .await
483 }
484
485 pub async fn get_html(&self, selector: &str) -> Result<String, NightFuryError> {
487 if let Some(timeout) = self.auto_wait() {
488 return self.get_html_with_timeout(selector, timeout).await;
489 }
490 self.get_html_inner(selector).await
491 }
492
493 async fn get_html_inner(&self, selector: &str) -> Result<String, NightFuryError> {
494 send_cmd!(
495 self,
496 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::GetHtml {
497 selector: selector.to_string(),
498 reply: tx,
499 }),
500 NightFuryError::ElementNotFound
501 )
502 }
503
504 pub async fn get_count(&self, selector: &str) -> Result<usize, NightFuryError> {
506 send_cmd!(
507 self,
508 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::GetCount {
509 selector: selector.to_string(),
510 reply: tx,
511 }),
512 NightFuryError::OperationFailed
513 )
514 }
515
516 pub async fn get_box_with_timeout(
518 &self,
519 selector: &str,
520 timeout: std::time::Duration,
521 ) -> Result<Value, NightFuryError> {
522 let selector = selector.to_string();
523 self.retry_on_not_found(timeout, || self.get_box_inner(&selector))
524 .await
525 }
526
527 pub async fn get_box(&self, selector: &str) -> Result<Value, NightFuryError> {
529 if let Some(timeout) = self.auto_wait() {
530 return self.get_box_with_timeout(selector, timeout).await;
531 }
532 self.get_box_inner(selector).await
533 }
534
535 async fn get_box_inner(&self, selector: &str) -> Result<Value, NightFuryError> {
536 send_cmd!(
537 self,
538 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::GetBox {
539 selector: selector.to_string(),
540 reply: tx,
541 }),
542 NightFuryError::ElementNotFound
543 )
544 }
545
546 pub async fn is_checked_with_timeout(
548 &self,
549 selector: &str,
550 timeout: std::time::Duration,
551 ) -> Result<bool, NightFuryError> {
552 let selector = selector.to_string();
553 self.retry_on_not_found(timeout, || self.is_checked_inner(&selector))
554 .await
555 }
556
557 pub async fn is_checked(&self, selector: &str) -> Result<bool, NightFuryError> {
560 if let Some(timeout) = self.auto_wait() {
561 return self.is_checked_with_timeout(selector, timeout).await;
562 }
563 self.is_checked_inner(selector).await
564 }
565
566 async fn is_checked_inner(&self, selector: &str) -> Result<bool, NightFuryError> {
567 send_cmd!(
568 self,
569 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::IsChecked {
570 selector: selector.to_string(),
571 reply: tx,
572 }),
573 NightFuryError::ElementNotFound
574 )
575 }
576
577 pub async fn is_visible_with_timeout(
579 &self,
580 selector: &str,
581 timeout: std::time::Duration,
582 ) -> Result<bool, NightFuryError> {
583 let selector = selector.to_string();
584 self.retry_on_not_found(timeout, || self.is_visible_inner(&selector))
585 .await
586 }
587
588 pub async fn is_visible(&self, selector: &str) -> Result<bool, NightFuryError> {
591 if let Some(timeout) = self.auto_wait() {
592 return self.is_visible_with_timeout(selector, timeout).await;
593 }
594 self.is_visible_inner(selector).await
595 }
596
597 async fn is_visible_inner(&self, selector: &str) -> Result<bool, NightFuryError> {
598 send_cmd!(
599 self,
600 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::IsVisible {
601 selector: selector.to_string(),
602 reply: tx,
603 }),
604 NightFuryError::ElementNotFound
605 )
606 }
607
608 pub async fn is_enabled_with_timeout(
610 &self,
611 selector: &str,
612 timeout: std::time::Duration,
613 ) -> Result<bool, NightFuryError> {
614 let selector = selector.to_string();
615 self.retry_on_not_found(timeout, || self.is_enabled_inner(&selector))
616 .await
617 }
618
619 pub async fn is_enabled(&self, selector: &str) -> Result<bool, NightFuryError> {
622 if let Some(timeout) = self.auto_wait() {
623 return self.is_enabled_with_timeout(selector, timeout).await;
624 }
625 self.is_enabled_inner(selector).await
626 }
627
628 async fn is_enabled_inner(&self, selector: &str) -> Result<bool, NightFuryError> {
629 send_cmd!(
630 self,
631 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::IsEnabled {
632 selector: selector.to_string(),
633 reply: tx,
634 }),
635 NightFuryError::ElementNotFound
636 )
637 }
638
639 pub async fn wait_for_selector(
643 &self,
644 selector: &str,
645 timeout_ms: u64,
646 ) -> Result<String, NightFuryError> {
647 send_cmd!(
648 self,
649 |tx| crate::cmd::BrowserCmd::Query(QueryCmd::WaitForSelector {
650 selector: selector.to_string(),
651 timeout_ms,
652 reply: tx,
653 }),
654 NightFuryError::ElementNotFound
655 )
656 }
657}