diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index cc287d20a41..c0561aa6add 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -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 diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index 224f319fa90..712c23cacb3 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -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), /// Check whether browsing context is open. IsBrowsingContextOpen(BrowsingContextId, IpcSender), + HandleUserPrompt( + WebViewId, + WebDriverUserPromptAction, + IpcSender>, + ), + GetAlertText(WebViewId, IpcSender>), } #[derive(Debug, Deserialize, Serialize)] @@ -237,4 +249,5 @@ pub enum WebDriverLoadStatus { Complete, Timeout, Canceled, + Blocked, } diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index ca26d28c045..b858cbb3dd7 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -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 { 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 { - // 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 { let session = self .session @@ -2231,6 +2240,8 @@ impl WebDriverHandler 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(), diff --git a/components/webdriver_server/user_prompt.rs b/components/webdriver_server/user_prompt.rs new file mode 100644 index 00000000000..52f8fbafdd4 --- /dev/null +++ b/components/webdriver_server/user_prompt.rs @@ -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 { + /// + pub(crate) fn handle_dismiss_alert(&mut self) -> WebDriverResult { + // 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), + } + } + + /// + pub(crate) fn handle_accept_alert(&mut self) -> WebDriverResult { + // 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 { + // 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), + ) + })?, + ))), + } + } +} diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index b94e12e40b0..04546ee0d48 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -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", diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs index b824aba1b63..e9d2da88805 100644 --- a/ports/servoshell/desktop/app_state.rs +++ b/ports/servoshell/desktop/app_state.rs @@ -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 { + 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 { 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() { diff --git a/ports/servoshell/desktop/dialog.rs b/ports/servoshell/desktop/dialog.rs index 34cde6c34bc..46b24260adb 100644 --- a/ports/servoshell/desktop/dialog.rs +++ b/ports/servoshell/desktop/dialog.rs @@ -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 { + #[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 { diff --git a/tests/wpt/meta/webdriver/tests/classic/accept_alert/accept.py.ini b/tests/wpt/meta/webdriver/tests/classic/accept_alert/accept.py.ini index a5d10aa6fe2..28746ac2aaa 100644 --- a/tests/wpt/meta/webdriver/tests/classic/accept_alert/accept.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/accept_alert/accept.py.ini @@ -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 diff --git a/tests/wpt/meta/webdriver/tests/classic/dismiss_alert/dismiss.py.ini b/tests/wpt/meta/webdriver/tests/classic/dismiss_alert/dismiss.py.ini index a2869095ca8..e3f632a82a8 100644 --- a/tests/wpt/meta/webdriver/tests/classic/dismiss_alert/dismiss.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/dismiss_alert/dismiss.py.ini @@ -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