diff --git a/components/webdriver_server/actions.rs b/components/webdriver_server/actions.rs index f33310ac001..ea25e52a6a1 100644 --- a/components/webdriver_server/actions.rs +++ b/components/webdriver_server/actions.rs @@ -2,7 +2,7 @@ * 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::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant}; use std::{cmp, thread}; @@ -12,10 +12,11 @@ use ipc_channel::ipc; use keyboard_types::webdriver::KeyInputState; use webdriver::actions::{ ActionSequence, ActionsType, GeneralAction, KeyAction, KeyActionItem, KeyDownAction, - KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerActionParameters, - PointerDownAction, PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, - WheelActionItem, WheelScrollAction, + KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerDownAction, + PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, WheelActionItem, + WheelScrollAction, }; +use webdriver::command::ActionsParameters; use webdriver::error::{ErrorStatus, WebDriverError}; use crate::{Handler, WebElement, wait_for_script_response}; @@ -24,11 +25,91 @@ use crate::{Handler, WebElement, wait_for_script_response}; static POINTERMOVE_INTERVAL: u64 = 17; static WHEELSCROLL_INTERVAL: u64 = 17; +// A single action, corresponding to an `action object` in the spec. +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 order in which they are performed is not guaranteed. +// The `id` is used to identify the source of the actions. +pub(crate) struct TickActions { + pub actions: HashMap, +} + +// Consumed by the `dispatch_actions` method. +pub(crate) struct ActionsByTick { + pub actions: Vec, +} + +/// +pub(crate) fn extract_an_action_sequence(params: ActionsParameters) -> ActionsByTick { + // Step 1. Let actions be the result of getting a property named "actions" from parameters. + let actions = params.actions; + + actions_by_tick_from_sequence(actions) +} + +pub(crate) fn actions_by_tick_from_sequence(actions: Vec) -> ActionsByTick { + let mut actions_by_tick: ActionsByTick = ActionsByTick { + actions: Vec::new(), + }; + + // Step 4. For each value action sequence corresponding to an indexed property in actions + for action_sequence in actions { + let mut source_actions: Vec = Vec::new(); + + match action_sequence.actions { + ActionsType::Null { + actions: null_actions, + } => { + source_actions.extend(null_actions.into_iter().map(ActionItem::Null)); + }, + ActionsType::Key { + actions: key_actions, + } => { + source_actions.extend(key_actions.into_iter().map(ActionItem::Key)); + }, + ActionsType::Pointer { + parameters: _, + actions: pointer_actions, + } => { + source_actions.extend(pointer_actions.into_iter().map(ActionItem::Pointer)); + }, + ActionsType::Wheel { + actions: wheel_actions, + } => { + source_actions.extend(wheel_actions.into_iter().map(ActionItem::Wheel)); + }, + } + + // Ensure we have enough ticks to hold all actions + while actions_by_tick.actions.len() < source_actions.len() { + actions_by_tick.actions.push(TickActions { + actions: HashMap::new(), + }); + } + + for (tick_index, action_item) in source_actions.into_iter().enumerate() { + actions_by_tick.actions[tick_index] + .actions + .insert(action_sequence.id.clone(), action_item); + } + } + + actions_by_tick +} + // https://w3c.github.io/webdriver/#dfn-input-source-state pub(crate) enum InputSourceState { Null, + #[allow(dead_code)] Key(KeyInputState), Pointer(PointerInputState), + #[allow(dead_code)] Wheel, } @@ -59,47 +140,43 @@ impl PointerInputState { } // https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration -fn compute_tick_duration(tick_actions: &ActionSequence) -> u64 { - let mut duration = 0; - match &tick_actions.actions { - ActionsType::Null { actions } => { - for action in actions.iter() { - let NullActionItem::General(GeneralAction::Pause(pause_action)) = action; - duration = cmp::max(duration, pause_action.duration.unwrap_or(0)); - } - }, - ActionsType::Pointer { - parameters: _, - actions, - } => { - for action in actions.iter() { - let action_duration = match action { - PointerActionItem::General(GeneralAction::Pause(action)) => action.duration, - PointerActionItem::Pointer(PointerAction::Move(action)) => action.duration, - _ => None, - }; - duration = cmp::max(duration, action_duration.unwrap_or(0)); - } - }, - ActionsType::Key { actions: _ } => (), - ActionsType::Wheel { actions } => { - for action in actions.iter() { - let action_duration = match action { - WheelActionItem::General(GeneralAction::Pause(action)) => action.duration, - WheelActionItem::Wheel(WheelAction::Scroll(action)) => action.duration, - }; - duration = cmp::max(duration, action_duration.unwrap_or(0)); - } - }, - } - duration +fn compute_tick_duration(tick_actions: &TickActions) -> u64 { + // Step 1. Let max duration be 0. + let mut max_duration = 0; + + // Step 2. For each action in tick actions: + tick_actions.actions.iter().for_each(|(_, 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. + let action_duration = 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.unwrap_or(0) + }, + ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move(action))) => { + action.duration.unwrap_or(0) + }, + ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(action))) => { + action.duration.unwrap_or(0) + }, + _ => 0, + }; + max_duration = cmp::max(max_duration, action_duration); + }); + + // Step 3. Return max duration. + max_duration } impl Handler { // https://w3c.github.io/webdriver/#dfn-dispatch-actions pub(crate) fn dispatch_actions( &self, - actions_by_tick: &[ActionSequence], + actions_by_tick: ActionsByTick, ) -> Result<(), ErrorStatus> { // Step 1. Wait for an action queue token with input state. let new_token = self.id_generator.next(); @@ -117,12 +194,9 @@ impl Handler { } // https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner - fn dispatch_actions_inner( - &self, - actions_by_tick: &[ActionSequence], - ) -> Result<(), ErrorStatus> { + fn dispatch_actions_inner(&self, actions_by_tick: ActionsByTick) -> Result<(), ErrorStatus> { // Step 1. For each item tick actions in actions by tick - for tick_actions in actions_by_tick.iter() { + for tick_actions in actions_by_tick.actions.iter() { // 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); @@ -137,15 +211,6 @@ impl Handler { // To ensure we wait for all events to be processed, only the last event in // this tick action step holds the message id. // Whenever a new event is generated, the message id is passed to it. - // - // TO-DO: remove the first match after webdriver_id is implemented in all commands - match tick_actions.actions { - ActionsType::Key { .. } | ActionsType::Wheel { .. } | ActionsType::Null { .. } => { - return Ok(()); - }, - _ => {}, - } - match self.constellation_receiver.recv() { Ok(response) => { let current_waiting_id = self @@ -170,6 +235,86 @@ impl Handler { Ok(()) } + // https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions + fn dispatch_tick_actions( + &self, + tick_actions: &TickActions, + tick_duration: u64, + ) -> Result<(), ErrorStatus> { + // 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.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. + self.session() + .unwrap() + .input_cancel_list + .borrow_mut() + .push(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. + self.session().unwrap().input_cancel_list.borrow_mut().push( + 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(scroll_action, tick_duration)?; + }, + _ => {}, + } + } + + Ok(()) + } + fn dispatch_general_action(&self, source_id: &str) { self.session() .unwrap() @@ -181,142 +326,6 @@ impl Handler { // Nothing to be done } - // https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions - fn dispatch_tick_actions( - &self, - tick_actions: &ActionSequence, - tick_duration: u64, - ) -> Result<(), ErrorStatus> { - let source_id = &tick_actions.id; - match &tick_actions.actions { - ActionsType::Null { actions } => { - for _action in actions.iter() { - self.dispatch_general_action(source_id); - } - }, - ActionsType::Key { actions } => { - for action in actions.iter() { - match action { - KeyActionItem::General(_action) => { - self.dispatch_general_action(source_id); - }, - KeyActionItem::Key(action) => { - self.session() - .unwrap() - .input_state_table - .borrow_mut() - .entry(source_id.to_string()) - .or_insert(InputSourceState::Key(KeyInputState::new())); - match action { - KeyAction::Down(action) => { - self.dispatch_keydown_action(source_id, 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. - self.session().unwrap().input_cancel_list.borrow_mut().push( - ActionSequence { - id: source_id.into(), - actions: ActionsType::Key { - actions: vec![KeyActionItem::Key(KeyAction::Up( - KeyUpAction { - value: action.value.clone(), - }, - ))], - }, - }, - ); - }, - KeyAction::Up(action) => { - self.dispatch_keyup_action(source_id, action) - }, - }; - }, - } - } - }, - ActionsType::Pointer { - parameters, - actions, - } => { - for action in actions.iter() { - match action { - PointerActionItem::General(_action) => { - self.dispatch_general_action(source_id); - }, - PointerActionItem::Pointer(action) => { - self.session() - .unwrap() - .input_state_table - .borrow_mut() - .entry(source_id.to_string()) - .or_insert(InputSourceState::Pointer(PointerInputState::new( - ¶meters.pointer_type, - ))); - match action { - PointerAction::Cancel => (), - PointerAction::Down(action) => { - self.dispatch_pointerdown_action(source_id, 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. - self.session().unwrap().input_cancel_list.borrow_mut().push( - ActionSequence { - id: source_id.into(), - actions: ActionsType::Pointer { - parameters: PointerActionParameters { - pointer_type: parameters.pointer_type, - }, - actions: vec![PointerActionItem::Pointer( - PointerAction::Up(PointerUpAction { - button: action.button, - ..Default::default() - }), - )], - }, - }, - ); - }, - PointerAction::Move(action) => self.dispatch_pointermove_action( - source_id, - action, - tick_duration, - )?, - PointerAction::Up(action) => { - self.dispatch_pointerup_action(source_id, action) - }, - } - }, - } - } - }, - ActionsType::Wheel { actions } => { - for action in actions.iter() { - match action { - WheelActionItem::General(_action) => { - self.dispatch_general_action(source_id) - }, - WheelActionItem::Wheel(action) => { - self.session() - .unwrap() - .input_state_table - .borrow_mut() - .entry(source_id.to_string()) - .or_insert(InputSourceState::Wheel); - match action { - WheelAction::Scroll(action) => { - self.dispatch_scroll_action(action, tick_duration)? - }, - } - }, - } - } - }, - } - - Ok(()) - } - // https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action fn dispatch_keydown_action(&self, source_id: &str, action: &KeyDownAction) { let session = self.session().unwrap(); diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index 0e3fa9058d6..4ef3e0bd10e 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -64,7 +64,10 @@ use webdriver::response::{ }; use webdriver::server::{self, Session, SessionTeardownKind, WebDriverHandler}; -use crate::actions::{InputSourceState, PointerInputState}; +use crate::actions::{ + ActionItem, InputSourceState, PointerInputState, actions_by_tick_from_sequence, + extract_an_action_sequence, +}; #[derive(Default)] pub struct WebDriverMessageIdGenerator { @@ -145,7 +148,9 @@ pub fn start_server(port: u16, constellation_chan: Sender, @@ -171,7 +176,7 @@ pub struct WebDriverSession { input_state_table: RefCell>, /// - input_cancel_list: RefCell>, + input_cancel_list: RefCell>, } impl WebDriverSession { @@ -1480,20 +1485,22 @@ impl Handler { fn handle_perform_actions( &mut self, - parameters: &ActionsParameters, + parameters: ActionsParameters, ) -> WebDriverResult { - match self.dispatch_actions(¶meters.actions) { + // Step 5. Let actions by tick be the result of trying to extract an action sequence + let actions_by_tick = extract_an_action_sequence(parameters); + + // Step 6. Dispatch actions + match self.dispatch_actions(actions_by_tick) { Ok(_) => Ok(WebDriverResponse::Void), Err(error) => Err(WebDriverError::new(error, "")), } } + /// fn handle_release_actions(&mut self) -> WebDriverResult { - let input_cancel_list = self.session().unwrap().input_cancel_list.borrow(); - if let Err(error) = self.dispatch_actions(&input_cancel_list) { - return Err(WebDriverError::new(error, "")); - } - + // TODO: The previous implementation of this function was different from the spec. + // Need to re-implement this to match the spec. let session = self.session()?; session.input_state_table.borrow_mut().clear(); @@ -1702,7 +1709,9 @@ impl Handler { }, }; - let _ = self.dispatch_actions(&[action_sequence]); + let actions_by_tick = actions_by_tick_from_sequence(vec![action_sequence]); + + let _ = self.dispatch_actions(actions_by_tick); // Step 8.17 Remove an input source with input state and input id. self.session_mut()? @@ -1936,7 +1945,9 @@ impl WebDriverHandler for Handler { self.handle_element_css(element, name) }, WebDriverCommand::GetPageSource => self.handle_get_page_source(), - WebDriverCommand::PerformActions(ref x) => self.handle_perform_actions(x), + WebDriverCommand::PerformActions(actions_parameters) => { + self.handle_perform_actions(actions_parameters) + }, WebDriverCommand::ReleaseActions => self.handle_release_actions(), WebDriverCommand::ExecuteScript(ref x) => self.handle_execute_script(x), WebDriverCommand::ExecuteAsyncScript(ref x) => self.handle_execute_async_script(x),