webdriver: Implement support for simple dialogs (#37913)

Implement webdriver user prompt: accept alert, dismiss alert, get alert
text.

Tests:
https://github.com/longvatrong111/servo/actions/runs/16175408035
https://github.com/longvatrong111/servo/actions/runs/16175409545

Signed-off-by: batu_hoang <longvatrong111@gmail.com>
This commit is contained in:
batu_hoang 2025-07-10 11:15:46 +08:00 committed by GitHub
parent 84f0cd5801
commit 2e44aba753
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 296 additions and 42 deletions

View file

@ -160,6 +160,59 @@ pub enum SimpleDialog {
}, },
} }
impl SimpleDialog {
/// Returns the message of the dialog.
pub fn message(&self) -> &str {
match self {
SimpleDialog::Alert { message, .. } => message,
SimpleDialog::Confirm { message, .. } => message,
SimpleDialog::Prompt { message, .. } => message,
}
}
pub fn dismiss(&self) {
match self {
SimpleDialog::Alert {
response_sender, ..
} => {
let _ = response_sender.send(AlertResponse::Ok);
},
SimpleDialog::Confirm {
response_sender, ..
} => {
let _ = response_sender.send(ConfirmResponse::Cancel);
},
SimpleDialog::Prompt {
response_sender, ..
} => {
let _ = response_sender.send(PromptResponse::Cancel);
},
}
}
pub fn accept(&self) {
match self {
SimpleDialog::Alert {
response_sender, ..
} => {
let _ = response_sender.send(AlertResponse::Ok);
},
SimpleDialog::Confirm {
response_sender, ..
} => {
let _ = response_sender.send(ConfirmResponse::Ok);
},
SimpleDialog::Prompt {
default,
response_sender,
..
} => {
let _ = response_sender.send(PromptResponse::Ok(default.clone()));
},
}
}
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct AuthenticationResponse { pub struct AuthenticationResponse {
/// Username for http request authentication /// Username for http request authentication

View file

@ -28,6 +28,12 @@ use crate::{MouseButton, MouseButtonAction};
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub struct WebDriverMessageId(pub usize); pub struct WebDriverMessageId(pub usize);
#[derive(Debug, Deserialize, Serialize)]
pub enum WebDriverUserPromptAction {
Accept,
Dismiss,
}
/// Messages to the constellation originating from the WebDriver server. /// Messages to the constellation originating from the WebDriver server.
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub enum WebDriverCommandMsg { pub enum WebDriverCommandMsg {
@ -115,6 +121,12 @@ pub enum WebDriverCommandMsg {
IsWebViewOpen(WebViewId, IpcSender<bool>), IsWebViewOpen(WebViewId, IpcSender<bool>),
/// Check whether browsing context is open. /// Check whether browsing context is open.
IsBrowsingContextOpen(BrowsingContextId, IpcSender<bool>), IsBrowsingContextOpen(BrowsingContextId, IpcSender<bool>),
HandleUserPrompt(
WebViewId,
WebDriverUserPromptAction,
IpcSender<Result<(), ()>>,
),
GetAlertText(WebViewId, IpcSender<Result<String, ()>>),
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -237,4 +249,5 @@ pub enum WebDriverLoadStatus {
Complete, Complete,
Timeout, Timeout,
Canceled, Canceled,
Blocked,
} }

View file

@ -8,6 +8,7 @@
mod actions; mod actions;
mod capabilities; mod capabilities;
mod user_prompt;
use std::borrow::ToOwned; use std::borrow::ToOwned;
use std::cell::{Cell, LazyCell, RefCell}; use std::cell::{Cell, LazyCell, RefCell};
@ -515,6 +516,11 @@ impl Handler {
Ok(()) Ok(())
} }
// This function is called only if session and webview are verified.
fn verified_webview_id(&self) -> WebViewId {
self.session().unwrap().webview_id
}
fn focus_webview_id(&self) -> WebDriverResult<WebViewId> { fn focus_webview_id(&self) -> WebDriverResult<WebViewId> {
let (sender, receiver) = ipc::channel().unwrap(); let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::GetFocusedWebView(sender.clone()))?; self.send_message_to_embedder(WebDriverCommandMsg::GetFocusedWebView(sender.clone()))?;
@ -782,7 +788,19 @@ impl Handler {
debug!("waiting for load"); debug!("waiting for load");
let timeout = self.session()?.load_timeout; let timeout = self.session()?.load_timeout;
let result = select! { let result = select! {
recv(self.load_status_receiver) -> _ => Ok(WebDriverResponse::Void), recv(self.load_status_receiver) -> res => {
match res {
Ok(WebDriverLoadStatus::Blocked) => {
Err(WebDriverError::new(
ErrorStatus::UnexpectedAlertOpen,
"Load is blocked",
))
}
_ => {
Ok(WebDriverResponse::Void)
}
}
},
recv(after(Duration::from_millis(timeout))) -> _ => Err( recv(after(Duration::from_millis(timeout))) -> _ => Err(
WebDriverError::new(ErrorStatus::Timeout, "Load timed out") WebDriverError::new(ErrorStatus::Timeout, "Load timed out")
), ),
@ -1605,15 +1623,6 @@ impl Handler {
} }
} }
// https://w3c.github.io/webdriver/#dismiss-alert
fn handle_dismiss_alert(&mut self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(self.session()?.webview_id)?;
// Since user prompts are not yet implement this will always succeed
Ok(WebDriverResponse::Void)
}
fn handle_get_timeouts(&mut self) -> WebDriverResult<WebDriverResponse> { fn handle_get_timeouts(&mut self) -> WebDriverResult<WebDriverResponse> {
let session = self let session = self
.session .session
@ -2231,6 +2240,8 @@ impl WebDriverHandler<ServoExtensionRoute> for Handler {
}, },
WebDriverCommand::ElementClick(ref element) => self.handle_element_click(element), WebDriverCommand::ElementClick(ref element) => self.handle_element_click(element),
WebDriverCommand::DismissAlert => self.handle_dismiss_alert(), WebDriverCommand::DismissAlert => self.handle_dismiss_alert(),
WebDriverCommand::AcceptAlert => self.handle_accept_alert(),
WebDriverCommand::GetAlertText => self.handle_get_alert_text(),
WebDriverCommand::DeleteCookies => self.handle_delete_cookies(), WebDriverCommand::DeleteCookies => self.handle_delete_cookies(),
WebDriverCommand::DeleteCookie(name) => self.handle_delete_cookie(name), WebDriverCommand::DeleteCookie(name) => self.handle_delete_cookie(name),
WebDriverCommand::GetTimeouts => self.handle_get_timeouts(), WebDriverCommand::GetTimeouts => self.handle_get_timeouts(),

View file

@ -0,0 +1,93 @@
/* 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 embedder_traits::{WebDriverCommandMsg, WebDriverUserPromptAction};
use ipc_channel::ipc;
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
use webdriver::response::{ValueResponse, WebDriverResponse};
use crate::{Handler, wait_for_script_response};
impl Handler {
/// <https://w3c.github.io/webdriver/#dismiss-alert>
pub(crate) fn handle_dismiss_alert(&mut self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(self.session()?.webview_id)?;
// Step 3. Dismiss the current user prompt.
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::HandleUserPrompt(
self.verified_webview_id(),
WebDriverUserPromptAction::Dismiss,
sender,
))?;
match wait_for_script_response(receiver)? {
// Step 2. If the current user prompt is null, return error with error code no such alert.
Err(()) => Err(WebDriverError::new(
ErrorStatus::NoSuchAlert,
"No user prompt is currently active.",
)),
// Step 4. Return success with data null.
Ok(()) => Ok(WebDriverResponse::Void),
}
}
/// <https://w3c.github.io/webdriver/#accept-alert>
pub(crate) fn handle_accept_alert(&mut self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(self.session()?.webview_id)?;
// Step 3. Accept the current user prompt.
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::HandleUserPrompt(
self.verified_webview_id(),
WebDriverUserPromptAction::Accept,
sender,
))?;
match wait_for_script_response(receiver)? {
// Step 2. If the current user prompt is null, return error with error code no such alert.
Err(()) => Err(WebDriverError::new(
ErrorStatus::NoSuchAlert,
"No user prompt is currently active.",
)),
// Step 4. Return success with data null.
Ok(()) => Ok(WebDriverResponse::Void),
}
}
pub(crate) fn handle_get_alert_text(&mut self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(self.session()?.webview_id)?;
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::GetAlertText(
self.verified_webview_id(),
sender,
))?;
match wait_for_script_response(receiver)? {
// Step 2. If the current user prompt is null, return error with error code no such alert.
Err(()) => Err(WebDriverError::new(
ErrorStatus::NoSuchAlert,
"No user prompt is currently active.",
)),
// Step 3. Let message be the text message associated with the current user prompt
// or otherwise be null
// Step 4. Return success with data message.
Ok(message) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(message).map_err(|e| {
WebDriverError::new(
ErrorStatus::UnknownError,
format!("Failed to serialize alert text: {}", e),
)
})?,
))),
}
}
}

View file

@ -27,7 +27,8 @@ use servo::webrender_api::ScrollLocation;
use servo::webrender_api::units::DeviceIntSize; use servo::webrender_api::units::DeviceIntSize;
use servo::{ use servo::{
EventLoopWaker, InputEvent, KeyboardEvent, MouseButtonEvent, MouseMoveEvent, EventLoopWaker, InputEvent, KeyboardEvent, MouseButtonEvent, MouseMoveEvent,
WebDriverCommandMsg, WebDriverScriptCommand, WheelDelta, WheelEvent, WheelMode, WebDriverCommandMsg, WebDriverScriptCommand, WebDriverUserPromptAction, WheelDelta, WheelEvent,
WheelMode,
}; };
use url::Url; use url::Url;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
@ -552,6 +553,35 @@ impl App {
webdriver_script_command, webdriver_script_command,
)); ));
}, },
WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => {
let response = if running_state.webview_has_active_dialog(webview_id) {
match action {
WebDriverUserPromptAction::Accept => {
running_state.accept_active_dialogs(webview_id)
},
WebDriverUserPromptAction::Dismiss => {
running_state.dismiss_active_dialogs(webview_id)
},
};
Ok(())
} else {
Err(())
};
if let Err(error) = response_sender.send(response) {
warn!("Failed to send response of HandleUserPrompt: {error}");
};
},
WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => {
let response = match running_state.alert_text_of_newest_dialog(webview_id) {
Some(text) => Ok(text),
None => Err(()),
};
if let Err(error) = response_sender.send(response) {
warn!("Failed to send response of GetAlertText: {error}");
};
},
WebDriverCommandMsg::TakeScreenshot(..) => { WebDriverCommandMsg::TakeScreenshot(..) => {
warn!( warn!(
"WebDriverCommand {:?} is still not moved from constellation to embedder", "WebDriverCommand {:?} is still not moved from constellation to embedder",

View file

@ -337,6 +337,38 @@ impl RunningAppState {
.is_some_and(|dialogs| !dialogs.is_empty()) .is_some_and(|dialogs| !dialogs.is_empty())
} }
pub(crate) fn webview_has_active_dialog(&self, webview_id: WebViewId) -> bool {
let inner = self.inner();
inner
.dialogs
.get(&webview_id)
.is_some_and(|dialogs| !dialogs.is_empty())
}
pub(crate) fn accept_active_dialogs(&self, webview_id: WebViewId) {
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
dialogs.drain(..).for_each(|dialog| {
dialog.accept();
});
}
}
pub(crate) fn dismiss_active_dialogs(&self, webview_id: WebViewId) {
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
dialogs.drain(..).for_each(|dialog| {
dialog.dismiss();
});
}
}
pub(crate) fn alert_text_of_newest_dialog(&self, webview_id: WebViewId) -> Option<String> {
self.inner()
.dialogs
.get(&webview_id)
.and_then(|dialogs| dialogs.last())
.and_then(|dialog| dialog.message())
}
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> { pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
let focused_id = self.inner().focused_webview_id?; let focused_id = self.inner().focused_webview_id?;
self.webviews() self.webviews()
@ -486,6 +518,17 @@ impl WebViewDelegate for RunningAppState {
fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) { fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) {
self.interrupt_webdriver_script_evaluation(); self.interrupt_webdriver_script_evaluation();
// Dialogs block the page load, so need need to notify WebDriver
let webview_id = webview.id();
if let Some(sender) = self
.webdriver_senders
.borrow_mut()
.load_status_senders
.get(&webview_id)
{
let _ = sender.send(WebDriverLoadStatus::Blocked);
};
if self.servoshell_preferences.headless && if self.servoshell_preferences.headless &&
self.servoshell_preferences.webdriver_port.is_none() self.servoshell_preferences.webdriver_port.is_none()
{ {

View file

@ -140,6 +140,34 @@ impl Dialog {
} }
} }
pub fn accept(&self) {
#[allow(clippy::single_match)]
match self {
Dialog::SimpleDialog(dialog) => {
dialog.accept();
},
_ => {},
}
}
pub fn dismiss(&self) {
#[allow(clippy::single_match)]
match self {
Dialog::SimpleDialog(dialog) => {
dialog.dismiss();
},
_ => {},
}
}
pub fn message(&self) -> Option<String> {
#[allow(clippy::single_match)]
match self {
Dialog::SimpleDialog(dialog) => Some(dialog.message().to_string()),
_ => None,
}
}
pub fn update(&mut self, ctx: &egui::Context) -> bool { pub fn update(&mut self, ctx: &egui::Context) -> bool {
match self { match self {
Dialog::File { Dialog::File {

View file

@ -1,19 +1,6 @@
[accept.py] [accept.py]
expected: TIMEOUT
[test_null_response_value]
expected: FAIL
[test_no_top_level_browsing_context] [test_no_top_level_browsing_context]
expected: FAIL expected: ERROR
[test_no_browsing_context]
expected: FAIL
[test_no_user_prompt]
expected: FAIL
[test_accept_alert]
expected: FAIL
[test_accept_confirm] [test_accept_confirm]
expected: FAIL expected: FAIL
@ -21,8 +8,11 @@
[test_accept_prompt] [test_accept_prompt]
expected: FAIL expected: FAIL
[test_unexpected_alert]
expected: FAIL
[test_accept_in_popup_window] [test_accept_in_popup_window]
expected: FAIL expected: FAIL
[test_null_response_value]
expected: FAIL
[test_accept_alert]
expected: FAIL

View file

@ -1,16 +1,6 @@
[dismiss.py] [dismiss.py]
expected: TIMEOUT
[test_no_top_browsing_context] [test_no_top_browsing_context]
expected: FAIL expected: ERROR
[test_no_browsing_context]
expected: FAIL
[test_no_user_prompt]
expected: FAIL
[test_dismiss_alert]
expected: FAIL
[test_dismiss_confirm] [test_dismiss_confirm]
expected: FAIL expected: FAIL
@ -18,8 +8,11 @@
[test_dismiss_prompt] [test_dismiss_prompt]
expected: FAIL expected: FAIL
[test_unexpected_alert]
expected: FAIL
[test_dismiss_in_popup_window] [test_dismiss_in_popup_window]
expected: FAIL expected: FAIL
[test_null_response_value]
expected: FAIL
[test_dismiss_alert]
expected: FAIL