webdriver: Evaluate script commands via the WebView API in servoshell (#37663)

Let `WebDriverCommandMsg::ScriptCommand` goes through embedder first.
Give `embedder` the ability to release `webdriver` from waiting for a
response of `ExecuteScript`.

Tests: https://github.com/longvatrong111/servo/actions/runs/16071375821
No regression compared to CI run on main branch.

Fixes: https://github.com/servo/servo/issues/37370

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-07-09 22:05:39 +08:00 committed by GitHub
parent 562d9e4a21
commit 4499fdeb2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 75 additions and 150 deletions

View file

@ -726,10 +726,10 @@ impl Handler {
if let VerifyBrowsingContextIsOpen::Yes = verify {
self.verify_browsing_context_is_open(browsing_context_id)?;
}
let msg = EmbedderToConstellationMessage::WebDriverCommand(
WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd_msg),
);
self.constellation_chan.send(msg).unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
cmd_msg,
))?;
Ok(())
}
@ -746,10 +746,10 @@ impl Handler {
self.verify_top_level_browsing_context_is_open(webview_id)?;
}
let browsing_context_id = BrowsingContextId::from(webview_id);
let msg = EmbedderToConstellationMessage::WebDriverCommand(
WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd_msg),
);
self.constellation_chan.send(msg).unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
cmd_msg,
))?;
Ok(())
}

View file

@ -27,7 +27,7 @@ use servo::webrender_api::ScrollLocation;
use servo::webrender_api::units::DeviceIntSize;
use servo::{
EventLoopWaker, InputEvent, KeyboardEvent, MouseButtonEvent, MouseMoveEvent,
WebDriverCommandMsg, WheelDelta, WheelEvent, WheelMode,
WebDriverCommandMsg, WebDriverScriptCommand, WheelDelta, WheelEvent, WheelMode,
};
use url::Url;
use winit::application::ApplicationHandler;
@ -542,7 +542,16 @@ impl App {
webview.notify_scroll_event(scroll_location, point.to_i32());
}
},
WebDriverCommandMsg::ScriptCommand(..) |
WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
webdriver_script_command,
) => {
self.handle_webdriver_script_commnd(&webdriver_script_command, running_state);
running_state.forward_webdriver_command(WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
webdriver_script_command,
));
},
WebDriverCommandMsg::TakeScreenshot(..) => {
warn!(
"WebDriverCommand {:?} is still not moved from constellation to embedder",
@ -552,6 +561,24 @@ impl App {
};
}
}
fn handle_webdriver_script_commnd(
&self,
msg: &WebDriverScriptCommand,
running_state: &RunningAppState,
) {
match msg {
WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) => {
// Give embedder a chance to interrupt the script command.
// Webdriver only handles 1 script command at a time, so we can
// safely set a new interrupt sender and remove the previous one here.
running_state.set_script_command_interrupt_sender(Some(response_sender.clone()));
},
_ => {
running_state.set_script_command_interrupt_sender(None);
},
}
}
}
impl ApplicationHandler<AppEvent> for App {

View file

@ -19,7 +19,8 @@ use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
use servo::{
AllowOrDenyRequest, AuthenticationRequest, FilterPattern, FormControl, GamepadHapticEffectType,
KeyboardEvent, LoadStatus, PermissionRequest, Servo, ServoDelegate, ServoError, SimpleDialog,
WebDriverCommandMsg, WebDriverLoadStatus, WebView, WebViewBuilder, WebViewDelegate,
WebDriverCommandMsg, WebDriverJSResult, WebDriverJSValue, WebDriverLoadStatus, WebView,
WebViewBuilder, WebViewDelegate,
};
use url::Url;
@ -42,6 +43,7 @@ pub(crate) enum AppState {
#[derive(Clone, Default)]
struct WebDriverSenders {
pub load_status_senders: HashMap<WebViewId, IpcSender<WebDriverLoadStatus>>,
pub script_evaluation_interrupt_sender: Option<IpcSender<WebDriverJSResult>>,
}
pub(crate) struct RunningAppState {
@ -406,6 +408,36 @@ impl RunningAppState {
.load_status_senders
.insert(webview_id, sender);
}
pub(crate) fn set_script_command_interrupt_sender(
&self,
sender: Option<IpcSender<WebDriverJSResult>>,
) {
self.webdriver_senders
.borrow_mut()
.script_evaluation_interrupt_sender = sender;
}
/// Interrupt any ongoing WebDriver-based script evaluation.
///
/// From <https://w3c.github.io/webdriver/#dfn-execute-a-function-body>:
/// > The rules to execute a function body are as follows. The algorithm returns
/// > an ECMAScript completion record.
/// >
/// > If at any point during the algorithm a user prompt appears, immediately return
/// > Completion { Type: normal, Value: null, Target: empty }, but continue to run the
/// > other steps of this algorithm in parallel.
fn interrupt_webdriver_script_evaluation(&self) {
if let Some(sender) = &self
.webdriver_senders
.borrow()
.script_evaluation_interrupt_sender
{
sender.send(Ok(WebDriverJSValue::Null)).unwrap_or_else(|err| {
info!("Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}");
});
}
}
}
struct ServoShellServoDelegate;
@ -452,6 +484,8 @@ impl WebViewDelegate for RunningAppState {
}
fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) {
self.interrupt_webdriver_script_evaluation();
if self.servoshell_preferences.headless &&
self.servoshell_preferences.webdriver_port.is_none()
{

View file

@ -4,12 +4,3 @@
[test_form_control_send_text[textarea\]]
expected: FAIL
[test_file_upload]
expected: FAIL
[test_not_blurred[input\]]
expected: FAIL
[test_not_blurred[textarea\]]
expected: FAIL

View file

@ -22,33 +22,3 @@
[test_strict_display_none]
expected: ERROR
[test_multiple_files]
expected: FAIL
[test_multiple_files_without_multiple_attribute]
expected: FAIL
[test_single_file]
expected: FAIL
[test_single_file_replaces_without_multiple_attribute]
expected: FAIL
[test_transparent]
expected: FAIL
[test_obscured]
expected: FAIL
[test_outside_viewport]
expected: FAIL
[test_hidden]
expected: FAIL
[test_display_none]
expected: FAIL
[test_not_focused]
expected: FAIL

View file

@ -4,18 +4,3 @@
[test_textarea_append]
expected: FAIL
[test_input]
expected: FAIL
[test_textarea]
expected: FAIL
[test_input_insert_when_focused]
expected: FAIL
[test_textarea_insert_when_focused]
expected: FAIL
[test_date]
expected: FAIL

View file

@ -22,12 +22,3 @@
[test_readonly_element]
expected: FAIL
[test_body_is_interactable]
expected: FAIL
[test_transparent_element]
expected: FAIL
[test_obscured_element]
expected: FAIL

View file

@ -22,6 +22,3 @@
[test_element_just_outside_viewport[Just below viewport\]]
expected: FAIL
[test_element_outside_of_not_scrollable_viewport]
expected: FAIL

View file

@ -1,33 +1,3 @@
[send_keys.py]
[test_null_response_value]
expected: FAIL
[test_no_top_browsing_context]
expected: FAIL
[test_no_such_element_with_invalid_value]
expected: FAIL
[test_no_such_element_with_shadow_root]
expected: FAIL
[test_no_such_element_from_other_window_handle[open\]]
expected: FAIL
[test_no_such_element_from_other_window_handle[closed\]]
expected: FAIL
[test_no_such_element_from_other_frame[open\]]
expected: FAIL
[test_no_such_element_from_other_frame[closed\]]
expected: FAIL
[test_stale_element_reference[top_context\]]
expected: FAIL
[test_stale_element_reference[child_context\]]
expected: FAIL
[test_surrogates]
[test_no_browsing_context]
expected: FAIL

View file

@ -1,24 +1,6 @@
[key_events.py]
[test_modifier_key_sends_correct_events[\\ue008-SHIFT\]]
expected: FAIL
[test_non_printable_key_sends_events[\\ue00c-ESCAPE\]]
expected: FAIL
[test_special_key_sends_keydown[EQUALS-expected12\]]
expected: FAIL
[test_special_key_sends_keydown[PAUSE-expected45\]]
expected: FAIL
[test_modifier_key_sends_correct_events[\\ue00a-ALT\]]
expected: FAIL
[test_modifier_key_sends_correct_events[\\ue009-CONTROL\]]
expected: FAIL
[test_modifier_key_sends_correct_events[\\ue03d-META\]]
expected: FAIL
[test_modifier_key_sends_correct_events[\\ue052-R_ALT\]]
expected: FAIL

View file

@ -5,18 +5,6 @@
[test_pointer_down_closes_browsing_context]
expected: FAIL
[test_click_at_coordinates]
expected: FAIL
[test_context_menu_at_coordinates]
expected: FAIL
[test_middle_click]
expected: FAIL
[test_click_element_center]
expected: FAIL
[test_click_element_in_shadow_tree[outer-open\]]
expected: FAIL

View file

@ -8,15 +8,6 @@
[test_scroll_with_key_pressed]
expected: FAIL
[test_scroll_not_scrollable]
expected: FAIL
[test_scroll_scrollable_overflow]
expected: FAIL
[test_scroll_iframe]
expected: FAIL
[test_scroll_shadow_tree[outer-open\]]
expected: FAIL

View file

@ -1,4 +1,3 @@
[alerts.py]
expected: TIMEOUT
[test_retain_tab_modal_status]
expected: FAIL

View file

@ -3,7 +3,7 @@
expected: FAIL
[test_finds_exising_user_prompt_after_tab_switch[confirm\]]
expected: FAIL
expected: ERROR
[test_finds_exising_user_prompt_after_tab_switch[prompt\]]
expected: FAIL
expected: ERROR