webdriver: Improve documentations for actions.rs and update TODOs (#39391)

Add specification quote for each steps, remove some irrelevant TODO, and
move function position closer to related function.

Testing: No behaviour change

---------

Signed-off-by: PotatoCP <kenzieradityatirtarahardja18@gmail.com>
Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
Co-authored-by: Euclid Ye <yezhizhenjiakang@gmail.com>
This commit is contained in:
Kenzie Raditya Tirtarahardja 2025-09-21 13:42:16 +08:00 committed by GitHub
parent 2c8533f38e
commit 77ae3a0eb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 166 additions and 134 deletions

View file

@ -368,11 +368,16 @@ impl Handler {
_ => unreachable!(), _ => unreachable!(),
}; };
// Step 3. If the source's pressed property contains button return success with data null.
if pointer_input_state.pressed.contains(&action.button) { if pointer_input_state.pressed.contains(&action.button) {
return; return;
} }
pointer_input_state.pressed.insert(action.button);
// 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(); self.increment_num_pending_actions();
let msg_id = self.current_action_id.get(); let msg_id = self.current_action_id.get();
let cmd_msg = WebDriverCommandMsg::MouseButtonAction( let cmd_msg = WebDriverCommandMsg::MouseButtonAction(
@ -384,6 +389,8 @@ impl Handler {
msg_id, msg_id,
); );
let _ = self.send_message_to_embedder(cmd_msg); let _ = self.send_message_to_embedder(cmd_msg);
// Step 17. Return success with data null.
} }
/// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action> /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action>
@ -396,9 +403,12 @@ impl Handler {
_ => unreachable!(), _ => 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) { if !pointer_input_state.pressed.contains(&action.button) {
return; return;
} }
// Step 6. Remove button from the set corresponding to source's pressed property,
pointer_input_state.pressed.remove(&action.button); pointer_input_state.pressed.remove(&action.button);
// Remove matching pointerUp(must be unique) from `[input_cancel_list]` due to bugs in spec // Remove matching pointerUp(must be unique) from `[input_cancel_list]` due to bugs in spec
@ -419,6 +429,7 @@ impl Handler {
} }
} }
// Step 7. Perform implementation-specific action dispatch steps
self.increment_num_pending_actions(); self.increment_num_pending_actions();
let msg_id = self.current_action_id.get(); let msg_id = self.current_action_id.get();
let cmd_msg = WebDriverCommandMsg::MouseButtonAction( let cmd_msg = WebDriverCommandMsg::MouseButtonAction(
@ -430,45 +441,8 @@ impl Handler {
msg_id, msg_id,
); );
let _ = self.send_message_to_embedder(cmd_msg); let _ = self.send_message_to_embedder(cmd_msg);
}
/// <https://w3c.github.io/webdriver/#dfn-get-coordinates-relative-to-an-origin> // Step 8. Return success with data null.
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 - 2: Check "no such element", covered in script thread handler.
// Step 3. Let x element and y element be the result of calculating the in-view center point of element.
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-dispatch-a-pointermove-action> /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action>
@ -482,6 +456,7 @@ impl Handler {
// Step 1. Let x offset be equal to the x property of action object. // Step 1. Let x offset be equal to the x property of action object.
let x_offset = action.x; let x_offset = action.x;
// Step 2. Let y offset be equal to the y property of action object. // Step 2. Let y offset be equal to the y property of action object.
let y_offset = action.y; let y_offset = action.y;
@ -493,7 +468,10 @@ impl Handler {
let (x, y) = self.get_origin_relative_coordinates(origin, x_offset, y_offset, source_id)?; let (x, y) = self.get_origin_relative_coordinates(origin, x_offset, y_offset, source_id)?;
// Step 5 - 6 // 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)?; self.check_viewport_bound(x, y)?;
// Step 7. Let duration be equal to action object's duration property // Step 7. Let duration be equal to action object's duration property
@ -523,9 +501,12 @@ impl Handler {
}; };
// Step 9 - 18 // 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); self.perform_pointer_move(source_id, duration, start_x, start_y, x, y, tick_start);
// Step 19 // Step 19. Return success with data null.
Ok(()) Ok(())
} }
@ -549,20 +530,27 @@ impl Handler {
}; };
loop { loop {
// Step 1 // 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(); let time_delta = tick_start.elapsed().as_millis();
// Step 2 // 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 { let duration_ratio = if duration > 0 {
time_delta as f64 / duration as f64 time_delta as f64 / duration as f64
} else { } else {
1.0 1.0
}; };
// Step 3 // 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; let last = 1.0 - duration_ratio < 0.001;
// Step 4 // 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 { let (x, y) = if last {
(target_x, target_y) (target_x, target_y)
} else { } else {
@ -572,15 +560,18 @@ impl Handler {
) )
}; };
// Steps 5 - 6 // Step 5. Let current x equal the x property of input state.
let current_x = pointer_input_state.x; 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; let current_y = pointer_input_state.y;
// Step 7 // Step 7. If x != current x or y != current y, run the following steps:
// Actually "last" should not be checked here based on spec. // FIXME: Actually "last" should not be checked here based on spec.
// However, we need to send the webdriver id at the final perform. // However, we need to send the webdriver id at the final perform.
if x != current_x || y != current_y || last { if x != current_x || y != current_y || last {
// Step 7.2 // 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 { let msg_id = if last {
self.increment_num_pending_actions(); self.increment_num_pending_actions();
self.current_action_id.get() self.current_action_id.get()
@ -594,18 +585,23 @@ impl Handler {
msg_id, msg_id,
); );
let _ = self.send_message_to_embedder(cmd_msg); let _ = self.send_message_to_embedder(cmd_msg);
// Step 7.3 // Step 7.3. Let input state's x property equal x and y property equal y.
pointer_input_state.x = x; pointer_input_state.x = x;
pointer_input_state.y = y; pointer_input_state.y = y;
} }
// Step 8 // Step 8. If last is true, return.
if last { if last {
return; return;
} }
// Step 9 // 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)); 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.
} }
} }
@ -616,10 +612,7 @@ impl Handler {
action: &WheelScrollAction, action: &WheelScrollAction,
tick_duration: u64, tick_duration: u64,
) -> Result<(), ErrorStatus> { ) -> Result<(), ErrorStatus> {
// Note: We have not implemented `extract an action sequence` which will calls // TODO: We should verify each variable when processing a wheel action.
// `process a wheel action` that validate many of the variable used here.
// Hence, we do all the checking here until those functions is properly
// implemented.
// <https://w3c.github.io/webdriver/#dfn-process-a-wheel-action> // <https://w3c.github.io/webdriver/#dfn-process-a-wheel-action>
let tick_start = Instant::now(); let tick_start = Instant::now();
@ -649,7 +642,10 @@ impl Handler {
let (x, y) = let (x, y) =
self.get_origin_relative_coordinates(origin, x_offset as _, y_offset as _, source_id)?; self.get_origin_relative_coordinates(origin, x_offset as _, y_offset as _, source_id)?;
// Step 5 - 6 // 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)?; self.check_viewport_bound(x, y)?;
// Step 7. Let delta x be equal to the deltaX property of action object. // Step 7. Let delta x be equal to the deltaX property of action object.
@ -687,7 +683,7 @@ impl Handler {
tick_start, tick_start,
); );
// Step 12 // Step 12. Return success with data null.
Ok(()) Ok(())
} }
@ -704,6 +700,7 @@ impl Handler {
mut curr_delta_y: f64, mut curr_delta_y: f64,
tick_start: Instant, tick_start: Instant,
) { ) {
loop {
// Step 1. Let time delta be the time since the beginning of the current tick, // Step 1. Let time delta be the time since the beginning of the current tick,
// measured in milliseconds on a monotonic clock. // measured in milliseconds on a monotonic clock.
let time_delta = tick_start.elapsed().as_millis(); let time_delta = tick_start.elapsed().as_millis();
@ -721,7 +718,11 @@ impl Handler {
// let last be true. Otherwise let last be false. // let last be true. Otherwise let last be false.
let last = 1.0 - duration_ratio < 0.001; let last = 1.0 - duration_ratio < 0.001;
// Step 4 // 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 { let (delta_x, delta_y) = if last {
(target_delta_x - curr_delta_x, target_delta_y - curr_delta_y) (target_delta_x - curr_delta_x, target_delta_y - curr_delta_y)
} else { } else {
@ -731,11 +732,11 @@ impl Handler {
) )
}; };
// Step 5 // Step 5. If delta x != 0 or delta y != 0, run the following steps:
// Actually "last" should not be checked here based on spec. // Actually "last" should not be checked here based on spec.
// However, we need to send the webdriver id at the final perform. // However, we need to send the webdriver id at the final perform.
if delta_x != 0.0 || delta_y != 0.0 || last { if delta_x != 0.0 || delta_y != 0.0 || last {
// Perform implementation-specific action dispatch steps // Step 5.1. Perform implementation-specific action dispatch steps
let msg_id = if last { let msg_id = if last {
self.increment_num_pending_actions(); self.increment_num_pending_actions();
self.current_action_id.get() self.current_action_id.get()
@ -752,11 +753,13 @@ impl Handler {
); );
let _ = self.send_message_to_embedder(cmd_msg); 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_x += delta_x;
curr_delta_y += delta_y; curr_delta_y += delta_y;
} }
// Step 6 // Step 6. If last is true, return.
if last { if last {
return; return;
} }
@ -767,16 +770,8 @@ impl Handler {
thread::sleep(Duration::from_millis(WHEELSCROLL_INTERVAL)); thread::sleep(Duration::from_millis(WHEELSCROLL_INTERVAL));
// 7.2. Perform a scroll with arguments duration, x, y, target delta x, // 7.2. Perform a scroll with arguments duration, x, y, target delta x,
// target delta y, current delta x, current delta y. // target delta y, current delta x, current delta y.
self.perform_scroll( // Notice that this simply repeat what we have done until last is true.
duration, }
x,
y,
target_delta_x,
target_delta_y,
curr_delta_x,
curr_delta_y,
tick_start,
);
} }
/// Verify that the given coordinates are within the boundary of the viewport. /// Verify that the given coordinates are within the boundary of the viewport.
@ -802,21 +797,64 @@ impl Handler {
} }
} }
/// <https://w3c.github.io/webdriver/#dfn-get-coordinates-relative-to-an-origin>
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))
},
}
}
/// <https://w3c.github.io/webdriver/#dfn-center-point> /// <https://w3c.github.io/webdriver/#dfn-center-point>
fn get_element_in_view_center_point( fn get_element_in_view_center_point(
&self, &self,
web_element: &WebElement, web_element: &WebElement,
) -> Result<(i64, i64), ErrorStatus> { ) -> Result<(i64, i64), ErrorStatus> {
let (sender, receiver) = ipc::channel().unwrap(); 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( self.browsing_context_script_command(
WebDriverScriptCommand::GetElementInViewCenterPoint(web_element.to_string(), sender), WebDriverScriptCommand::GetElementInViewCenterPoint(web_element.to_string(), sender),
VerifyBrowsingContextIsOpen::No, VerifyBrowsingContextIsOpen::No,
) )
.unwrap(); .unwrap();
// Step 2. If element is null, return error with error code no such element.
let response = match wait_for_ipc_response(receiver) { let response = match wait_for_ipc_response(receiver) {
Ok(response) => response, Ok(response) => response,
Err(WebDriverError { error, .. }) => return Err(error), 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? { match response? {
Some(point) => Ok(point), Some(point) => Ok(point),
None => Err(ErrorStatus::UnknownError), None => Err(ErrorStatus::UnknownError),
@ -826,7 +864,7 @@ impl Handler {
/// <https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence> /// <https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence>
pub(crate) fn extract_an_action_sequence(&self, params: ActionsParameters) -> ActionsByTick { 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 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, // Step 2. (ignored because params is already validated earlier). If actions is not a list,
// return an error with status InvalidArgument. // return an error with status InvalidArgument.
let actions = params.actions; let actions = params.actions;
@ -852,7 +890,7 @@ impl Handler {
actions_by_tick.push(Vec::new()); actions_by_tick.push(Vec::new());
} }
// Step 4.2.3. // 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() { for (tick_index, action_item) in source_actions.into_iter().enumerate() {
actions_by_tick[tick_index].push((id.clone(), action_item)); actions_by_tick[tick_index].push((id.clone(), action_item));
} }

View file

@ -2134,17 +2134,12 @@ impl Handler {
); );
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
// TODO: distinguish the not found and not focusable cases
// File input and non-typeable form control should have // File input and non-typeable form control should have
// been handled in `webdriver_handler.rs`. // been handled in `webdriver_handler.rs`.
if !wait_for_ipc_response(receiver)?.map_err(|error| WebDriverError::new(error, ""))? { if !wait_for_ipc_response(receiver)?.map_err(|error| WebDriverError::new(error, ""))? {
return Ok(WebDriverResponse::Void); return Ok(WebDriverResponse::Void);
} }
// TODO: there's a race condition caused by the focus command and the
// send keys command being two separate messages,
// so the constellation may have changed state between them.
// Step 10. Let input id be a the result of generating a UUID. // Step 10. Let input id be a the result of generating a UUID.
let id = Uuid::new_v4().to_string(); let id = Uuid::new_v4().to_string();

View file

@ -502,7 +502,6 @@ impl App {
} }
}, },
WebDriverCommandMsg::KeyboardAction(webview_id, key_event, msg_id) => { WebDriverCommandMsg::KeyboardAction(webview_id, key_event, msg_id) => {
// TODO: We should do processing like in `headed_window:handle_keyboard_input`.
if let Some(webview) = running_state.webview_by_id(webview_id) { if let Some(webview) = running_state.webview_by_id(webview_id) {
webview.notify_input_event( webview.notify_input_event(
InputEvent::Keyboard(KeyboardEvent::new(key_event)) InputEvent::Keyboard(KeyboardEvent::new(key_event))