/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::thread; use std::time::{Duration, Instant}; use base::id::BrowsingContextId; use embedder_traits::{MouseButtonAction, WebDriverCommandMsg, WebDriverScriptCommand}; use ipc_channel::ipc; use keyboard_types::webdriver::KeyInputState; use log::{error, info}; use rustc_hash::FxHashSet; use webdriver::actions::{ ActionSequence, ActionsType, GeneralAction, KeyAction, KeyActionItem, KeyDownAction, KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerDownAction, PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, WheelActionItem, WheelScrollAction, }; use webdriver::command::ActionsParameters; use webdriver::error::{ErrorStatus, WebDriverError}; use crate::{Handler, VerifyBrowsingContextIsOpen, WebElement, wait_for_ipc_response}; // Interval between wheelScroll and pointerMove increments in ms, based on common vsync static POINTERMOVE_INTERVAL: u64 = 17; static WHEELSCROLL_INTERVAL: u64 = 17; // https://262.ecma-international.org/6.0/#sec-number.max_safe_integer static MAXIMUM_SAFE_INTEGER: u64 = 9_007_199_254_740_991; // A single action, corresponding to an `action object` in the spec. // In the spec, `action item` refers to a plain JSON object. // However, we use the name ActionItem here // to be consistent with type names from webdriver crate. #[derive(Debug, PartialEq)] pub(crate) enum ActionItem { Null(NullActionItem), Key(KeyActionItem), Pointer(PointerActionItem), Wheel(WheelActionItem), } // A set of actions with multiple sources executed within a single tick. // The `id` is used to identify the source of the actions. pub(crate) type TickActions = Vec<(String, ActionItem)>; // Consumed by the `dispatch_actions` method. pub(crate) type ActionsByTick = Vec; /// pub(crate) enum InputSourceState { Null, #[allow(dead_code)] Key(KeyInputState), Pointer(PointerInputState), #[allow(dead_code)] Wheel, } // https://w3c.github.io/webdriver/#dfn-pointer-input-source // TODO: subtype is used for https://w3c.github.io/webdriver/#dfn-get-a-pointer-id // Need to add pointer-id to the following struct #[allow(dead_code)] pub(crate) struct PointerInputState { subtype: PointerType, pressed: FxHashSet, x: f64, y: f64, } impl PointerInputState { pub fn new(subtype: PointerType) -> PointerInputState { PointerInputState { subtype, pressed: FxHashSet::default(), x: 0.0, y: 0.0, } } } /// fn compute_tick_duration(tick_actions: &TickActions) -> u64 { // Step 1. Let max duration be 0. // Step 2. For each action in tick actions: tick_actions .iter() .filter_map(|(_, action_item)| { // If action object has subtype property set to "pause" or // action object has type property set to "pointer" and subtype property set to "pointerMove", // or action object has type property set to "wheel" and subtype property set to "scroll", // let duration be equal to the duration property of action object. match action_item { ActionItem::Null(NullActionItem::General(GeneralAction::Pause(pause_action))) | ActionItem::Key(KeyActionItem::General(GeneralAction::Pause(pause_action))) | ActionItem::Pointer(PointerActionItem::General(GeneralAction::Pause( pause_action, ))) | ActionItem::Wheel(WheelActionItem::General(GeneralAction::Pause(pause_action))) => { pause_action.duration }, ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move(action))) => { action.duration }, ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(action))) => { action.duration }, _ => None, } }) .max() .unwrap_or(0) } impl Handler { /// pub(crate) fn dispatch_actions( &self, actions_by_tick: ActionsByTick, browsing_context: BrowsingContextId, ) -> Result<(), ErrorStatus> { // Step 1. Wait for an action queue token with input state. let new_token = self.id_generator.next(); assert!(self.current_action_id.get().is_none()); self.current_action_id.set(Some(new_token)); // Step 2. Let actions result be the result of dispatch actions inner. let res = self.dispatch_actions_inner(actions_by_tick, browsing_context); // Step 3. Dequeue input state's actions queue. self.current_action_id.set(None); // Step 4. Return actions result. res } /// fn dispatch_actions_inner( &self, actions_by_tick: ActionsByTick, browsing_context: BrowsingContextId, ) -> Result<(), ErrorStatus> { // Step 1. For each item tick actions in actions by tick for tick_actions in actions_by_tick.iter() { // Step 1.1. If browsing context is no longer open, // return error with error code no such window. self.verify_browsing_context_is_open(browsing_context) .map_err(|e| e.error)?; // Step 1.2. Let tick duration be the result of // computing the tick duration with argument tick actions. let tick_duration = compute_tick_duration(tick_actions); // FIXME: This is out of spec, but the test `perform_actions/invalid.py` requires // that duration more than `MAXIMUM_SAFE_INTEGER` is considered invalid. if tick_duration > MAXIMUM_SAFE_INTEGER { return Err(ErrorStatus::InvalidArgument); } let now = Instant::now(); // Step 1.3. Try to dispatch tick actions self.dispatch_tick_actions(tick_actions, tick_duration)?; // Step 1.4. Wait for // The user agent event loop has spun enough times to process the DOM events // generated by the last invocation of the dispatch tick actions steps. self.wait_for_user_agent_handling_complete()?; // At least tick duration milliseconds have passed. let elapsed = now.elapsed(); if elapsed.as_millis() < tick_duration as u128 { let sleep_duration = tick_duration - elapsed.as_millis() as u64; thread::sleep(Duration::from_millis(sleep_duration)); } } // Step 2. Return success with data null. info!("Dispatch actions completed successfully"); Ok(()) } fn wait_for_user_agent_handling_complete(&self) -> Result<(), ErrorStatus> { // To ensure we wait for all events to be processed, only the last event // in each tick action step holds the message id. // Whenever a new event is generated, the message id is passed to it. // // Wait for num_pending_actions number of responses for _ in 0..self.num_pending_actions.get() { match self.webdriver_response_receiver.recv() { Ok(response) => { let current_waiting_id = self .current_action_id .get() .expect("Current id should be set before dispatch_actions_inner is called"); if current_waiting_id != response.id { error!("Dispatch actions completed with wrong id in response"); return Err(ErrorStatus::UnknownError); } }, Err(error) => { error!("Dispatch actions completed with IPC error: {error}"); return Err(ErrorStatus::UnknownError); }, }; } self.num_pending_actions.set(0); Ok(()) } /// fn dispatch_tick_actions( &self, tick_actions: &TickActions, tick_duration: u64, ) -> Result<(), ErrorStatus> { let session = self.session().unwrap(); // Step 1. For each action object in tick actions: // Step 1.1. Let input_id be the value of the id property of action object. for (input_id, action) in tick_actions.iter() { // Step 6. Let subtype be action object's subtype. // Steps 7, 8. Try to run specific algorithm based on the action type. match action { ActionItem::Null(NullActionItem::General(_)) => { self.dispatch_general_action(input_id); }, ActionItem::Key(KeyActionItem::General(_)) => { self.dispatch_general_action(input_id); }, ActionItem::Key(KeyActionItem::Key(KeyAction::Down(keydown_action))) => { self.dispatch_keydown_action(input_id, keydown_action); // Step 9. If subtype is "keyDown", append a copy of action // object with the subtype property changed to "keyUp" to // input state's input cancel list. session.input_cancel_list_mut().push(( input_id.clone(), ActionItem::Key(KeyActionItem::Key(KeyAction::Up(KeyUpAction { value: keydown_action.value.clone(), }))), )); }, ActionItem::Key(KeyActionItem::Key(KeyAction::Up(keyup_action))) => { self.dispatch_keyup_action(input_id, keyup_action); }, ActionItem::Pointer(PointerActionItem::General(_)) => { self.dispatch_general_action(input_id); }, ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Down( pointer_down_action, ))) => { self.dispatch_pointerdown_action(input_id, pointer_down_action); // Step 10. If subtype is "pointerDown", append a copy of action // object with the subtype property changed to "pointerUp" to // input state's input cancel list. session.input_cancel_list_mut().push(( input_id.clone(), ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up( PointerUpAction { button: pointer_down_action.button, ..Default::default() }, ))), )); }, ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move( pointer_move_action, ))) => { self.dispatch_pointermove_action(input_id, pointer_move_action, tick_duration)?; }, ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up( pointer_up_action, ))) => { self.dispatch_pointerup_action(input_id, pointer_up_action); }, ActionItem::Wheel(WheelActionItem::General(_)) => { self.dispatch_general_action(input_id); }, ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(scroll_action))) => { self.dispatch_scroll_action(input_id, scroll_action, tick_duration)?; }, _ => {}, } } Ok(()) } /// fn dispatch_general_action(&self, source_id: &str) { self.session() .unwrap() .input_state_table_mut() .entry(source_id.to_string()) .or_insert(InputSourceState::Null); } /// fn dispatch_keydown_action(&self, source_id: &str, action: &KeyDownAction) { let session = self.session().unwrap(); let mut input_state_table = session.input_state_table_mut(); let raw_key = action.value.chars().next().unwrap(); let key_input_state = match input_state_table.get_mut(source_id).unwrap() { InputSourceState::Key(key_input_state) => key_input_state, _ => unreachable!(), }; let keyboard_event = key_input_state.dispatch_keydown(raw_key); // Step 12 self.increment_num_pending_actions(); let msg_id = self.current_action_id.get(); let cmd_msg = WebDriverCommandMsg::KeyboardAction(self.verified_webview_id(), keyboard_event, msg_id); let _ = self.send_message_to_embedder(cmd_msg); } /// fn dispatch_keyup_action(&self, source_id: &str, action: &KeyUpAction) { let session = self.session().unwrap(); // Remove the last matching keyUp from `[input_cancel_list]` due to bugs in spec // See https://github.com/w3c/webdriver/issues/1905 && // https://github.com/servo/servo/issues/37579#issuecomment-2990762713 { let mut input_cancel_list = session.input_cancel_list_mut(); if let Some(pos) = input_cancel_list.iter().rposition(|(id, item)| { id == source_id && matches!(item, ActionItem::Key(KeyActionItem::Key(KeyAction::Up(KeyUpAction { value }))) if *value == action.value ) }) { info!("dispatch_keyup_action: removing last matching keyup from input_cancel_list"); input_cancel_list.remove(pos); } } let raw_key = action.value.chars().next().unwrap(); let mut input_state_table = session.input_state_table_mut(); let key_input_state = match input_state_table.get_mut(source_id).unwrap() { InputSourceState::Key(key_input_state) => key_input_state, _ => unreachable!(), }; if let Some(keyboard_event) = key_input_state.dispatch_keyup(raw_key) { // Step 12 self.increment_num_pending_actions(); let msg_id = self.current_action_id.get(); let cmd_msg = WebDriverCommandMsg::KeyboardAction( self.verified_webview_id(), keyboard_event, msg_id, ); let _ = self.send_message_to_embedder(cmd_msg); } } /// pub(crate) fn dispatch_pointerdown_action(&self, source_id: &str, action: &PointerDownAction) { let session = self.session().unwrap(); let mut input_state_table = session.input_state_table_mut(); let pointer_input_state = match input_state_table.get_mut(source_id).unwrap() { InputSourceState::Pointer(pointer_input_state) => pointer_input_state, _ => unreachable!(), }; // Step 3. If the source's pressed property contains button return success with data null. if pointer_input_state.pressed.contains(&action.button) { return; } // Step 6. Add button to the set corresponding to source's pressed property pointer_input_state.pressed.insert(action.button); // Step 7 - 15: Variable namings already done. // Step 16. Perform implementation-specific action dispatch steps // TODO: We have not considered pen/touch pointer type self.increment_num_pending_actions(); let msg_id = self.current_action_id.get(); let cmd_msg = WebDriverCommandMsg::MouseButtonAction( self.verified_webview_id(), MouseButtonAction::Down, action.button.into(), pointer_input_state.x as f32, pointer_input_state.y as f32, msg_id, ); let _ = self.send_message_to_embedder(cmd_msg); // Step 17. Return success with data null. } /// pub(crate) fn dispatch_pointerup_action(&self, source_id: &str, action: &PointerUpAction) { let session = self.session().unwrap(); let mut input_state_table = session.input_state_table_mut(); let pointer_input_state = match input_state_table.get_mut(source_id).unwrap() { InputSourceState::Pointer(pointer_input_state) => pointer_input_state, _ => unreachable!(), }; // Step 3. If the source's pressed property does not contain button, return success with data null. if !pointer_input_state.pressed.contains(&action.button) { return; } // Step 6. Remove button from the set corresponding to source's pressed property, pointer_input_state.pressed.remove(&action.button); // Remove matching pointerUp(must be unique) from `[input_cancel_list]` due to bugs in spec // See https://github.com/w3c/webdriver/issues/1905 && // https://github.com/servo/servo/issues/37579#issuecomment-2990762713 { let mut input_cancel_list = session.input_cancel_list_mut(); if let Some(pos) = input_cancel_list.iter().position(|(id, item)| { id == source_id && matches!(item, ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up( PointerUpAction { button, .. }, ))) if *button == action.button ) }) { info!( "dispatch_pointerup_action: removing matching pointerup from input_cancel_list" ); input_cancel_list.remove(pos); } } // Step 7. Perform implementation-specific action dispatch steps self.increment_num_pending_actions(); let msg_id = self.current_action_id.get(); let cmd_msg = WebDriverCommandMsg::MouseButtonAction( self.verified_webview_id(), MouseButtonAction::Up, action.button.into(), pointer_input_state.x as f32, pointer_input_state.y as f32, msg_id, ); let _ = self.send_message_to_embedder(cmd_msg); // Step 8. Return success with data null. } /// pub(crate) fn dispatch_pointermove_action( &self, source_id: &str, action: &PointerMoveAction, tick_duration: u64, ) -> Result<(), ErrorStatus> { let tick_start = Instant::now(); // Step 1. Let x offset be equal to the x property of action object. let x_offset = action.x; // Step 2. Let y offset be equal to the y property of action object. let y_offset = action.y; // Step 3. Let origin be equal to the origin property of action object. let origin = &action.origin; // Step 4. Let (x, y) be the result of trying to get coordinates relative to an origin // with source, x offset, y offset, origin, browsing context, and actions options. let (x, y) = self.get_origin_relative_coordinates(origin, x_offset, y_offset, source_id)?; // Step 5. If x is less than 0 or greater than the width of the viewport in CSS pixels, // then return error with error code move target out of bounds. // Step 6. If y is less than 0 or greater than the height of the viewport in CSS pixels, // then return error with error code move target out of bounds. self.check_viewport_bound(x, y)?; // Step 7. Let duration be equal to action object's duration property // if it is not undefined, or tick duration otherwise. let duration = match action.duration { Some(duration) => duration, None => tick_duration, }; // Step 8. If duration is greater than 0 and inside any implementation-defined bounds, // asynchronously wait for an implementation defined amount of time to pass. if duration > 0 { thread::sleep(Duration::from_millis(POINTERMOVE_INTERVAL)); } let (start_x, start_y) = match self .session() .unwrap() .input_state_table() .get(source_id) .unwrap() { InputSourceState::Pointer(pointer_input_state) => { (pointer_input_state.x, pointer_input_state.y) }, _ => unreachable!(), }; // Step 9 - 18 // Perform a pointer move with arguments source, global key state, duration, start x, start y, // x, y, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle. // TODO: We have not considered pen/touch pointer type self.perform_pointer_move(source_id, duration, start_x, start_y, x, y, tick_start); // Step 19. Return success with data null. Ok(()) } /// #[allow(clippy::too_many_arguments)] fn perform_pointer_move( &self, source_id: &str, duration: u64, start_x: f64, start_y: f64, target_x: f64, target_y: f64, tick_start: Instant, ) { let session = self.session().unwrap(); let mut input_state_table = session.input_state_table_mut(); let pointer_input_state = match input_state_table.get_mut(source_id).unwrap() { InputSourceState::Pointer(pointer_input_state) => pointer_input_state, _ => unreachable!(), }; loop { // Step 1. Let time delta be the time since the beginning of the // current tick, measured in milliseconds on a monotonic clock. let time_delta = tick_start.elapsed().as_millis(); // Step 2. Let duration ratio be the ratio of time delta and duration, // if duration is greater than 0, or 1 otherwise. let duration_ratio = if duration > 0 { time_delta as f64 / duration as f64 } else { 1.0 }; // Step 3. If duration ratio is 1, or close enough to 1 that the // implementation will not further subdivide the move action, // let last be true. Otherwise let last be false. let last = 1.0 - duration_ratio < 0.001; // Step 4. If last is true, let x equal target x and y equal target y. // Otherwise // let x equal an approximation to duration ratio × (target x - start x) + start x, // and y equal an approximation to duration ratio × (target y - start y) + start y. let (x, y) = if last { (target_x, target_y) } else { ( duration_ratio * (target_x - start_x) + start_x, duration_ratio * (target_y - start_y) + start_y, ) }; // Step 5. Let current x equal the x property of input state. let current_x = pointer_input_state.x; // Step 6. Let current y equal the y property of input state. let current_y = pointer_input_state.y; // Step 7. If x != current x or y != current y, run the following steps: // FIXME: Actually "last" should not be checked here based on spec. // However, we need to send the webdriver id at the final perform. if x != current_x || y != current_y || last { // Step 7.1. Let buttons be equal to input state's buttons property. // Step 7.2. Perform implementation-specific action dispatch steps let msg_id = if last { self.increment_num_pending_actions(); self.current_action_id.get() } else { None }; let cmd_msg = WebDriverCommandMsg::MouseMoveAction( self.verified_webview_id(), x as f32, y as f32, msg_id, ); let _ = self.send_message_to_embedder(cmd_msg); // Step 7.3. Let input state's x property equal x and y property equal y. pointer_input_state.x = x; pointer_input_state.y = y; } // Step 8. If last is true, return. if last { return; } // Step 9. Run the following substeps in parallel: // Step 9.1. Asynchronously wait for an implementationdefined amount of time to pass thread::sleep(Duration::from_millis(POINTERMOVE_INTERVAL)); // Step 9.2. Perform a pointer move with arguments // input state, duration, start x, start y, target x, target y. // Notice that this simply repeat what we have done until last is true. } } /// fn dispatch_scroll_action( &self, source_id: &str, action: &WheelScrollAction, tick_duration: u64, ) -> Result<(), ErrorStatus> { // TODO: We should verify each variable when processing a wheel action. // let tick_start = Instant::now(); // Step 1. Let x offset be equal to the x property of action object. let Some(x_offset) = action.x else { return Err(ErrorStatus::InvalidArgument); }; // Step 2. Let y offset be equal to the y property of action object. let Some(y_offset) = action.y else { return Err(ErrorStatus::InvalidArgument); }; // Step 3. Let origin be equal to the origin property of action object. let origin = &action.origin; // Pointer origin isn't currently supported for wheel input source // See: https://github.com/w3c/webdriver/issues/1758 if origin == &PointerOrigin::Pointer { return Err(ErrorStatus::InvalidArgument); } // Step 4. Let (x, y) be the result of trying to get coordinates relative to an origin // with source, x offset, y offset, origin, browsing context, and actions options. let (x, y) = self.get_origin_relative_coordinates(origin, x_offset as _, y_offset as _, source_id)?; // Step 5. If x is less than 0 or greater than the width of the viewport in CSS pixels, // then return error with error code move target out of bounds. // Step 6. If y is less than 0 or greater than the height of the viewport in CSS pixels, // then return error with error code move target out of bounds. self.check_viewport_bound(x, y)?; // Step 7. Let delta x be equal to the deltaX property of action object. let Some(delta_x) = action.deltaX else { return Err(ErrorStatus::InvalidArgument); }; // Step 8. Let delta y be equal to the deltaY property of action object. let Some(delta_y) = action.deltaY else { return Err(ErrorStatus::InvalidArgument); }; // Step 9. Let duration be equal to action object's duration property // if it is not undefined, or tick duration otherwise. let duration = match action.duration { Some(duration) => duration, None => tick_duration, }; // Step 10. If duration is greater than 0 and inside any implementation-defined bounds, // asynchronously wait for an implementation defined amount of time to pass. if duration > 0 { thread::sleep(Duration::from_millis(WHEELSCROLL_INTERVAL)); } // Step 11. Perform a scroll with arguments global key state, duration, x, y, delta x, delta y, 0, 0. self.perform_scroll( duration, x, y, delta_x as _, delta_y as _, 0.0, 0.0, tick_start, ); // Step 12. Return success with data null. Ok(()) } /// #[allow(clippy::too_many_arguments)] fn perform_scroll( &self, duration: u64, x: f64, y: f64, target_delta_x: f64, target_delta_y: f64, mut curr_delta_x: f64, mut curr_delta_y: f64, tick_start: Instant, ) { loop { // Step 1. Let time delta be the time since the beginning of the current tick, // measured in milliseconds on a monotonic clock. let time_delta = tick_start.elapsed().as_millis(); // Step 2. Let duration ratio be the ratio of time delta and duration, // if duration is greater than 0, or 1 otherwise. let duration_ratio = if duration > 0 { time_delta as f64 / duration as f64 } else { 1.0 }; // Step 3. If duration ratio is 1, or close enough to 1 that // the implementation will not further subdivide the move action, // let last be true. Otherwise let last be false. let last = 1.0 - duration_ratio < 0.001; // Step 4. If last is true, // let delta x equal target delta x - current delta x and delta y equal target delta y - current delta y. // Otherwise // let delta x equal an approximation to duration ratio × target delta x - current delta x, // and delta y equal an approximation to duration ratio × target delta y - current delta y. let (delta_x, delta_y) = if last { (target_delta_x - curr_delta_x, target_delta_y - curr_delta_y) } else { ( duration_ratio * target_delta_x - curr_delta_x, duration_ratio * target_delta_y - curr_delta_y, ) }; // Step 5. If delta x != 0 or delta y != 0, run the following steps: // Actually "last" should not be checked here based on spec. // However, we need to send the webdriver id at the final perform. if delta_x != 0.0 || delta_y != 0.0 || last { // Step 5.1. Perform implementation-specific action dispatch steps let msg_id = if last { self.increment_num_pending_actions(); self.current_action_id.get() } else { None }; let cmd_msg = WebDriverCommandMsg::WheelScrollAction( self.verified_webview_id(), x, y, delta_x, delta_y, msg_id, ); let _ = self.send_message_to_embedder(cmd_msg); // Step 5.2. Let current delta x property equal delta x + current delta x // and current delta y property equal delta y + current delta y. curr_delta_x += delta_x; curr_delta_y += delta_y; } // Step 6. If last is true, return. if last { return; } // Step 7 // TODO: The two steps should be done in parallel // 7.1. Asynchronously wait for an implementation defined amount of time to pass. thread::sleep(Duration::from_millis(WHEELSCROLL_INTERVAL)); // 7.2. Perform a scroll with arguments duration, x, y, target delta x, // target delta y, current delta x, current delta y. // Notice that this simply repeat what we have done until last is true. } } /// Verify that the given coordinates are within the boundary of the viewport. /// If x or y is less than 0 or greater than the width of the viewport in CSS pixels, /// then return error with error code move target out of bounds. fn check_viewport_bound(&self, x: f64, y: f64) -> Result<(), ErrorStatus> { if x < 0.0 || y < 0.0 { return Err(ErrorStatus::MoveTargetOutOfBounds); } let (sender, receiver) = ipc::channel().unwrap(); let cmd_msg = WebDriverCommandMsg::GetViewportSize(self.verified_webview_id(), sender); self.send_message_to_embedder(cmd_msg) .map_err(|_| ErrorStatus::UnknownError)?; let viewport_size = match wait_for_ipc_response(receiver) { Ok(response) => response, Err(WebDriverError { error, .. }) => return Err(error), }; if x > viewport_size.width.into() || y > viewport_size.height.into() { Err(ErrorStatus::MoveTargetOutOfBounds) } else { Ok(()) } } /// fn get_origin_relative_coordinates( &self, origin: &PointerOrigin, x_offset: f64, y_offset: f64, source_id: &str, ) -> Result<(f64, f64), ErrorStatus> { match origin { PointerOrigin::Viewport => Ok((x_offset, y_offset)), PointerOrigin::Pointer => { // Step 1. Let start x be equal to the x property of source. // Step 2. Let start y be equal to the y property of source. let (start_x, start_y) = match self .session() .unwrap() .input_state_table() .get(source_id) .unwrap() { InputSourceState::Pointer(pointer_input_state) => { (pointer_input_state.x, pointer_input_state.y) }, _ => unreachable!(), }; // Step 3. Let x equal start x + x offset and y equal start y + y offset. Ok((start_x + x_offset, start_y + y_offset)) }, PointerOrigin::Element(web_element) => { // Steps 1 - 3 let (x_element, y_element) = self.get_element_in_view_center_point(web_element)?; // Step 4. Let x equal x element + x offset, and y equal y element + y offset. Ok((x_element as f64 + x_offset, y_element as f64 + y_offset)) }, } } /// fn get_element_in_view_center_point( &self, web_element: &WebElement, ) -> Result<(i64, i64), ErrorStatus> { let (sender, receiver) = ipc::channel().unwrap(); // Step 1. Let element be the result of trying to run actions options' // get element origin steps with origin and browsing context. self.browsing_context_script_command( WebDriverScriptCommand::GetElementInViewCenterPoint(web_element.to_string(), sender), VerifyBrowsingContextIsOpen::No, ) .unwrap(); // Step 2. If element is null, return error with error code no such element. let response = match wait_for_ipc_response(receiver) { Ok(response) => response, Err(WebDriverError { error, .. }) => return Err(error), }; // Step 3. Let x element and y element be the result of calculating the in-view center point of element. match response? { Some(point) => Ok(point), None => Err(ErrorStatus::UnknownError), } } /// pub(crate) fn extract_an_action_sequence(&self, params: ActionsParameters) -> ActionsByTick { // Step 1. Let actions be the result of getting a property named "actions" from parameters. // Step 2. (ignored because params is already validated earlier). If actions is not a list, // return an error with status InvalidArgument. let actions = params.actions; self.actions_by_tick_from_sequence(actions) } pub(crate) fn actions_by_tick_from_sequence( &self, actions: Vec, ) -> ActionsByTick { // Step 3. Let actions by tick be an empty list. let mut actions_by_tick: ActionsByTick = Vec::new(); // Step 4. For each value action sequence corresponding to an indexed property in actions for action_sequence in actions { // Store id before moving action_sequence let id = action_sequence.id.clone(); // Step 4.1. Let source actions be the result of trying to process an input source action sequence let source_actions = self.process_an_input_source_action_sequence(action_sequence); // Step 4.2.2. Ensure we have enough ticks to hold all actions while actions_by_tick.len() < source_actions.len() { actions_by_tick.push(Vec::new()); } // Step 4.2.3. Append action to the List at index i in actions by tick. for (tick_index, action_item) in source_actions.into_iter().enumerate() { actions_by_tick[tick_index].push((id.clone(), action_item)); } } actions_by_tick } /// pub(crate) fn process_an_input_source_action_sequence( &self, action_sequence: ActionSequence, ) -> Vec { // Step 2. Let id be the value of the id property of action sequence. let id = action_sequence.id.clone(); let mut input_state_table = self.session().unwrap().input_state_table_mut(); match action_sequence.actions { ActionsType::Null { actions: null_actions, } => { input_state_table .entry(id) .or_insert(InputSourceState::Null); null_actions.into_iter().map(ActionItem::Null).collect() }, ActionsType::Key { actions: key_actions, } => { input_state_table .entry(id) .or_insert(InputSourceState::Key(KeyInputState::new())); key_actions.into_iter().map(ActionItem::Key).collect() }, ActionsType::Pointer { parameters: _, actions: pointer_actions, } => { input_state_table .entry(id) .or_insert(InputSourceState::Pointer(PointerInputState::new( PointerType::Mouse, ))); pointer_actions .into_iter() .map(ActionItem::Pointer) .collect() }, ActionsType::Wheel { actions: wheel_actions, } => { input_state_table .entry(id) .or_insert(InputSourceState::Wheel); wheel_actions.into_iter().map(ActionItem::Wheel).collect() }, } } }