Files
servo/components/webdriver_server/actions.rs
Euclid Ye 12b8f5794b webdriver: Improve error reporting for Cookie Addition (#43690)
Improve the error reporting for [cookie
addition](https://w3c.github.io/webdriver/#add-cookie).
Most notably, when
- expiry time exceeds maximum safe integer
- cookie domain is not equal to session's current browsing context's
active document's domain

There is a bug with spec:
https://github.com/servo/servo/pull/43690#discussion_r2994771111.
We fix spec in https://github.com/w3c/webdriver/pull/1955 and
make sure Servo behaves consistently with other browsers.

Testing: Added one test for invalid expiry time. New passing with
existing.

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
2026-03-26 17:38:30 +00:00

1101 lines
45 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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::collections::HashMap;
use std::thread;
use std::time::{Duration, Instant};
use embedder_traits::{
InputEvent, KeyboardEvent, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, TouchEvent,
TouchEventType, TouchId, WebDriverCommandMsg, WebDriverScriptCommand, WebViewPoint, WheelDelta,
WheelEvent, WheelMode,
};
use euclid::Point2D;
use keyboard_types::webdriver::KeyInputState;
use log::info;
use rustc_hash::FxHashSet;
use servo_base::generic_channel;
use servo_base::id::BrowsingContextId;
use webdriver::actions::{
ActionSequence, ActionsType, GeneralAction, KeyAction, KeyActionItem, KeyDownAction,
KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerDownAction,
PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, WheelActionItem,
WheelScrollAction,
};
use webdriver::error::{ErrorStatus, WebDriverError};
use crate::{
Handler, MAXIMUM_SAFE_INTEGER, VerifyBrowsingContextIsOpen, WebElement,
wait_for_oneshot_response,
};
/// Interval between wheelScroll and pointerMove increments in ms, based on common vsync
static MOVESCROLL_INTERVAL: u64 = 16;
/// <https://w3c.github.io/webdriver/#dfn-element-click>
/// This is hard-coded as 0 in spec.
pub(crate) static ELEMENT_CLICK_BUTTON: u64 = 0;
// 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<TickActions>;
/// <https://w3c.github.io/webdriver/#dfn-input-source-state>
pub(crate) enum InputSourceState {
Null,
Key(KeyInputState),
Pointer(PointerInputState),
Wheel,
}
#[expect(private_interfaces)]
pub(crate) enum PendingActions {
Scroll(PendingScroll),
PointerMove(PendingPointerMove),
}
/// <https://github.com/w3c/webdriver/issues/1952>
/// For some unknown reason, unlike Pointer Actions,
/// Wheel Actions in spec has precision of integer.
/// We just use double precision, since our [`WheelEvent`] also uses double precision.
struct PendingScroll {
input_id: String,
duration: u64,
x: f64,
y: f64,
current_delta_x: f64,
current_delta_y: f64,
target_delta_x: f64,
target_delta_y: f64,
}
struct PendingPointerMove {
input_id: String,
duration: u64,
start_x: f64,
start_y: f64,
target_x: f64,
target_y: f64,
}
/// <https://w3c.github.io/webdriver/#dfn-pointer-input-source>
pub(crate) struct PointerInputState {
subtype: PointerType,
pressed: FxHashSet<u64>,
pub(crate) pointer_id: u32,
x: f64,
y: f64,
}
impl PointerInputState {
/// <https://w3c.github.io/webdriver/#dfn-create-a-pointer-input-source>
pub(crate) fn new(
subtype: PointerType,
pointer_ids: FxHashSet<u32>,
x: f64,
y: f64,
) -> PointerInputState {
PointerInputState {
subtype,
pressed: FxHashSet::default(),
pointer_id: Self::get_pointer_id(subtype, pointer_ids),
x,
y,
}
}
/// <https://w3c.github.io/webdriver/#dfn-get-a-pointer-id>
fn get_pointer_id(subtype: PointerType, pointer_ids: FxHashSet<u32>) -> u32 {
// Step 2 - 4: Let pointer ids be all the values in input state map which is
// pointer input source. This is already done and passed by the caller.
if subtype == PointerType::Mouse {
for id in 0..=1 {
if !pointer_ids.contains(&id) {
return id;
}
}
}
// We are dealing with subtype other than mouse, which has minimum id 2.
1 + pointer_ids.into_iter().max().unwrap_or(1)
}
}
/// <https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration>
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 {
/// <https://w3c.github.io/webdriver/#dfn-dispatch-actions>
/// <https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner>
/// For Servo, "dispatch actions" is identical to "dispatch actions inner",
/// as they are only different for a session that can run commands in parallel.
pub(crate) fn dispatch_actions(
&mut 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 tick_start = Instant::now();
// Step 1.3. Try to dispatch tick actions
self.dispatch_tick_actions(tick_actions, tick_duration, &tick_start)?;
self.process_pending_actions(&tick_start);
// Step 1.4.1
// There are no pending asynchronous waits arising
// from the last invocation of the dispatch tick actions steps.
// Step 1.4.2
// 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.
// Step 1.4.3
// At least tick duration milliseconds have passed.
// TODO: 1.4.2 is not fully implemented.
// Specifically, this happens for click simulation from touch events.
// You can guarantee to catch it with `time.sleep` in the test.
self.wait_for_input_event_handled()?;
// At least tick duration milliseconds have passed.
let elapsed = tick_start.elapsed().as_millis() as u64;
if elapsed < tick_duration {
thread::sleep(Duration::from_millis(tick_duration - elapsed));
}
}
// Step 2. Return success with data null.
info!("Dispatch actions completed successfully");
Ok(())
}
/// <https://w3c.github.io/webdriver/#dfn-perform-a-pointer-move>
/// Step 9. Run the following substeps in parallel:
/// Step 9.1. Asynchronously wait for an implementation defined amount of time to pass.
/// Step 9.2. Perform a pointer move with arguments input state,
/// duration, start x, start y, target x, target y.
/// <https://w3c.github.io/webdriver/#dfn-perform-a-scroll>
/// Step 7. Run the following substeps in parallel:
/// Step 7.1. Asynchronously wait for an implementation defined amount of time to pass.
/// Step 7.2. Perform a scroll with arguments duration, x, y,
/// target delta x, target delta y, current delta x, current delta y.
fn process_pending_actions(&mut self, tick_start: &Instant) {
while !self.pending_actions.is_empty() {
let pending_actions = std::mem::take(&mut self.pending_actions);
thread::sleep(Duration::from_millis(MOVESCROLL_INTERVAL));
for action in pending_actions {
match action {
PendingActions::PointerMove(PendingPointerMove {
input_id,
duration,
start_x,
start_y,
target_x,
target_y,
}) => {
self.perform_pointer_move(
&input_id, duration, start_x, start_y, target_x, target_y, tick_start,
);
},
PendingActions::Scroll(PendingScroll {
input_id,
duration,
x,
y,
current_delta_x,
current_delta_y,
target_delta_x,
target_delta_y,
}) => {
self.perform_scroll(
&input_id,
duration,
x,
y,
target_delta_x,
target_delta_y,
current_delta_x,
current_delta_y,
tick_start,
);
},
}
}
}
}
fn wait_for_input_event_handled(&mut self) -> Result<(), ErrorStatus> {
let pending_receivers = std::mem::take(&mut self.pending_input_event_receivers);
for receiver in pending_receivers {
let _ = receiver.recv();
}
Ok(())
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions>
fn dispatch_tick_actions(
&mut self,
tick_actions: &TickActions,
tick_duration: u64,
tick_start: &Instant,
) -> 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.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(_) |
ActionItem::Key(KeyActionItem::General(_)) |
ActionItem::Pointer(PointerActionItem::General(_)) |
ActionItem::Wheel(WheelActionItem::General(_)) => {
self.dispatch_pause_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_mut().unwrap().input_cancel_list.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::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_mut().unwrap().input_cancel_list.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,
tick_start,
)?;
},
ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up(
pointer_up_action,
))) => {
self.dispatch_pointerup_action(input_id, pointer_up_action);
},
ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(scroll_action))) => {
self.dispatch_scroll_action(
input_id,
scroll_action,
tick_duration,
tick_start,
)?;
},
ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Cancel)) => {
self.dispatch_pointercancel_action(input_id);
},
}
}
Ok(())
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pause-action>
/// This is a dummy action. The sole effect of the action is that
/// its `duration` parameter may decide "tick duration".
fn dispatch_pause_action(&mut self, _input_id: &str) {}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action>
fn dispatch_keydown_action(&mut self, input_id: &str, action: &KeyDownAction) {
let raw_key = action.value.chars().next().unwrap();
let key_input_state = match self.input_state_table_mut().get_mut(input_id).unwrap() {
InputSourceState::Key(key_input_state) => key_input_state,
_ => unreachable!(),
};
let keyboard_event = key_input_state.dispatch_keydown(raw_key);
// Step 12: Perform implementation-specific action dispatch steps on browsing
// context equivalent to pressing a key on the keyboard in accordance with the
// requirements of [UI-EVENTS], and producing the following events, as
// appropriate, with the specified properties. This will always produce events
// including at least a keyDown event.
self.send_blocking_input_event_to_embedder(InputEvent::Keyboard(KeyboardEvent::new(
keyboard_event,
)));
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action>
fn dispatch_keyup_action(&mut self, input_id: &str, action: &KeyUpAction) {
let session = self.session_mut().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 input_cancel_list = &mut session.input_cancel_list;
if let Some(pos) = input_cancel_list.iter().rposition(|(id, item)| {
id == input_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 key_input_state = match session.input_state_table.get_mut(input_id).unwrap() {
InputSourceState::Key(key_input_state) => key_input_state,
_ => unreachable!(),
};
// Step 12: Perform implementation-specific action dispatch steps on browsing
// context equivalent to releasing a key on the keyboard in accordance with the
// requirements of [UI-EVENTS], ...
let Some(keyboard_event) = key_input_state.dispatch_keyup(raw_key) else {
return;
};
self.send_blocking_input_event_to_embedder(InputEvent::Keyboard(KeyboardEvent::new(
keyboard_event,
)));
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointercancel-action>
fn dispatch_pointercancel_action(&mut self, source_id: &str) {
// Perform implementation-specific action dispatch steps on browsing context equivalent
// to cancelling the any action of the pointer with
// pointerId equal to source's pointerId item. having type pointerType,
// in accordance with the requirements of [UI-EVENTS] and [POINTER-EVENTS].
let PointerInputState {
subtype,
pointer_id,
x,
y,
..
} = *self.get_pointer_input_state(source_id);
match subtype {
PointerType::Pen | PointerType::Touch => {
self.send_blocking_input_event_to_embedder(InputEvent::Touch(TouchEvent::new(
TouchEventType::Cancel,
TouchId(pointer_id as i32),
WebViewPoint::Page(Point2D::new(x as f32, y as f32)),
)));
},
PointerType::Mouse => {
info!("WebDriver pointerCancel is not implemented for mouse yet");
},
}
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerdown-action>
fn dispatch_pointerdown_action(&mut self, input_id: &str, action: &PointerDownAction) {
let pointer_input_state = self.get_pointer_input_state_mut(input_id);
// Step 3. If the source's pressed property contains button return success with data null.
if pointer_input_state.pressed.contains(&action.button) {
return;
}
let PointerInputState { x, y, subtype, .. } = *pointer_input_state;
// 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 pointer type
let point = WebViewPoint::Page(Point2D::new(x as f32, y as f32));
let input_event = match subtype {
PointerType::Mouse => InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Down,
action.button.into(),
point,
)),
PointerType::Pen | PointerType::Touch => InputEvent::Touch(TouchEvent::new(
TouchEventType::Down,
TouchId(pointer_input_state.pointer_id as i32),
point,
)),
};
self.send_blocking_input_event_to_embedder(input_event);
// Step 17. Return success with data null.
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action>
fn dispatch_pointerup_action(&mut self, input_id: &str, action: &PointerUpAction) {
let pointer_input_state = self.get_pointer_input_state_mut(input_id);
// 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);
let PointerInputState {
x,
y,
subtype,
pointer_id,
..
} = *pointer_input_state;
// 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 input_cancel_list = &mut self.session_mut().unwrap().input_cancel_list;
if let Some(pos) = input_cancel_list.iter().position(|(id, item)| {
id == input_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
let point = WebViewPoint::Page(Point2D::new(x as f32, y as f32));
let input_event = match subtype {
PointerType::Mouse => InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Up,
action.button.into(),
point,
)),
PointerType::Pen | PointerType::Touch => InputEvent::Touch(TouchEvent::new(
TouchEventType::Up,
TouchId(pointer_id as i32),
point,
)),
};
self.send_blocking_input_event_to_embedder(input_event);
// Step 8. Return success with data null.
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action>
fn dispatch_pointermove_action(
&mut self,
input_id: &str,
action: &PointerMoveAction,
tick_duration: u64,
tick_start: &Instant,
) -> Result<(), ErrorStatus> {
// 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, input_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.
// Asynchronously wait means do not block browser to process event loop.
// In the context of Servo, it means block the webdriver server thread.
if duration > 0 {
thread::sleep(Duration::from_millis(MOVESCROLL_INTERVAL));
}
let (start_x, start_y) = {
let pointer_input_state = self.get_pointer_input_state(input_id);
(pointer_input_state.x, pointer_input_state.y)
};
// 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 pointer type
self.perform_pointer_move(input_id, duration, start_x, start_y, x, y, tick_start);
// Step 19. Return success with data null.
Ok(())
}
/// <https://w3c.github.io/webdriver/#dfn-perform-a-pointer-move>
#[expect(clippy::too_many_arguments)]
fn perform_pointer_move(
&mut self,
input_id: &str,
duration: u64,
start_x: f64,
start_y: f64,
target_x: f64,
target_y: f64,
tick_start: &Instant,
) {
// 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 - 6: Let current x/y equal the x/y property of input state.
let PointerInputState {
x: current_x,
y: current_y,
subtype,
pressed,
pointer_id,
} = self.get_pointer_input_state(input_id);
// Step 7. If x != current x or y != current y, run the following steps:
if x != *current_x || y != *current_y {
// Step 7.1. Let buttons be equal to input state's pressed property.
// Step 7.2. Perform implementation-specific action dispatch steps
let point = WebViewPoint::Page(Point2D::new(x as f32, y as f32));
// For a pointer of type "mouse"
// this will always produce events including at least a pointerMove event.
match subtype {
PointerType::Mouse => {
let input_event = InputEvent::MouseMove(MouseMoveEvent::new(point));
self.send_blocking_input_event_to_embedder(input_event);
},
// In the case where the pointerType is "pen" or "touch", and buttons is empty,
// this may be a no-op.
PointerType::Touch | PointerType::Pen => {
if pressed.contains(&ELEMENT_CLICK_BUTTON) {
let input_event = InputEvent::Touch(TouchEvent::new(
TouchEventType::Move,
TouchId(*pointer_id as i32),
point,
));
// FIXME: Should replace with `send_blocking_input_event_to_embedder`
// after we revamp the touch chain.
self.send_input_event_to_embedder(input_event);
}
},
}
// Step 7.3. Let input state's x property equal x and y property equal y.
let pointer_input_state = self.get_pointer_input_state_mut(input_id);
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 implementation defined amount of time to pass.
// Step 9.2. Perform a pointer move with arguments input state,
// duration, start x, start y, target x, target y.
// This is done in `fn process_pending_actions`.
// NOTE: The initial pointer movement is performed synchronously.
// This ensures determinism in the sequence of the first event
// triggered by each action in the tick.
// Subsequent movements (if any) are performed asynchronously.
// This allows events from two pointerMove actions in the tick to be interspersed.
// We use [`PendingPointerMove`] to achieve the same effect as asynchronous wait and
// parallelism required by spec.
// This conveniently unify the wait interval between ticks.
self.pending_actions
.push(PendingActions::PointerMove(PendingPointerMove {
input_id: input_id.to_owned(),
duration,
start_x,
start_y,
target_x,
target_y,
}));
}
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-scroll-action>
fn dispatch_scroll_action(
&mut self,
input_id: &str,
action: &WheelScrollAction,
tick_duration: u64,
tick_start: &Instant,
) -> Result<(), ErrorStatus> {
// <https://w3c.github.io/webdriver/#dfn-process-a-wheel-action>
// The validation is normally done already by webdriver crate,
// but it is not the case for this action currently.
// 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 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 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 _, input_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 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(MOVESCROLL_INTERVAL));
}
// Step 11. Perform a scroll with arguments global key state, duration, x, y, delta x, delta y, 0, 0.
self.perform_scroll(
input_id,
duration,
x,
y,
delta_x as _,
delta_y as _,
0.0,
0.0,
tick_start,
);
// Step 12. Return success with data null.
Ok(())
}
/// <https://w3c.github.io/webdriver/#dfn-perform-a-scroll>
#[expect(clippy::too_many_arguments)]
fn perform_scroll(
&mut self,
input_id: &str,
duration: u64,
x: f64,
y: f64,
target_delta_x: f64,
target_delta_y: f64,
mut current_delta_x: f64,
mut current_delta_y: f64,
tick_start: &Instant,
) {
// 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 - current_delta_x,
target_delta_y - current_delta_y,
)
} else {
(
duration_ratio * target_delta_x - current_delta_x,
duration_ratio * target_delta_y - current_delta_y,
)
};
// Step 5. If delta x != 0 or delta y != 0, run the following steps:
if delta_x != 0.0 || delta_y != 0.0 {
// Step 5.1. Perform implementation-specific action dispatch steps
let delta = WheelDelta {
x: -delta_x,
y: -delta_y,
z: 0.0,
mode: WheelMode::DeltaPixel,
};
let point = WebViewPoint::Page(Point2D::new(x as f32, y as f32));
let input_event = InputEvent::Wheel(WheelEvent::new(delta, point));
self.send_blocking_input_event_to_embedder(input_event);
// 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.
current_delta_x += delta_x;
current_delta_y += delta_y;
}
// Step 6. If last is true, return.
if last {
return;
}
// Step 7. Run the following substeps in parallel:
// Step 7.1. Asynchronously wait for an implementation defined amount of time to pass.
// Step 7.2. Perform a scroll with arguments duration, x, y,
// target delta x, target delta y, current delta x, current delta y.
// This is done in `fn process_pending_actions`.
// NOTE: The initial scroll is performed synchronously.
// This ensures determinism in the sequence of the first event
// triggered by each action in the tick.
// Subsequent scrolls (if any) are performed asynchronously.
// This allows events from two scroll actions in the tick to be interspersed.
// We use [`PendingScroll`] to achieve the same effect as asynchronous wait and
// parallelism required by spec.
// This conveniently unify the wait interval between ticks.
self.pending_actions
.push(PendingActions::Scroll(PendingScroll {
input_id: input_id.to_owned(),
duration,
x,
y,
current_delta_x,
current_delta_y,
target_delta_x,
target_delta_y,
}));
}
/// 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) = generic_channel::oneshot().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_oneshot_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(())
}
}
/// <https://w3c.github.io/webdriver/#dfn-get-coordinates-relative-to-an-origin>
pub(crate) fn get_origin_relative_coordinates(
&self,
origin: &PointerOrigin,
x_offset: f64,
y_offset: f64,
input_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) = {
let pointer_input_state = self.get_pointer_input_state(input_id);
(pointer_input_state.x, pointer_input_state.y)
};
// 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))
},
}
}
/// <https://w3c.github.io/webdriver/#dfn-center-point>
fn get_element_in_view_center_point(
&self,
web_element: &WebElement,
) -> Result<(i64, i64), ErrorStatus> {
let (sender, receiver) = generic_channel::oneshot().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_oneshot_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),
}
}
/// <https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence>
pub(crate) fn extract_an_action_sequence(
&mut self,
actions: Vec<ActionSequence>,
) -> 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 {
let id = action_sequence.id.clone();
// Step 4.1. Let "source actions" be the result of trying to process an input source action sequence
// given "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
if actions_by_tick.len() < source_actions.len() {
actions_by_tick.resize_with(source_actions.len(), Vec::new);
}
// Step 4.2.3. Append "action" to the List at index i in "actions by tick",
// for each "action" in "source actions".
for (tick_index, action_item) in source_actions.into_iter().enumerate() {
actions_by_tick[tick_index].push((id.clone(), action_item));
}
}
actions_by_tick
}
/// <https://w3c.github.io/webdriver/#dfn-process-an-input-source-action-sequence>
fn process_an_input_source_action_sequence(
&mut self,
action_sequence: ActionSequence,
) -> Vec<ActionItem> {
// Step 2. Let id be the value of the id property of action sequence.
let id = action_sequence.id;
match action_sequence.actions {
ActionsType::Null {
actions: null_actions,
} => {
self.input_state_table_mut()
.entry(id)
.or_insert(InputSourceState::Null);
null_actions.into_iter().map(ActionItem::Null).collect()
},
ActionsType::Key {
actions: key_actions,
} => {
self.input_state_table_mut()
.entry(id)
.or_insert(InputSourceState::Key(KeyInputState::new()));
key_actions.into_iter().map(ActionItem::Key).collect()
},
ActionsType::Pointer {
parameters,
actions: pointer_actions,
} => {
let pointer_ids = self.session().unwrap().pointer_ids();
// Get or create a pointer input source with subtype, and other iterms
// set to default values.
self.input_state_table_mut()
.entry(id)
.or_insert(InputSourceState::Pointer(PointerInputState::new(
parameters.pointer_type,
pointer_ids,
0.0,
0.0,
)));
pointer_actions
.into_iter()
.map(ActionItem::Pointer)
.collect()
},
ActionsType::Wheel {
actions: wheel_actions,
} => {
self.input_state_table_mut()
.entry(id)
.or_insert(InputSourceState::Wheel);
wheel_actions.into_iter().map(ActionItem::Wheel).collect()
},
}
}
fn input_state_table_mut(&mut self) -> &mut HashMap<String, InputSourceState> {
&mut self.session_mut().unwrap().input_state_table
}
fn get_pointer_input_state_mut(&mut self, input_id: &str) -> &mut PointerInputState {
let InputSourceState::Pointer(pointer_input_state) =
self.input_state_table_mut().get_mut(input_id).unwrap()
else {
unreachable!();
};
pointer_input_state
}
fn get_pointer_input_state(&self, input_id: &str) -> &PointerInputState {
let InputSourceState::Pointer(pointer_input_state) = self
.session()
.unwrap()
.input_state_table
.get(input_id)
.unwrap()
else {
unreachable!();
};
pointer_input_state
}
}