1use super::*;
8
9impl BrowserSession {
10 pub async fn click_at(&self, x: f64, y: f64) -> BrowserResult<CoordinateClickOutcome> {
15 self.policy
16 .require(crate::browser::policy::PolicyCapability::CoordinateClick)?;
17 if !x.is_finite() || !y.is_finite() || x < 0.0 || y < 0.0 {
18 return Err("click-at coordinates must be finite and non-negative".into());
19 }
20
21 self.cdp
22 .with_current_route(async {
23 let hit = self
24 .evaluate_value(&format!(
25 "(() => {{ if ({x} >= innerWidth || {y} >= innerHeight) return null; const e = document.elementFromPoint({x}, {y}); if (!e) return null; return {{tag:e.tagName.toLowerCase(), role:e.getAttribute('role'), name:e.getAttribute('aria-label') || e.textContent?.trim().slice(0, 160) || null}}; }})()"
26 ))
27 .await?;
28 let hit = if hit.is_null() {
29 None
30 } else {
31 Some(CoordinateHit {
32 tag: hit["tag"].as_str().unwrap_or("unknown").to_string(),
33 role: hit["role"].as_str().map(str::to_string),
34 name: hit["name"].as_str().map(str::to_string),
35 })
36 };
37 if hit.is_none() {
38 return Err("click-at coordinates are outside the viewport or hit no element".into());
39 }
40 self.cdp
41 .dispatch_mouse_event("mouseMoved", x, y, None, None)
42 .await?;
43 self.cdp
44 .dispatch_mouse_event("mousePressed", x, y, Some("left"), Some(1))
45 .await?;
46 self.cdp
47 .dispatch_mouse_event("mouseReleased", x, y, Some("left"), Some(1))
48 .await?;
49 let (target_id, frame_id) = self.ensured_route_identity().await?;
50 Ok(CoordinateClickOutcome {
51 x,
52 y,
53 hit,
54 revision: self.invalidate_observation(),
55 target_id,
56 frame_id,
57 })
58 })
59 .await
60 }
61
62 pub async fn scroll(&self, dx: f64, dy: f64) -> BrowserResult<ActionOutcome> {
66 self.cdp
67 .with_current_route(async {
68 self.cdp.scroll_by(dx, dy).await?;
69 let (target_id, frame_id) = self.ensured_route_identity().await?;
70 Ok(ActionOutcome {
71 action: ActionKind::Scroll,
72 target: None,
73 revision: self.invalidate_observation(),
74 target_id,
75 frame_id,
76 evidence: None,
77 })
78 })
79 .await
80 }
81
82 pub async fn snapshot(&self) -> BrowserResult<AccessibilitySnapshot> {
88 self.cdp
89 .with_current_route(async {
90 let revision = self.page_revision.load(Ordering::Relaxed);
91 let raw = self.cdp.get_accessibility_tree().await?;
92 let roots = parse_accessibility_tree(&raw);
93 let interactive = interactive_elements(&roots, revision);
94 Ok(AccessibilitySnapshot {
95 page: self.page_info().await?,
96 roots,
97 interactive,
98 })
99 })
100 .await
101 }
102
103 pub async fn click(&self, target: &str) -> BrowserResult<ActionOutcome> {
105 self.pointer_click(target, false).await
106 }
107
108 pub async fn double_click(&self, target: &str) -> BrowserResult<ActionOutcome> {
111 self.pointer_click(target, true).await
112 }
113
114 pub async fn hover(&self, target: &str) -> BrowserResult<ActionOutcome> {
119 self.cdp
120 .with_current_route(async {
121 let element = self.resolve_element(target).await?;
122 let object_id = self
123 .cdp
124 .resolve_node_object(element.node_id, element.backend_dom_node_id)
125 .await?;
126 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
127 let local = self.verified_action_point(&remote.object_id).await?;
128 let point = self.target_viewport_point(local).await?;
129 self.move_pointer(point).await?;
130 self.action_outcome(ActionKind::Hover, Some(element), None)
131 .await
132 })
133 .await
134 }
135
136 pub async fn drag(&self, source: &str, destination: &str) -> BrowserResult<ActionOutcome> {
141 self.cdp
142 .with_current_route(async {
143 let source = self.resolve_element(source).await?;
144 let source_object = self
145 .cdp
146 .resolve_node_object(source.node_id, source.backend_dom_node_id)
147 .await?;
148 let source_guard = RemoteObjectGuard::new(self.cdp.clone(), source_object);
149 let destination = self.resolve_element(destination).await?;
150 let destination_object = self
151 .cdp
152 .resolve_node_object(destination.node_id, destination.backend_dom_node_id)
153 .await?;
154 let destination_guard =
155 RemoteObjectGuard::new(self.cdp.clone(), destination_object);
156 let source_local = self.verified_action_point(&source_guard.object_id).await?;
157 let destination_local = self
158 .verified_action_point(&destination_guard.object_id)
159 .await?;
160 let source_point = self.target_viewport_point(source_local).await?;
161 let destination_point = self.target_viewport_point(destination_local).await?;
162 self.move_pointer(source_point).await?;
163 let verified_source = self.verified_action_point(&source_guard.object_id).await?;
164 if (verified_source.x - source_local.x).abs() > 1.0
165 || (verified_source.y - source_local.y).abs() > 1.0
166 {
167 return Err(TargetError {
168 kind: TargetErrorKind::NotActionable,
169 reason: Some(TargetActionabilityReason::GeometryChanged),
170 candidates: Vec::new(),
171 recovery: None,
172 }
173 .into());
174 }
175 self.cdp
176 .dispatch_mouse_event(
177 "mousePressed",
178 source_point.x,
179 source_point.y,
180 Some("left"),
181 Some(1),
182 )
183 .await?;
184 let mut pressed = PressedButtonGuard {
185 cdp: self.cdp.clone(),
186 point: source_point,
187 click_count: 1,
188 armed: true,
189 };
190 let drag_path = interaction_path(
191 self.interaction_mode,
192 &self.mouse,
193 source_point,
194 destination_point,
195 );
196 for window in drag_path.windows(2) {
197 let point = window[1];
198 if self.interaction_mode == InteractionMode::Human {
199 tokio::time::sleep(self.mouse.move_delay(window[0], point)).await;
200 }
201 self.cdp
202 .dispatch_mouse_event("mouseMoved", point.x, point.y, Some("left"), Some(1))
203 .await?;
204 }
205 let verified_destination = self
206 .verified_action_point(&destination_guard.object_id)
207 .await?;
208 if (verified_destination.x - destination_local.x).abs() > 1.0
209 || (verified_destination.y - destination_local.y).abs() > 1.0
210 {
211 return Err(TargetError {
212 kind: TargetErrorKind::NotActionable,
213 reason: Some(TargetActionabilityReason::GeometryChanged),
214 candidates: Vec::new(),
215 recovery: None,
216 }
217 .into());
218 }
219 self.cdp
220 .dispatch_mouse_event(
221 "mouseReleased",
222 destination_point.x,
223 destination_point.y,
224 Some("left"),
225 Some(1),
226 )
227 .await?;
228 pressed.armed = false;
229 *self.pointer.lock().await = Some(destination_point);
230 self.action_outcome(ActionKind::Drag, Some(source), None)
231 .await
232 })
233 .await
234 }
235
236 pub async fn key_down(&self, key: &str) -> BrowserResult<ActionOutcome> {
240 self.keyboard_action(ActionKind::KeyDown, key, "rawKeyDown", 0)
241 .await
242 }
243
244 pub async fn key_up(&self, key: &str) -> BrowserResult<ActionOutcome> {
248 self.keyboard_action(ActionKind::KeyUp, key, "keyUp", 0)
249 .await
250 }
251
252 pub async fn key_press(&self, key: &str) -> BrowserResult<ActionOutcome> {
257 validate_key(key)?;
258 self.cdp
259 .with_current_route(async {
260 let code = key_code(key);
261 self.cdp
262 .dispatch_key_event_with_modifiers("rawKeyDown", key, &code, "", 0)
263 .await?;
264 if key.chars().count() == 1 {
265 self.cdp
266 .dispatch_key_event_with_modifiers("char", key, &code, key, 0)
267 .await?;
268 }
269 self.cdp
270 .dispatch_key_event_with_modifiers("keyUp", key, &code, "", 0)
271 .await?;
272 self.action_outcome(ActionKind::KeyPress, None, None).await
273 })
274 .await
275 }
276
277 pub async fn shortcut(&self, shortcut: &str) -> BrowserResult<ActionOutcome> {
282 let (modifiers, key) = parse_shortcut(shortcut)?;
283 self.cdp
284 .with_current_route(async {
285 let code = key_code(&key);
286 self.cdp
287 .dispatch_key_event_with_modifiers("rawKeyDown", &key, &code, "", modifiers)
288 .await?;
289 self.cdp
290 .dispatch_key_event_with_modifiers("keyUp", &key, &code, "", modifiers)
291 .await?;
292 self.action_outcome(ActionKind::Shortcut, None, None).await
293 })
294 .await
295 }
296
297 pub async fn clear(&self, target: &str) -> BrowserResult<ActionOutcome> {
302 self.cdp
303 .with_current_route(async {
304 let element = self.resolve_element(target).await?;
305 let object_id = self.cdp.resolve_node_object(element.node_id, element.backend_dom_node_id).await?;
306 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
307 let editable = runtime_value(&self.cdp.call_on_object(&remote.object_id, "function(){return this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement || this.isContentEditable}").await?)?;
308 if editable.as_bool() != Some(true) { return Err("clear target is not editable".into()); }
309 let clicked = self.click(target).await?;
310 self.cdp.dispatch_select_all().await?;
311 self.key_press("Backspace").await?;
312 let empty = runtime_value(&self.cdp.call_on_object(&remote.object_id, "function(){return this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement ? this.value === '' : this.textContent === ''}").await?)?;
313 if empty.as_bool() != Some(true) { return Err("clear target did not become empty".into()); }
314 self.action_outcome_from_target(ActionKind::Clear, clicked.target)
315 .await
316 })
317 .await
318 }
319
320 pub async fn check(&self, target: &str) -> BrowserResult<ActionOutcome> {
324 self.set_checked(target, true).await
325 }
326
327 pub async fn uncheck(&self, target: &str) -> BrowserResult<ActionOutcome> {
331 self.set_checked(target, false).await
332 }
333
334 pub async fn select_option(&self, target: &str, value: &str) -> BrowserResult<ActionOutcome> {
338 if value.is_empty() || value.len() > 4096 {
339 return Err("select value must be 1..=4096 bytes".into());
340 }
341 let value_json = serde_json::to_string(value)?;
342 self.form_object_action(target, ActionKind::Select, &format!(r#"function() {{ if (!(this instanceof HTMLSelectElement)) return {{ok:false,reason:'not_select'}}; const option = Array.from(this.options).find(option => option.value === {value_json}); if (!option) return {{ok:false,reason:'option_not_found'}}; this.value = option.value; this.dispatchEvent(new Event('input',{{bubbles:true}})); this.dispatchEvent(new Event('change',{{bubbles:true}})); return {{ok:this.value === option.value}}; }}"#)).await
343 }
344
345 pub async fn upload_files(
346 &self,
347 target: &str,
348 paths: &[PathBuf],
349 ) -> BrowserResult<ActionOutcome> {
350 self.policy.require(PolicyCapability::Upload)?;
351 self.cdp.with_current_route(async {
352 if paths.is_empty() || paths.len() > 16 { return Err("upload requires 1..=16 files".into()); }
353 let mut files = Vec::with_capacity(paths.len());
354 for path in paths {
355 let canonical = self.policy.require_existing_path(path)?;
356 if !canonical.is_file() { return Err("upload path must be a regular file".into()); }
357 if !canonical.starts_with(&self.upload_root) { return Err("upload path is outside the allowed workspace root".into()); }
358 files.push(canonical.to_string_lossy().into_owned());
359 }
360 let element = self.resolve_element(target).await?;
361 let object_id = self.cdp.resolve_node_object(element.node_id, element.backend_dom_node_id).await?;
362 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
363 self.verified_action_point(&remote.object_id).await?;
364 let input = runtime_value(&self.cdp.call_on_object(&remote.object_id, "function(){return {ok:this instanceof HTMLInputElement && this.type === 'file'}}").await?)?;
365 if input["ok"].as_bool() != Some(true) { return Err("upload target is not a file input".into()); }
366 if element.node_id.is_none() && element.backend_dom_node_id.is_none() { return Err("file input target has no DOM node ID".into()); }
367 self.cdp.set_file_input_files(element.node_id, element.backend_dom_node_id, &files).await?;
368 let verified = runtime_value(&self.cdp.call_on_object(&remote.object_id, "function(){return this.files.length}").await?)?;
369 if verified.as_u64() != Some(files.len() as u64) { return Err("file input did not retain the requested file count".into()); }
370 let outcome = self.action_outcome(ActionKind::Upload, Some(element), Some(serde_json::json!({"file_count": files.len()}))).await?;
371 self.record_audit("upload", format!("{} files", files.len()));
372 Ok(outcome)
373 }).await
374 }
375
376 async fn pointer_click(
377 &self,
378 target: &str,
379 double_click: bool,
380 ) -> BrowserResult<ActionOutcome> {
381 self.cdp
382 .with_current_route(async {
383 let element = self.resolve_element(target).await?;
384 let object_id = self
385 .cdp
386 .resolve_node_object(element.node_id, element.backend_dom_node_id)
387 .await
388 .map_err(|error| {
389 tracing::debug!(%error, "target node could not be resolved");
390 TargetError {
391 kind: TargetErrorKind::NotActionable,
392 reason: Some(TargetActionabilityReason::NodeUnavailable),
393 candidates: Vec::new(),
394 recovery: None,
395 }
396 })?;
397 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
398 let local_point = self.verified_action_point(&remote.object_id).await?;
399 let point = self.target_viewport_point(local_point).await?;
400 let events = if double_click {
401 self.mouse.generate_double_click_events(point)
402 } else {
403 self.mouse.generate_click_events(point)
404 };
405 self.dispatch_pointer_events(&remote.object_id, local_point, point, events)
406 .await?;
407 let (target_id, frame_id) = self.route_identity().await?;
408 Ok(ActionOutcome {
409 action: if double_click {
410 ActionKind::DoubleClick
411 } else {
412 ActionKind::Click
413 },
414 target: Some(ActionTarget {
415 label: element.label,
416 reference: element.reference,
417 }),
418 revision: self.invalidate_observation(),
419 target_id,
420 frame_id,
421 evidence: None,
422 })
423 })
424 .await
425 }
426
427 async fn dispatch_pointer_events(
428 &self,
429 object_id: &str,
430 local_point: Point,
431 point: Point,
432 events: Vec<crate::browser::mouse::MouseEvent>,
433 ) -> BrowserResult<()> {
434 let mut pointer = self.pointer.lock().await;
435 let start = match (self.interaction_mode, *pointer) {
436 (_, Some(point)) => point,
437 (InteractionMode::Human, None) => self
438 .viewport_center()
439 .await
440 .unwrap_or(Point { x: 640.0, y: 360.0 }),
441 (InteractionMode::Fast, None) => point,
442 };
443 let path = interaction_path(self.interaction_mode, &self.mouse, start, point);
444 if self.interaction_mode == InteractionMode::Human && pointer.is_none() {
445 self.cdp
446 .dispatch_mouse_event("mouseMoved", start.x, start.y, None, None)
447 .await?;
448 }
449 for window in path.windows(2) {
450 let next = window[1];
451 if self.interaction_mode == InteractionMode::Human {
452 tokio::time::sleep(self.mouse.move_delay(window[0], next)).await;
453 }
454 self.cdp
455 .dispatch_mouse_event("mouseMoved", next.x, next.y, None, None)
456 .await?;
457 }
458 let press_point = self.verified_action_point(object_id).await?;
459 if (press_point.x - local_point.x).abs() > 1.0
460 || (press_point.y - local_point.y).abs() > 1.0
461 {
462 return Err(TargetError {
463 kind: TargetErrorKind::NotActionable,
464 reason: Some(TargetActionabilityReason::GeometryChanged),
465 candidates: Vec::new(),
466 recovery: None,
467 }
468 .into());
469 }
470 let mut pressed = None;
471 for event in events {
472 if event.event_type == "mousePressed" {
473 pressed = Some(PressedButtonGuard {
474 cdp: self.cdp.clone(),
475 point,
476 click_count: event.click_count,
477 armed: true,
478 });
479 }
480 self.cdp
481 .dispatch_mouse_event(
482 &event.event_type,
483 event.x,
484 event.y,
485 Some(&event.button),
486 Some(event.click_count),
487 )
488 .await?;
489 if event.event_type == "mouseReleased"
490 && let Some(mut guard) = pressed.take()
491 {
492 guard.armed = false;
493 }
494 if self.interaction_mode == InteractionMode::Human && event.event_type == "mousePressed"
495 {
496 tokio::time::sleep(self.mouse.click_delay()).await;
497 }
498 }
499 *pointer = Some(point);
500 Ok(())
501 }
502
503 pub async fn type_text(
509 &self,
510 text: &str,
511 target: Option<&str>,
512 ) -> BrowserResult<ActionOutcome> {
513 self.cdp
514 .with_current_route(async {
515 let target = match target {
516 Some(target) => self.click(target).await?.target,
517 None => None,
518 };
519 self.cdp.insert_text(text).await?;
520 let (target_id, frame_id) = self.route_identity().await?;
521 Ok(ActionOutcome {
522 action: ActionKind::Type,
523 target,
524 revision: self.invalidate_observation(),
525 target_id,
526 frame_id,
527 evidence: None,
528 })
529 })
530 .await
531 }
532
533 async fn move_pointer(&self, destination: Point) -> BrowserResult<()> {
534 let mut pointer = self.pointer.lock().await;
535 let start = pointer.unwrap_or(destination);
536 for window in
537 interaction_path(self.interaction_mode, &self.mouse, start, destination).windows(2)
538 {
539 if self.interaction_mode == InteractionMode::Human {
540 tokio::time::sleep(self.mouse.move_delay(window[0], window[1])).await;
541 }
542 self.cdp
543 .dispatch_mouse_event("mouseMoved", window[1].x, window[1].y, None, None)
544 .await?;
545 }
546 if start == destination {
547 self.cdp
548 .dispatch_mouse_event("mouseMoved", destination.x, destination.y, None, None)
549 .await?;
550 }
551 *pointer = Some(destination);
552 Ok(())
553 }
554
555 async fn keyboard_action(
556 &self,
557 action: ActionKind,
558 key: &str,
559 event_type: &str,
560 modifiers: i64,
561 ) -> BrowserResult<ActionOutcome> {
562 validate_key(key)?;
563 self.cdp
564 .with_current_route(async {
565 self.cdp
566 .dispatch_key_event_with_modifiers(
567 event_type,
568 key,
569 &key_code(key),
570 "",
571 modifiers,
572 )
573 .await?;
574 self.action_outcome(action, None, None).await
575 })
576 .await
577 }
578
579 async fn set_checked(&self, target: &str, checked: bool) -> BrowserResult<ActionOutcome> {
580 let action = if checked {
581 ActionKind::Check
582 } else {
583 ActionKind::Uncheck
584 };
585 let script = format!(
586 r#"function() {{ if (!(this instanceof HTMLInputElement) || !['checkbox','radio'].includes(this.type)) return {{ok:false,reason:'not_checkable'}}; if (this.checked !== {checked}) this.click(); return {{ok:this.checked === {checked}}}; }}"#
587 );
588 self.form_object_action(target, action, &script).await
589 }
590
591 async fn form_object_action(
592 &self,
593 target: &str,
594 action: ActionKind,
595 function: &str,
596 ) -> BrowserResult<ActionOutcome> {
597 self.cdp
598 .with_current_route(async {
599 let element = self.resolve_element(target).await?;
600 let object_id = self
601 .cdp
602 .resolve_node_object(element.node_id, element.backend_dom_node_id)
603 .await?;
604 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
605 self.verified_action_point(&remote.object_id).await?;
606 let result = self.cdp.call_on_object(&remote.object_id, function).await?;
607 let value = runtime_value(&result)?;
608 if value["ok"].as_bool() != Some(true) {
609 return Err(format!(
610 "form action failed: {}",
611 value["reason"].as_str().unwrap_or("verification_failed")
612 )
613 .into());
614 }
615 self.action_outcome(action, Some(element), None).await
616 })
617 .await
618 }
619
620 async fn action_outcome(
621 &self,
622 action: ActionKind,
623 element: Option<ResolvedElement>,
624 evidence: Option<Value>,
625 ) -> BrowserResult<ActionOutcome> {
626 let target = element.map(|element| ActionTarget {
627 label: element.label,
628 reference: element.reference,
629 });
630 let mut outcome = self.action_outcome_from_target(action, target).await?;
631 outcome.evidence = evidence;
632 Ok(outcome)
633 }
634
635 pub(crate) async fn action_outcome_from_target(
636 &self,
637 action: ActionKind,
638 target: Option<ActionTarget>,
639 ) -> BrowserResult<ActionOutcome> {
640 if let Some(interception) = &self.policy_interception {
641 let _ = self.cdp.evaluate("0").await;
645 tokio::task::yield_now().await;
646 if let Some(error) = interception.take_denial().await {
647 return Err(error.into());
648 }
649 }
650 let (target_id, frame_id) = self.route_identity().await?;
651 Ok(ActionOutcome {
652 action,
653 target,
654 revision: self.invalidate_observation(),
655 target_id,
656 frame_id,
657 evidence: None,
658 })
659 }
660
661 pub(crate) async fn viewport_center(&self) -> BrowserResult<Point> {
662 let value = self
663 .evaluate_value("[window.innerWidth / 2, window.innerHeight / 2]")
664 .await?;
665 let coordinates = value
666 .as_array()
667 .filter(|coordinates| coordinates.len() == 2)
668 .ok_or("viewport evaluation returned invalid coordinates")?;
669 let x = coordinates[0]
670 .as_f64()
671 .ok_or("viewport width was not numeric")?;
672 let y = coordinates[1]
673 .as_f64()
674 .ok_or("viewport height was not numeric")?;
675 Ok(Point { x, y })
676 }
677
678 pub(crate) async fn target_viewport_point(&self, point: Point) -> BrowserResult<Point> {
679 let Some(frame_id) = self.cdp.active_frame() else {
680 return Ok(point);
681 };
682 let frame = {
683 let topology = self.topology.lock().await;
684 topology
685 .frames
686 .iter()
687 .find(|frame| frame.id == frame_id)
688 .cloned()
689 };
690 let frame = match frame {
691 Some(frame) => frame,
692 None => self
693 .list_frames()
694 .await?
695 .into_iter()
696 .find(|frame| frame.id == frame_id)
697 .ok_or("selected frame is no longer attached")?,
698 };
699 if frame.parent_id.is_none() {
700 return Ok(point);
701 }
702 let (x, y) = self.cdp.frame_viewport_offset(&frame_id).await?;
703 Ok(Point {
704 x: point.x + x,
705 y: point.y + y,
706 })
707 }
708
709 pub(crate) async fn evaluate_value(&self, expression: &str) -> BrowserResult<Value> {
710 let raw = self.cdp.evaluate(expression).await?;
711 runtime_value(&raw)
712 }
713
714 pub(crate) fn invalidate_observation(&self) -> u64 {
715 self.page_revision.fetch_add(1, Ordering::Relaxed) + 1
716 }
717
718 pub(crate) async fn verified_action_point(&self, object_id: &str) -> BrowserResult<Point> {
719 let raw = match self.cdp.call_on_object(object_id, HIT_TEST_FUNCTION).await {
720 Ok(raw) => raw,
721 Err(error) => {
722 tracing::debug!(%error, "target node could not be verified");
723 return Err(TargetError {
724 kind: TargetErrorKind::NotActionable,
725 reason: Some(TargetActionabilityReason::NodeUnavailable),
726 candidates: Vec::new(),
727 recovery: None,
728 }
729 .into());
730 }
731 };
732 let value = runtime_value(&raw)?;
733 if value["ok"].as_bool() != Some(true) {
734 let reason = value["reason"].as_str().unwrap_or("verification_failed");
735 tracing::debug!(reason, "target actionability check failed");
736 return Err(TargetError {
737 kind: TargetErrorKind::NotActionable,
738 reason: Some(actionability_reason(reason)),
739 candidates: Vec::new(),
740 recovery: None,
741 }
742 .into());
743 }
744 let x = value["x"]
745 .as_f64()
746 .ok_or("verified target x was not numeric")?;
747 let y = value["y"]
748 .as_f64()
749 .ok_or("verified target y was not numeric")?;
750 Ok(Point { x, y })
751 }
752}