Synchronize dispatch_actions in WebDriver (#36932)

Implement missing synchronization in `dispatch_actions` of `WebDriver`.
https://w3c.github.io/webdriver/#dispatching-actions

> The user agent event loop has spun enough times to process the DOM
events generated by the last invocation of the >[dispatch tick
actions](https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions)
steps.

- Add a way for `ScriptThread` to notify `WebDriver` about the
completion of input commands.
- Add a `webdriver_id` field for `InputEvent`. `ScriptThread` uses it to
distinguish WebDriver events and sends notification.

Tests:
`./mach test-wpt --product servodriver -r
tests\wpt\tests\webdriver\tests\classic\element_click\events.py` pass if
`hit_testing` pass. Check
[issue](https://github.com/servo/servo/issues/36676#issuecomment-2882917136)

cc: @xiaochengh

---------

Signed-off-by: batu_hoang <longvatrong111@gmail.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
batu_hoang 2025-05-21 19:03:04 +08:00 committed by GitHub
parent 3a527d784b
commit f52fa9b672
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 471 additions and 140 deletions

View file

@ -10,11 +10,12 @@ mod actions;
mod capabilities;
use std::borrow::ToOwned;
use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, HashMap};
use std::io::Cursor;
use std::net::{SocketAddr, SocketAddrV4};
use std::time::Duration;
use std::{env, fmt, mem, process, thread};
use std::{env, fmt, process, thread};
use base::id::{BrowsingContextId, WebViewId};
use base64::Engine;
@ -23,8 +24,9 @@ use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection};
use cookie::{CookieBuilder, Expiration};
use crossbeam_channel::{Receiver, Sender, after, select, unbounded};
use embedder_traits::{
MouseButton, WebDriverCommandMsg, WebDriverCookieError, WebDriverFrameId, WebDriverJSError,
WebDriverJSResult, WebDriverJSValue, WebDriverLoadStatus, WebDriverScriptCommand,
MouseButton, WebDriverCommandMsg, WebDriverCommandResponse, WebDriverCookieError,
WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverJSValue, WebDriverLoadStatus,
WebDriverMessageId, WebDriverScriptCommand,
};
use euclid::{Rect, Size2D};
use http::method::Method;
@ -43,8 +45,8 @@ use servo_url::ServoUrl;
use style_traits::CSSPixel;
use uuid::Uuid;
use webdriver::actions::{
ActionSequence, PointerDownAction, PointerMoveAction, PointerOrigin, PointerType,
PointerUpAction,
ActionSequence, ActionsType, PointerAction, PointerActionItem, PointerActionParameters,
PointerDownAction, PointerMoveAction, PointerOrigin, PointerType, PointerUpAction,
};
use webdriver::capabilities::CapabilitiesMatching;
use webdriver::command::{
@ -64,6 +66,26 @@ use webdriver::server::{self, Session, SessionTeardownKind, WebDriverHandler};
use crate::actions::{InputSourceState, PointerInputState};
#[derive(Default)]
pub struct WebDriverMessageIdGenerator {
counter: Cell<usize>,
}
impl WebDriverMessageIdGenerator {
pub fn new() -> Self {
Self {
counter: Cell::new(0),
}
}
/// Returns a unique ID.
pub fn next(&self) -> WebDriverMessageId {
let id = self.counter.get();
self.counter.set(id + 1);
WebDriverMessageId(id)
}
}
fn extension_routes() -> Vec<(Method, &'static str, ServoExtensionRoute)> {
vec![
(
@ -145,10 +167,11 @@ pub struct WebDriverSession {
unhandled_prompt_behavior: String,
// https://w3c.github.io/webdriver/#dfn-input-state-table
input_state_table: HashMap<String, InputSourceState>,
// https://w3c.github.io/webdriver/#dfn-input-cancel-list
input_cancel_list: Vec<ActionSequence>,
/// <https://w3c.github.io/webdriver/#dfn-input-state-map>
input_state_table: RefCell<HashMap<String, InputSourceState>>,
/// <https://w3c.github.io/webdriver/#dfn-input-cancel-list>
input_cancel_list: RefCell<Vec<ActionSequence>>,
}
impl WebDriverSession {
@ -172,8 +195,8 @@ impl WebDriverSession {
strict_file_interactability: false,
unhandled_prompt_behavior: "dismiss and notify".to_string(),
input_state_table: HashMap::new(),
input_cancel_list: Vec::new(),
input_state_table: RefCell::new(HashMap::new()),
input_cancel_list: RefCell::new(Vec::new()),
}
}
}
@ -187,8 +210,22 @@ struct Handler {
/// for it to send us a load-status. Messages sent on it
/// will be forwarded to the load_status_receiver.
load_status_sender: IpcSender<WebDriverLoadStatus>,
session: Option<WebDriverSession>,
/// The channel for sending Webdriver messages to the constellation.
constellation_chan: Sender<EmbedderToConstellationMessage>,
/// The IPC sender which we can clone and pass along to the constellation
constellation_sender: IpcSender<WebDriverCommandResponse>,
/// Receiver notification from the constellation when a command is completed
constellation_receiver: IpcReceiver<WebDriverCommandResponse>,
id_generator: WebDriverMessageIdGenerator,
current_action_id: Cell<Option<WebDriverMessageId>>,
resize_timeout: u32,
}
@ -409,11 +446,18 @@ impl Handler {
let (load_status_sender, receiver) = ipc::channel().unwrap();
let (sender, load_status_receiver) = unbounded();
ROUTER.route_ipc_receiver_to_crossbeam_sender(receiver, sender);
let (constellation_sender, constellation_receiver) = ipc::channel().unwrap();
Handler {
load_status_sender,
load_status_receiver,
session: None,
constellation_chan,
constellation_sender,
constellation_receiver,
id_generator: WebDriverMessageIdGenerator::new(),
current_action_id: Cell::new(None),
resize_timeout: 500,
}
}
@ -1445,18 +1489,13 @@ impl Handler {
}
fn handle_release_actions(&mut self) -> WebDriverResult<WebDriverResponse> {
let input_cancel_list = {
let session = self.session_mut()?;
session.input_cancel_list.reverse();
mem::take(&mut session.input_cancel_list)
};
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, ""));
}
let session = self.session_mut()?;
session.input_state_table = HashMap::new();
let session = self.session()?;
session.input_state_table.borrow_mut().clear();
Ok(WebDriverResponse::Void)
}
@ -1614,7 +1653,7 @@ impl Handler {
let id = Uuid::new_v4().to_string();
// Step 8.1
self.session_mut()?.input_state_table.insert(
self.session_mut()?.input_state_table.borrow_mut().insert(
id.clone(),
InputSourceState::Pointer(PointerInputState::new(&PointerType::Mouse)),
);
@ -1645,19 +1684,31 @@ impl Handler {
..Default::default()
};
// Step 8.16 Dispatch a list of actions with input state,
// actions, session's current browsing context, and actions options.
if let Err(error) =
self.dispatch_pointermove_action(&id, &pointer_move_action, 0)
{
return Err(WebDriverError::new(error, ""));
}
let action_sequence = ActionSequence {
id: id.clone(),
actions: ActionsType::Pointer {
parameters: PointerActionParameters {
pointer_type: PointerType::Mouse,
},
actions: vec![
PointerActionItem::Pointer(PointerAction::Move(
pointer_move_action,
)),
PointerActionItem::Pointer(PointerAction::Down(
pointer_down_action,
)),
PointerActionItem::Pointer(PointerAction::Up(pointer_up_action)),
],
},
};
self.dispatch_pointerdown_action(&id, &pointer_down_action);
self.dispatch_pointerup_action(&id, &pointer_up_action);
let _ = self.dispatch_actions(&[action_sequence]);
// Step 8.17 Remove an input source with input state and input id.
self.session_mut()?.input_state_table.remove(&id);
self.session_mut()?
.input_state_table
.borrow_mut()
.remove(&id);
// Step 13
Ok(WebDriverResponse::Void)