mirror of
https://github.com/servo/servo.git
synced 2025-07-15 19:33:46 +01:00
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:
parent
84f0cd5801
commit
2e44aba753
9 changed files with 296 additions and 42 deletions
|
@ -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)]
|
||||
pub struct AuthenticationResponse {
|
||||
/// Username for http request authentication
|
||||
|
|
|
@ -28,6 +28,12 @@ use crate::{MouseButton, MouseButtonAction};
|
|||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct WebDriverMessageId(pub usize);
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum WebDriverUserPromptAction {
|
||||
Accept,
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
/// Messages to the constellation originating from the WebDriver server.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum WebDriverCommandMsg {
|
||||
|
@ -115,6 +121,12 @@ pub enum WebDriverCommandMsg {
|
|||
IsWebViewOpen(WebViewId, IpcSender<bool>),
|
||||
/// Check whether browsing context is open.
|
||||
IsBrowsingContextOpen(BrowsingContextId, IpcSender<bool>),
|
||||
HandleUserPrompt(
|
||||
WebViewId,
|
||||
WebDriverUserPromptAction,
|
||||
IpcSender<Result<(), ()>>,
|
||||
),
|
||||
GetAlertText(WebViewId, IpcSender<Result<String, ()>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -237,4 +249,5 @@ pub enum WebDriverLoadStatus {
|
|||
Complete,
|
||||
Timeout,
|
||||
Canceled,
|
||||
Blocked,
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
mod actions;
|
||||
mod capabilities;
|
||||
mod user_prompt;
|
||||
|
||||
use std::borrow::ToOwned;
|
||||
use std::cell::{Cell, LazyCell, RefCell};
|
||||
|
@ -515,6 +516,11 @@ impl Handler {
|
|||
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> {
|
||||
let (sender, receiver) = ipc::channel().unwrap();
|
||||
self.send_message_to_embedder(WebDriverCommandMsg::GetFocusedWebView(sender.clone()))?;
|
||||
|
@ -782,7 +788,19 @@ impl Handler {
|
|||
debug!("waiting for load");
|
||||
let timeout = self.session()?.load_timeout;
|
||||
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(
|
||||
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> {
|
||||
let session = self
|
||||
.session
|
||||
|
@ -2231,6 +2240,8 @@ impl WebDriverHandler<ServoExtensionRoute> for Handler {
|
|||
},
|
||||
WebDriverCommand::ElementClick(ref element) => self.handle_element_click(element),
|
||||
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::DeleteCookie(name) => self.handle_delete_cookie(name),
|
||||
WebDriverCommand::GetTimeouts => self.handle_get_timeouts(),
|
||||
|
|
93
components/webdriver_server/user_prompt.rs
Normal file
93
components/webdriver_server/user_prompt.rs
Normal 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),
|
||||
)
|
||||
})?,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,7 +27,8 @@ use servo::webrender_api::ScrollLocation;
|
|||
use servo::webrender_api::units::DeviceIntSize;
|
||||
use servo::{
|
||||
EventLoopWaker, InputEvent, KeyboardEvent, MouseButtonEvent, MouseMoveEvent,
|
||||
WebDriverCommandMsg, WebDriverScriptCommand, WheelDelta, WheelEvent, WheelMode,
|
||||
WebDriverCommandMsg, WebDriverScriptCommand, WebDriverUserPromptAction, WheelDelta, WheelEvent,
|
||||
WheelMode,
|
||||
};
|
||||
use url::Url;
|
||||
use winit::application::ApplicationHandler;
|
||||
|
@ -552,6 +553,35 @@ impl App {
|
|||
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(..) => {
|
||||
warn!(
|
||||
"WebDriverCommand {:?} is still not moved from constellation to embedder",
|
||||
|
|
|
@ -337,6 +337,38 @@ impl RunningAppState {
|
|||
.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> {
|
||||
let focused_id = self.inner().focused_webview_id?;
|
||||
self.webviews()
|
||||
|
@ -486,6 +518,17 @@ impl WebViewDelegate for RunningAppState {
|
|||
fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) {
|
||||
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 &&
|
||||
self.servoshell_preferences.webdriver_port.is_none()
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
match self {
|
||||
Dialog::File {
|
||||
|
|
|
@ -1,19 +1,6 @@
|
|||
[accept.py]
|
||||
expected: TIMEOUT
|
||||
[test_null_response_value]
|
||||
expected: FAIL
|
||||
|
||||
[test_no_top_level_browsing_context]
|
||||
expected: FAIL
|
||||
|
||||
[test_no_browsing_context]
|
||||
expected: FAIL
|
||||
|
||||
[test_no_user_prompt]
|
||||
expected: FAIL
|
||||
|
||||
[test_accept_alert]
|
||||
expected: FAIL
|
||||
expected: ERROR
|
||||
|
||||
[test_accept_confirm]
|
||||
expected: FAIL
|
||||
|
@ -21,8 +8,11 @@
|
|||
[test_accept_prompt]
|
||||
expected: FAIL
|
||||
|
||||
[test_unexpected_alert]
|
||||
expected: FAIL
|
||||
|
||||
[test_accept_in_popup_window]
|
||||
expected: FAIL
|
||||
|
||||
[test_null_response_value]
|
||||
expected: FAIL
|
||||
|
||||
[test_accept_alert]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
[dismiss.py]
|
||||
expected: TIMEOUT
|
||||
[test_no_top_browsing_context]
|
||||
expected: FAIL
|
||||
|
||||
[test_no_browsing_context]
|
||||
expected: FAIL
|
||||
|
||||
[test_no_user_prompt]
|
||||
expected: FAIL
|
||||
|
||||
[test_dismiss_alert]
|
||||
expected: FAIL
|
||||
expected: ERROR
|
||||
|
||||
[test_dismiss_confirm]
|
||||
expected: FAIL
|
||||
|
@ -18,8 +8,11 @@
|
|||
[test_dismiss_prompt]
|
||||
expected: FAIL
|
||||
|
||||
[test_unexpected_alert]
|
||||
expected: FAIL
|
||||
|
||||
[test_dismiss_in_popup_window]
|
||||
expected: FAIL
|
||||
|
||||
[test_null_response_value]
|
||||
expected: FAIL
|
||||
|
||||
[test_dismiss_alert]
|
||||
expected: FAIL
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue