webdriver_server/actions.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashMap;
6use std::thread;
7use std::time::{Duration, Instant};
8
9use base::id::BrowsingContextId;
10use crossbeam_channel::Select;
11use embedder_traits::{
12 InputEvent, KeyboardEvent, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
13 WebDriverCommandMsg, WebDriverScriptCommand, WebViewPoint, WheelDelta, WheelEvent, WheelMode,
14};
15use euclid::Point2D;
16use ipc_channel::ipc;
17use keyboard_types::webdriver::KeyInputState;
18use log::info;
19use rustc_hash::FxHashSet;
20use webdriver::actions::{
21 ActionSequence, ActionsType, GeneralAction, KeyAction, KeyActionItem, KeyDownAction,
22 KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerDownAction,
23 PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, WheelActionItem,
24 WheelScrollAction,
25};
26use webdriver::error::{ErrorStatus, WebDriverError};
27
28use crate::{Handler, VerifyBrowsingContextIsOpen, WebElement, wait_for_ipc_response};
29
30// Interval between wheelScroll and pointerMove increments in ms, based on common vsync
31static POINTERMOVE_INTERVAL: u64 = 17;
32static WHEELSCROLL_INTERVAL: u64 = 17;
33
34// https://262.ecma-international.org/6.0/#sec-number.max_safe_integer
35static MAXIMUM_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
36
37// A single action, corresponding to an `action object` in the spec.
38// In the spec, `action item` refers to a plain JSON object.
39// However, we use the name ActionItem here
40// to be consistent with type names from webdriver crate.
41#[derive(Debug, PartialEq)]
42pub(crate) enum ActionItem {
43 Null(NullActionItem),
44 Key(KeyActionItem),
45 Pointer(PointerActionItem),
46 Wheel(WheelActionItem),
47}
48
49/// A set of actions with multiple sources executed within a single tick.
50/// The `id` is used to identify the source of the actions.
51pub(crate) type TickActions = Vec<(String, ActionItem)>;
52
53/// Consumed by the `dispatch_actions` method.
54pub(crate) type ActionsByTick = Vec<TickActions>;
55
56/// <https://w3c.github.io/webdriver/#dfn-input-source-state>
57pub(crate) enum InputSourceState {
58 Null,
59 Key(KeyInputState),
60 Pointer(PointerInputState),
61 Wheel,
62}
63
64/// <https://w3c.github.io/webdriver/#dfn-pointer-input-source>
65/// TODO: subtype is used for <https://w3c.github.io/webdriver/#dfn-get-a-pointer-id>
66/// Need to add pointer-id to the following struct
67#[expect(dead_code)]
68pub(crate) struct PointerInputState {
69 subtype: PointerType,
70 pressed: FxHashSet<u64>,
71 pub(crate) pointer_id: u32,
72 x: f64,
73 y: f64,
74}
75
76impl PointerInputState {
77 /// <https://w3c.github.io/webdriver/#dfn-create-a-pointer-input-source>
78 pub(crate) fn new(subtype: PointerType, pointer_ids: FxHashSet<u32>) -> PointerInputState {
79 PointerInputState {
80 subtype,
81 pressed: FxHashSet::default(),
82 pointer_id: Self::get_pointer_id(subtype, pointer_ids),
83 x: 0.0,
84 y: 0.0,
85 }
86 }
87
88 /// <https://w3c.github.io/webdriver/#dfn-get-a-pointer-id>
89 fn get_pointer_id(subtype: PointerType, pointer_ids: FxHashSet<u32>) -> u32 {
90 // Step 2 - 4: Let pointer ids be all the values in input state map which is
91 // pointer input source. This is already done and passed by the caller.
92 if subtype == PointerType::Mouse {
93 for id in 0..=1 {
94 if !pointer_ids.contains(&id) {
95 return id;
96 }
97 }
98 }
99
100 // We are dealing with subtype other than mouse, which has minimum id 2.
101 1 + pointer_ids.into_iter().max().unwrap_or(1)
102 }
103}
104
105/// <https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration>
106fn compute_tick_duration(tick_actions: &TickActions) -> u64 {
107 // Step 1. Let max duration be 0.
108 // Step 2. For each action in tick actions:
109 tick_actions
110 .iter()
111 .filter_map(|(_, action_item)| {
112 // If action object has subtype property set to "pause" or
113 // action object has type property set to "pointer" and subtype property set to "pointerMove",
114 // or action object has type property set to "wheel" and subtype property set to "scroll",
115 // let duration be equal to the duration property of action object.
116 match action_item {
117 ActionItem::Null(NullActionItem::General(GeneralAction::Pause(pause_action))) |
118 ActionItem::Key(KeyActionItem::General(GeneralAction::Pause(pause_action))) |
119 ActionItem::Pointer(PointerActionItem::General(GeneralAction::Pause(
120 pause_action,
121 ))) |
122 ActionItem::Wheel(WheelActionItem::General(GeneralAction::Pause(pause_action))) => {
123 pause_action.duration
124 },
125 ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move(action))) => {
126 action.duration
127 },
128 ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(action))) => {
129 action.duration
130 },
131 _ => None,
132 }
133 })
134 .max()
135 .unwrap_or(0)
136}
137
138impl Handler {
139 /// <https://w3c.github.io/webdriver/#dfn-dispatch-actions>
140 /// <https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner>
141 /// For Servo, "dispatch actions" is identical to "dispatch actions inner",
142 /// as they are only different for a session that can run commands in parallel.
143 pub(crate) fn dispatch_actions(
144 &mut self,
145 actions_by_tick: ActionsByTick,
146 browsing_context: BrowsingContextId,
147 ) -> Result<(), ErrorStatus> {
148 // Step 1. For each item tick actions in actions by tick
149 for tick_actions in actions_by_tick.iter() {
150 // Step 1.1. If browsing context is no longer open,
151 // return error with error code no such window.
152 self.verify_browsing_context_is_open(browsing_context)
153 .map_err(|e| e.error)?;
154 // Step 1.2. Let tick duration be the result of
155 // computing the tick duration with argument tick actions.
156 let tick_duration = compute_tick_duration(tick_actions);
157
158 // FIXME: This is out of spec, but the test `perform_actions/invalid.py` requires
159 // that duration more than `MAXIMUM_SAFE_INTEGER` is considered invalid.
160 if tick_duration > MAXIMUM_SAFE_INTEGER {
161 return Err(ErrorStatus::InvalidArgument);
162 }
163
164 let now = Instant::now();
165
166 // Step 1.3. Try to dispatch tick actions
167 self.dispatch_tick_actions(tick_actions, tick_duration)?;
168
169 // Step 1.4. Wait for
170 // The user agent event loop has spun enough times to process the DOM events
171 // generated by the last invocation of the dispatch tick actions steps.
172 self.wait_for_user_agent_handling_complete()?;
173 // At least tick duration milliseconds have passed.
174 let elapsed = now.elapsed().as_millis() as u64;
175 if elapsed < tick_duration {
176 let sleep_duration = tick_duration - elapsed;
177 thread::sleep(Duration::from_millis(sleep_duration));
178 }
179 }
180
181 // Step 2. Return success with data null.
182 info!("Dispatch actions completed successfully");
183 Ok(())
184 }
185
186 fn wait_for_user_agent_handling_complete(&self) -> Result<(), ErrorStatus> {
187 let mut pending_event_receivers =
188 std::mem::take(&mut *self.pending_input_event_receivers.borrow_mut());
189
190 while !pending_event_receivers.is_empty() {
191 let mut select = Select::new();
192 for receiver in &pending_event_receivers {
193 select.recv(receiver);
194 }
195
196 let operation = select.select();
197 let index = operation.index();
198 let _ = operation.recv(&pending_event_receivers[index]);
199
200 pending_event_receivers.remove(index);
201 }
202
203 self.num_pending_actions.set(0);
204
205 Ok(())
206 }
207
208 /// <https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions>
209 fn dispatch_tick_actions(
210 &mut self,
211 tick_actions: &TickActions,
212 tick_duration: u64,
213 ) -> Result<(), ErrorStatus> {
214 // Step 1. For each action object in tick actions:
215 // Step 1.1. Let input_id be the value of the id property of action object.
216 for (input_id, action) in tick_actions.iter() {
217 // Step 6. Let subtype be action object's subtype.
218 // Steps 7, 8. Try to run specific algorithm based on the action type.
219 match action {
220 ActionItem::Null(_) |
221 ActionItem::Key(KeyActionItem::General(_)) |
222 ActionItem::Pointer(PointerActionItem::General(_)) |
223 ActionItem::Wheel(WheelActionItem::General(_)) => {
224 self.dispatch_pause_action(input_id);
225 },
226 ActionItem::Key(KeyActionItem::Key(KeyAction::Down(keydown_action))) => {
227 self.dispatch_keydown_action(input_id, keydown_action);
228 // Step 9. If subtype is "keyDown", append a copy of action
229 // object with the subtype property changed to "keyUp" to
230 // input state's input cancel list.
231 self.session_mut().unwrap().input_cancel_list.push((
232 input_id.clone(),
233 ActionItem::Key(KeyActionItem::Key(KeyAction::Up(KeyUpAction {
234 value: keydown_action.value.clone(),
235 }))),
236 ));
237 },
238 ActionItem::Key(KeyActionItem::Key(KeyAction::Up(keyup_action))) => {
239 self.dispatch_keyup_action(input_id, keyup_action);
240 },
241 ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Down(
242 pointer_down_action,
243 ))) => {
244 self.dispatch_pointerdown_action(input_id, pointer_down_action);
245 // Step 10. If subtype is "pointerDown", append a copy of action
246 // object with the subtype property changed to "pointerUp" to
247 // input state's input cancel list.
248 self.session_mut().unwrap().input_cancel_list.push((
249 input_id.clone(),
250 ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up(
251 PointerUpAction {
252 button: pointer_down_action.button,
253 ..Default::default()
254 },
255 ))),
256 ));
257 },
258 ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move(
259 pointer_move_action,
260 ))) => {
261 self.dispatch_pointermove_action(input_id, pointer_move_action, tick_duration)?;
262 },
263 ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up(
264 pointer_up_action,
265 ))) => {
266 self.dispatch_pointerup_action(input_id, pointer_up_action);
267 },
268 ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(scroll_action))) => {
269 self.dispatch_scroll_action(input_id, scroll_action, tick_duration)?;
270 },
271 ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Cancel)) => {
272 info!("dispatch_tick_actions: PointerCancel action is not implemented yet");
273 },
274 }
275 }
276
277 Ok(())
278 }
279
280 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pause-action>
281 fn dispatch_pause_action(&mut self, source_id: &str) {
282 self.input_state_table_mut()
283 .entry(source_id.to_string())
284 .or_insert(InputSourceState::Null);
285 }
286
287 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action>
288 fn dispatch_keydown_action(&mut self, source_id: &str, action: &KeyDownAction) {
289 let raw_key = action.value.chars().next().unwrap();
290 let key_input_state = match self.input_state_table_mut().get_mut(source_id).unwrap() {
291 InputSourceState::Key(key_input_state) => key_input_state,
292 _ => unreachable!(),
293 };
294
295 let keyboard_event = key_input_state.dispatch_keydown(raw_key);
296
297 // Step 12: Perform implementation-specific action dispatch steps on browsing
298 // context equivalent to pressing a key on the keyboard in accordance with the
299 // requirements of [UI-EVENTS], and producing the following events, as
300 // appropriate, with the specified properties. This will always produce events
301 // including at least a keyDown event.
302 self.send_blocking_input_event_to_embedder(InputEvent::Keyboard(KeyboardEvent::new(
303 keyboard_event,
304 )));
305 }
306
307 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action>
308 fn dispatch_keyup_action(&mut self, source_id: &str, action: &KeyUpAction) {
309 let session = self.session_mut().unwrap();
310
311 // Remove the last matching keyUp from `[input_cancel_list]` due to bugs in spec
312 // See https://github.com/w3c/webdriver/issues/1905 &&
313 // https://github.com/servo/servo/issues/37579#issuecomment-2990762713
314 let input_cancel_list = &mut session.input_cancel_list;
315 if let Some(pos) = input_cancel_list.iter().rposition(|(id, item)| {
316 id == source_id &&
317 matches!(item,
318 ActionItem::Key(KeyActionItem::Key(KeyAction::Up(KeyUpAction { value })))
319 if *value == action.value )
320 }) {
321 info!("dispatch_keyup_action: removing last matching keyup from input_cancel_list");
322 input_cancel_list.remove(pos);
323 }
324
325 let raw_key = action.value.chars().next().unwrap();
326 let key_input_state = match session.input_state_table.get_mut(source_id).unwrap() {
327 InputSourceState::Key(key_input_state) => key_input_state,
328 _ => unreachable!(),
329 };
330
331 // Step 12: Perform implementation-specific action dispatch steps on browsing
332 // context equivalent to releasing a key on the keyboard in accordance with the
333 // requirements of [UI-EVENTS], ...
334 let Some(keyboard_event) = key_input_state.dispatch_keyup(raw_key) else {
335 return;
336 };
337 self.send_blocking_input_event_to_embedder(InputEvent::Keyboard(KeyboardEvent::new(
338 keyboard_event,
339 )));
340 }
341
342 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerdown-action>
343 fn dispatch_pointerdown_action(&mut self, source_id: &str, action: &PointerDownAction) {
344 let pointer_input_state = self.get_pointer_input_state_mut(source_id);
345 // Step 3. If the source's pressed property contains button return success with data null.
346 if pointer_input_state.pressed.contains(&action.button) {
347 return;
348 }
349
350 let PointerInputState { x, y, .. } = *pointer_input_state;
351 // Step 6. Add button to the set corresponding to source's pressed property
352 pointer_input_state.pressed.insert(action.button);
353 // Step 7 - 15: Variable namings already done.
354
355 // Step 16. Perform implementation-specific action dispatch steps
356 // TODO: We have not considered pen/touch pointer type
357 self.send_blocking_input_event_to_embedder(InputEvent::MouseButton(MouseButtonEvent::new(
358 MouseButtonAction::Down,
359 action.button.into(),
360 WebViewPoint::Page(Point2D::new(x as f32, y as f32)),
361 )));
362
363 // Step 17. Return success with data null.
364 }
365
366 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action>
367 fn dispatch_pointerup_action(&mut self, source_id: &str, action: &PointerUpAction) {
368 let pointer_input_state = self.get_pointer_input_state_mut(source_id);
369 // Step 3. If the source's pressed property does not contain button, return success with data null.
370 if !pointer_input_state.pressed.contains(&action.button) {
371 return;
372 }
373
374 // Step 6. Remove button from the set corresponding to source's pressed property,
375 pointer_input_state.pressed.remove(&action.button);
376 let PointerInputState { x, y, .. } = *pointer_input_state;
377
378 // Remove matching pointerUp(must be unique) from `[input_cancel_list]` due to bugs in spec
379 // See https://github.com/w3c/webdriver/issues/1905 &&
380 // https://github.com/servo/servo/issues/37579#issuecomment-2990762713
381 let input_cancel_list = &mut self.session_mut().unwrap().input_cancel_list;
382 if let Some(pos) = input_cancel_list.iter().position(|(id, item)| {
383 id == source_id &&
384 matches!(item, ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up(
385 PointerUpAction { button, .. },
386 ))) if *button == action.button )
387 }) {
388 info!("dispatch_pointerup_action: removing matching pointerup from input_cancel_list");
389 input_cancel_list.remove(pos);
390 }
391
392 // Step 7. Perform implementation-specific action dispatch steps
393 self.send_blocking_input_event_to_embedder(InputEvent::MouseButton(MouseButtonEvent::new(
394 MouseButtonAction::Up,
395 action.button.into(),
396 WebViewPoint::Page(Point2D::new(x as f32, y as f32)),
397 )));
398
399 // Step 8. Return success with data null.
400 }
401
402 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action>
403 fn dispatch_pointermove_action(
404 &mut self,
405 source_id: &str,
406 action: &PointerMoveAction,
407 tick_duration: u64,
408 ) -> Result<(), ErrorStatus> {
409 let tick_start = Instant::now();
410
411 // Step 1. Let x offset be equal to the x property of action object.
412 let x_offset = action.x;
413
414 // Step 2. Let y offset be equal to the y property of action object.
415 let y_offset = action.y;
416
417 // Step 3. Let origin be equal to the origin property of action object.
418 let origin = &action.origin;
419
420 // Step 4. Let (x, y) be the result of trying to get coordinates relative to an origin
421 // with source, x offset, y offset, origin, browsing context, and actions options.
422
423 let (x, y) = self.get_origin_relative_coordinates(origin, x_offset, y_offset, source_id)?;
424
425 // Step 5. If x is less than 0 or greater than the width of the viewport in CSS pixels,
426 // then return error with error code move target out of bounds.
427 // Step 6. If y is less than 0 or greater than the height of the viewport in CSS pixels,
428 // then return error with error code move target out of bounds.
429 self.check_viewport_bound(x, y)?;
430
431 // Step 7. Let duration be equal to action object's duration property
432 // if it is not undefined, or tick duration otherwise.
433 let duration = match action.duration {
434 Some(duration) => duration,
435 None => tick_duration,
436 };
437
438 // Step 8. If duration is greater than 0 and inside any implementation-defined bounds,
439 // asynchronously wait for an implementation defined amount of time to pass.
440 if duration > 0 {
441 thread::sleep(Duration::from_millis(POINTERMOVE_INTERVAL));
442 }
443
444 let (start_x, start_y) = {
445 let pointer_input_state = self.get_pointer_input_state(source_id);
446 (pointer_input_state.x, pointer_input_state.y)
447 };
448
449 // Step 9 - 18
450 // Perform a pointer move with arguments source, global key state, duration, start x, start y,
451 // x, y, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle.
452 // TODO: We have not considered pen/touch pointer type
453 self.perform_pointer_move(source_id, duration, start_x, start_y, x, y, tick_start);
454
455 // Step 19. Return success with data null.
456 Ok(())
457 }
458
459 /// <https://w3c.github.io/webdriver/#dfn-perform-a-pointer-move>
460 #[allow(clippy::too_many_arguments)]
461 fn perform_pointer_move(
462 &mut self,
463 source_id: &str,
464 duration: u64,
465 start_x: f64,
466 start_y: f64,
467 target_x: f64,
468 target_y: f64,
469 tick_start: Instant,
470 ) {
471 loop {
472 // Step 1. Let time delta be the time since the beginning of the
473 // current tick, measured in milliseconds on a monotonic clock.
474 let time_delta = tick_start.elapsed().as_millis();
475
476 // Step 2. Let duration ratio be the ratio of time delta and duration,
477 // if duration is greater than 0, or 1 otherwise.
478 let duration_ratio = if duration > 0 {
479 time_delta as f64 / duration as f64
480 } else {
481 1.0
482 };
483
484 // Step 3. If duration ratio is 1, or close enough to 1 that the
485 // implementation will not further subdivide the move action,
486 // let last be true. Otherwise let last be false.
487 let last = 1.0 - duration_ratio < 0.001;
488
489 // Step 4. If last is true, let x equal target x and y equal target y.
490 // Otherwise
491 // let x equal an approximation to duration ratio × (target x - start x) + start x,
492 // and y equal an approximation to duration ratio × (target y - start y) + start y.
493 let (x, y) = if last {
494 (target_x, target_y)
495 } else {
496 (
497 duration_ratio * (target_x - start_x) + start_x,
498 duration_ratio * (target_y - start_y) + start_y,
499 )
500 };
501
502 // Step 5 - 6: Let current x/y equal the x/y property of input state.
503 let (current_x, current_y) = {
504 let pointer_input_state = self.get_pointer_input_state(source_id);
505 (pointer_input_state.x, pointer_input_state.y)
506 };
507
508 // Step 7. If x != current x or y != current y, run the following steps:
509 // FIXME: Actually "last" should not be checked here based on spec.
510 if x != current_x || y != current_y || last {
511 // Step 7.1. Let buttons be equal to input state's buttons property.
512 // Step 7.2. Perform implementation-specific action dispatch steps
513 let point = WebViewPoint::Page(Point2D::new(x as f32, y as f32));
514 let input_event = InputEvent::MouseMove(MouseMoveEvent::new(point));
515 if last {
516 self.send_blocking_input_event_to_embedder(input_event);
517 } else {
518 self.send_input_event_to_embedder(input_event);
519 }
520
521 // Step 7.3. Let input state's x property equal x and y property equal y.
522 let pointer_input_state = self.get_pointer_input_state_mut(source_id);
523 pointer_input_state.x = x;
524 pointer_input_state.y = y;
525 }
526
527 // Step 8. If last is true, return.
528 if last {
529 return;
530 }
531
532 // Step 9. Run the following substeps in parallel:
533 // Step 9.1. Asynchronously wait for an implementationdefined amount of time to pass
534 thread::sleep(Duration::from_millis(POINTERMOVE_INTERVAL));
535
536 // Step 9.2. Perform a pointer move with arguments
537 // input state, duration, start x, start y, target x, target y.
538 // Notice that this simply repeat what we have done until last is true.
539 }
540 }
541
542 /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-scroll-action>
543 fn dispatch_scroll_action(
544 &self,
545 source_id: &str,
546 action: &WheelScrollAction,
547 tick_duration: u64,
548 ) -> Result<(), ErrorStatus> {
549 // TODO: We should verify each variable when processing a wheel action.
550 // <https://w3c.github.io/webdriver/#dfn-process-a-wheel-action>
551
552 let tick_start = Instant::now();
553
554 // Step 1. Let x offset be equal to the x property of action object.
555 let Some(x_offset) = action.x else {
556 return Err(ErrorStatus::InvalidArgument);
557 };
558
559 // Step 2. Let y offset be equal to the y property of action object.
560 let Some(y_offset) = action.y else {
561 return Err(ErrorStatus::InvalidArgument);
562 };
563
564 // Step 3. Let origin be equal to the origin property of action object.
565 let origin = &action.origin;
566
567 // Pointer origin isn't currently supported for wheel input source
568 // See: https://github.com/w3c/webdriver/issues/1758
569
570 if origin == &PointerOrigin::Pointer {
571 return Err(ErrorStatus::InvalidArgument);
572 }
573
574 // Step 4. Let (x, y) be the result of trying to get coordinates relative to an origin
575 // with source, x offset, y offset, origin, browsing context, and actions options.
576 let (x, y) =
577 self.get_origin_relative_coordinates(origin, x_offset as _, y_offset as _, source_id)?;
578
579 // Step 5. If x is less than 0 or greater than the width of the viewport in CSS pixels,
580 // then return error with error code move target out of bounds.
581 // Step 6. If y is less than 0 or greater than the height of the viewport in CSS pixels,
582 // then return error with error code move target out of bounds.
583 self.check_viewport_bound(x, y)?;
584
585 // Step 7. Let delta x be equal to the deltaX property of action object.
586 let Some(delta_x) = action.deltaX else {
587 return Err(ErrorStatus::InvalidArgument);
588 };
589
590 // Step 8. Let delta y be equal to the deltaY property of action object.
591 let Some(delta_y) = action.deltaY else {
592 return Err(ErrorStatus::InvalidArgument);
593 };
594
595 // Step 9. Let duration be equal to action object's duration property
596 // if it is not undefined, or tick duration otherwise.
597 let duration = match action.duration {
598 Some(duration) => duration,
599 None => tick_duration,
600 };
601
602 // Step 10. If duration is greater than 0 and inside any implementation-defined bounds,
603 // asynchronously wait for an implementation defined amount of time to pass.
604 if duration > 0 {
605 thread::sleep(Duration::from_millis(WHEELSCROLL_INTERVAL));
606 }
607
608 // Step 11. Perform a scroll with arguments global key state, duration, x, y, delta x, delta y, 0, 0.
609 self.perform_scroll(
610 duration,
611 x,
612 y,
613 delta_x as _,
614 delta_y as _,
615 0.0,
616 0.0,
617 tick_start,
618 );
619
620 // Step 12. Return success with data null.
621 Ok(())
622 }
623
624 /// <https://w3c.github.io/webdriver/#dfn-perform-a-scroll>
625 #[allow(clippy::too_many_arguments)]
626 fn perform_scroll(
627 &self,
628 duration: u64,
629 x: f64,
630 y: f64,
631 target_delta_x: f64,
632 target_delta_y: f64,
633 mut curr_delta_x: f64,
634 mut curr_delta_y: f64,
635 tick_start: Instant,
636 ) {
637 loop {
638 // Step 1. Let time delta be the time since the beginning of the current tick,
639 // measured in milliseconds on a monotonic clock.
640 let time_delta = tick_start.elapsed().as_millis();
641
642 // Step 2. Let duration ratio be the ratio of time delta and duration,
643 // if duration is greater than 0, or 1 otherwise.
644 let duration_ratio = if duration > 0 {
645 time_delta as f64 / duration as f64
646 } else {
647 1.0
648 };
649
650 // Step 3. If duration ratio is 1, or close enough to 1 that
651 // the implementation will not further subdivide the move action,
652 // let last be true. Otherwise let last be false.
653 let last = 1.0 - duration_ratio < 0.001;
654
655 // Step 4. If last is true,
656 // let delta x equal target delta x - current delta x and delta y equal target delta y - current delta y.
657 // Otherwise
658 // let delta x equal an approximation to duration ratio × target delta x - current delta x,
659 // and delta y equal an approximation to duration ratio × target delta y - current delta y.
660 let (delta_x, delta_y) = if last {
661 (target_delta_x - curr_delta_x, target_delta_y - curr_delta_y)
662 } else {
663 (
664 duration_ratio * target_delta_x - curr_delta_x,
665 duration_ratio * target_delta_y - curr_delta_y,
666 )
667 };
668
669 // Step 5. If delta x != 0 or delta y != 0, run the following steps:
670 // Actually "last" should not be checked here based on spec.
671 // However, we need to send the webdriver id at the final perform.
672 if delta_x != 0.0 || delta_y != 0.0 || last {
673 // Step 5.1. Perform implementation-specific action dispatch steps
674 let delta = WheelDelta {
675 x: -delta_x,
676 y: -delta_y,
677 z: 0.0,
678 mode: WheelMode::DeltaPixel,
679 };
680 let point = WebViewPoint::Page(Point2D::new(x as f32, y as f32));
681 let input_event = InputEvent::Wheel(WheelEvent::new(delta, point));
682 if last {
683 self.send_blocking_input_event_to_embedder(input_event);
684 } else {
685 self.send_input_event_to_embedder(input_event);
686 }
687
688 // Step 5.2. Let current delta x property equal delta x + current delta x
689 // and current delta y property equal delta y + current delta y.
690 curr_delta_x += delta_x;
691 curr_delta_y += delta_y;
692 }
693
694 // Step 6. If last is true, return.
695 if last {
696 return;
697 }
698
699 // Step 7
700 // TODO: The two steps should be done in parallel
701 // 7.1. Asynchronously wait for an implementation defined amount of time to pass.
702 thread::sleep(Duration::from_millis(WHEELSCROLL_INTERVAL));
703 // 7.2. Perform a scroll with arguments duration, x, y, target delta x,
704 // target delta y, current delta x, current delta y.
705 // Notice that this simply repeat what we have done until last is true.
706 }
707 }
708
709 /// Verify that the given coordinates are within the boundary of the viewport.
710 /// If x or y is less than 0 or greater than the width of the viewport in CSS pixels,
711 /// then return error with error code move target out of bounds.
712 fn check_viewport_bound(&self, x: f64, y: f64) -> Result<(), ErrorStatus> {
713 if x < 0.0 || y < 0.0 {
714 return Err(ErrorStatus::MoveTargetOutOfBounds);
715 }
716 let (sender, receiver) = ipc::channel().unwrap();
717 let cmd_msg = WebDriverCommandMsg::GetViewportSize(self.verified_webview_id(), sender);
718 self.send_message_to_embedder(cmd_msg)
719 .map_err(|_| ErrorStatus::UnknownError)?;
720
721 let viewport_size = match wait_for_ipc_response(receiver) {
722 Ok(response) => response,
723 Err(WebDriverError { error, .. }) => return Err(error),
724 };
725 if x > viewport_size.width.into() || y > viewport_size.height.into() {
726 Err(ErrorStatus::MoveTargetOutOfBounds)
727 } else {
728 Ok(())
729 }
730 }
731
732 /// <https://w3c.github.io/webdriver/#dfn-get-coordinates-relative-to-an-origin>
733 fn get_origin_relative_coordinates(
734 &self,
735 origin: &PointerOrigin,
736 x_offset: f64,
737 y_offset: f64,
738 source_id: &str,
739 ) -> Result<(f64, f64), ErrorStatus> {
740 match origin {
741 PointerOrigin::Viewport => Ok((x_offset, y_offset)),
742 PointerOrigin::Pointer => {
743 // Step 1. Let start x be equal to the x property of source.
744 // Step 2. Let start y be equal to the y property of source.
745 let (start_x, start_y) = {
746 let pointer_input_state = self.get_pointer_input_state(source_id);
747 (pointer_input_state.x, pointer_input_state.y)
748 };
749 // Step 3. Let x equal start x + x offset and y equal start y + y offset.
750 Ok((start_x + x_offset, start_y + y_offset))
751 },
752 PointerOrigin::Element(web_element) => {
753 // Steps 1 - 3
754 let (x_element, y_element) = self.get_element_in_view_center_point(web_element)?;
755 // Step 4. Let x equal x element + x offset, and y equal y element + y offset.
756 Ok((x_element as f64 + x_offset, y_element as f64 + y_offset))
757 },
758 }
759 }
760
761 /// <https://w3c.github.io/webdriver/#dfn-center-point>
762 fn get_element_in_view_center_point(
763 &self,
764 web_element: &WebElement,
765 ) -> Result<(i64, i64), ErrorStatus> {
766 let (sender, receiver) = ipc::channel().unwrap();
767 // Step 1. Let element be the result of trying to run actions options'
768 // get element origin steps with origin and browsing context.
769 self.browsing_context_script_command(
770 WebDriverScriptCommand::GetElementInViewCenterPoint(web_element.to_string(), sender),
771 VerifyBrowsingContextIsOpen::No,
772 )
773 .unwrap();
774
775 // Step 2. If element is null, return error with error code no such element.
776 let response = match wait_for_ipc_response(receiver) {
777 Ok(response) => response,
778 Err(WebDriverError { error, .. }) => return Err(error),
779 };
780
781 // Step 3. Let x element and y element be the result of calculating the in-view center point of element.
782 match response? {
783 Some(point) => Ok(point),
784 None => Err(ErrorStatus::UnknownError),
785 }
786 }
787
788 /// <https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence>
789 pub(crate) fn extract_an_action_sequence(
790 &mut self,
791 actions: Vec<ActionSequence>,
792 ) -> ActionsByTick {
793 // Step 3. Let "actions by tick" be an empty list.
794 let mut actions_by_tick: ActionsByTick = Vec::new();
795
796 // Step 4. For each value "action sequence" corresponding to an indexed property in actions
797 for action_sequence in actions {
798 let id = action_sequence.id.clone();
799 // Step 4.1. Let "source actions" be the result of trying to process an input source action sequence
800 // given "action sequence".
801 let source_actions = self.process_an_input_source_action_sequence(action_sequence);
802
803 // Step 4.2.2. Ensure we have enough ticks to hold all actions
804 if actions_by_tick.len() < source_actions.len() {
805 actions_by_tick.resize_with(source_actions.len(), Vec::new);
806 }
807
808 // Step 4.2.3. Append "action" to the List at index i in "actions by tick",
809 // for each "action" in "source actions".
810 for (tick_index, action_item) in source_actions.into_iter().enumerate() {
811 actions_by_tick[tick_index].push((id.clone(), action_item));
812 }
813 }
814
815 actions_by_tick
816 }
817
818 /// <https://w3c.github.io/webdriver/#dfn-process-an-input-source-action-sequence>
819 fn process_an_input_source_action_sequence(
820 &mut self,
821 action_sequence: ActionSequence,
822 ) -> Vec<ActionItem> {
823 // Step 2. Let id be the value of the id property of action sequence.
824 let id = action_sequence.id;
825 match action_sequence.actions {
826 ActionsType::Null {
827 actions: null_actions,
828 } => {
829 self.input_state_table_mut()
830 .entry(id)
831 .or_insert(InputSourceState::Null);
832 null_actions.into_iter().map(ActionItem::Null).collect()
833 },
834 ActionsType::Key {
835 actions: key_actions,
836 } => {
837 self.input_state_table_mut()
838 .entry(id)
839 .or_insert(InputSourceState::Key(KeyInputState::new()));
840 key_actions.into_iter().map(ActionItem::Key).collect()
841 },
842 ActionsType::Pointer {
843 parameters: _,
844 actions: pointer_actions,
845 } => {
846 let pointer_ids = self.session().unwrap().pointer_ids();
847 self.input_state_table_mut()
848 .entry(id)
849 .or_insert(InputSourceState::Pointer(PointerInputState::new(
850 PointerType::Mouse,
851 pointer_ids,
852 )));
853 pointer_actions
854 .into_iter()
855 .map(ActionItem::Pointer)
856 .collect()
857 },
858 ActionsType::Wheel {
859 actions: wheel_actions,
860 } => {
861 self.input_state_table_mut()
862 .entry(id)
863 .or_insert(InputSourceState::Wheel);
864 wheel_actions.into_iter().map(ActionItem::Wheel).collect()
865 },
866 }
867 }
868
869 fn input_state_table_mut(&mut self) -> &mut HashMap<String, InputSourceState> {
870 &mut self.session_mut().unwrap().input_state_table
871 }
872
873 fn get_pointer_input_state_mut(&mut self, source_id: &str) -> &mut PointerInputState {
874 let InputSourceState::Pointer(pointer_input_state) =
875 self.input_state_table_mut().get_mut(source_id).unwrap()
876 else {
877 unreachable!();
878 };
879 pointer_input_state
880 }
881
882 fn get_pointer_input_state(&self, source_id: &str) -> &PointerInputState {
883 let InputSourceState::Pointer(pointer_input_state) = self
884 .session()
885 .unwrap()
886 .input_state_table
887 .get(source_id)
888 .unwrap()
889 else {
890 unreachable!();
891 };
892 pointer_input_state
893 }
894}