webdriver: Element click waits for navigation complete (#37935)

Step 11 in https://w3c.github.io/webdriver/#dfn-element-click

> [Try](https://w3c.github.io/webdriver/#dfn-try) to [wait for
navigation to
complete](https://w3c.github.io/webdriver/#dfn-wait-for-navigation-to-complete)
with session.

This fixes issue in which element_click triggers navigation, but
incoming commands still interact with old page.

Testing:
https://github.com/longvatrong111/servo/actions/runs/16175767947
https://github.com/longvatrong111/servo/actions/runs/16175770044

Signed-off-by: batu_hoang <longvatrong111@gmail.com>
This commit is contained in:
batu_hoang 2025-07-15 14:51:05 +08:00 committed by GitHub
parent ff02fdad6d
commit f155c95e1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 137 additions and 63 deletions

View file

@ -2499,6 +2499,13 @@ impl ScriptThread {
reply, reply,
can_gc, can_gc,
), ),
WebDriverScriptCommand::IsDocumentReadyStateComplete(response_sender) => {
webdriver_handlers::handle_try_wait_for_document_navigation(
&documents,
pipeline_id,
response_sender,
)
},
_ => (), _ => (),
} }
} }

View file

@ -33,7 +33,9 @@ use webdriver::error::ErrorStatus;
use crate::document_collection::DocumentCollection; use crate::document_collection::DocumentCollection;
use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods; use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods; use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
DocumentMethods, DocumentReadyState,
};
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
@ -1717,3 +1719,20 @@ pub(crate) fn handle_is_selected(
) )
.unwrap(); .unwrap();
} }
pub(crate) fn handle_try_wait_for_document_navigation(
documents: &DocumentCollection,
pipeline: PipelineId,
reply: IpcSender<bool>,
) {
let document = match documents.find_document(pipeline) {
Some(document) => document,
None => {
return reply.send(false).unwrap();
},
};
let wait_for_document_ready = document.ReadyState() != DocumentReadyState::Complete;
reply.send(wait_for_document_ready).unwrap();
}

View file

@ -127,6 +127,8 @@ pub enum WebDriverCommandMsg {
IpcSender<Result<(), ()>>, IpcSender<Result<(), ()>>,
), ),
GetAlertText(WebViewId, IpcSender<Result<String, ()>>), GetAlertText(WebViewId, IpcSender<Result<String, ()>>),
AddLoadStatusSender(WebViewId, IpcSender<WebDriverLoadStatus>),
RemoveLoadStatusSender(WebViewId),
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -202,6 +204,7 @@ pub enum WebDriverScriptCommand {
GetTitle(IpcSender<String>), GetTitle(IpcSender<String>),
/// Match the element type before sending the event for webdriver `element send keys`. /// Match the element type before sending the event for webdriver `element send keys`.
WillSendKeys(String, String, bool, IpcSender<Result<bool, ErrorStatus>>), WillSendKeys(String, String, bool, IpcSender<Result<bool, ErrorStatus>>),
IsDocumentReadyStateComplete(IpcSender<bool>),
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]

View file

@ -1871,9 +1871,11 @@ impl Handler {
Ok(WebDriverResponse::Void) Ok(WebDriverResponse::Void)
} }
// https://w3c.github.io/webdriver/#element-click /// <https://w3c.github.io/webdriver/#element-click>
fn handle_element_click(&mut self, element: &WebElement) -> WebDriverResult<WebDriverResponse> { fn handle_element_click(&mut self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
let (sender, receiver) = ipc::channel().unwrap(); let (sender, receiver) = ipc::channel().unwrap();
let webview_id = self.session()?.webview_id;
let browsing_context_id = self.session()?.browsing_context_id;
// Steps 1 - 7 + Step 8 for <option> // Steps 1 - 7 + Step 8 for <option>
let cmd = WebDriverScriptCommand::ElementClick(element.to_string(), sender); let cmd = WebDriverScriptCommand::ElementClick(element.to_string(), sender);
@ -1882,72 +1884,36 @@ impl Handler {
match wait_for_script_response(receiver)? { match wait_for_script_response(receiver)? {
Ok(element_id) => match element_id { Ok(element_id) => match element_id {
Some(element_id) => { Some(element_id) => {
let id = Uuid::new_v4().to_string(); // Load status sender should be set up before we dispatch actions
// Step 8 for elements other than <option> self.send_message_to_embedder(WebDriverCommandMsg::AddLoadStatusSender(
// Step 8.1 webview_id,
self.session_mut()?.input_state_table.borrow_mut().insert( self.load_status_sender.clone(),
id.clone(), ))?;
InputSourceState::Pointer(PointerInputState::new(PointerType::Mouse)),
);
// Step 8.7. Construct a pointer move action. self.perform_element_click(element_id)?;
// Step 8.8. Set a property x to 0 on pointer move action.
// Step 8.9. Set a property y to 0 on pointer move action.
// Step 8.10. Set a property origin to element on pointer move action.
let pointer_move_action = PointerMoveAction {
duration: None,
origin: PointerOrigin::Element(WebElement(element_id)),
x: 0.0,
y: 0.0,
..Default::default()
};
// Step 8.11. Construct pointer down action. // Step 11. Try to wait for navigation to complete with session.
// Step 8.12. Set a property button to 0 on pointer down action. // The most reliable way to try to wait for a potential navigation
let pointer_down_action = PointerDownAction { // which is caused by element click to check with script thread
button: i16::from(MouseButton::Left) as u64, let (sender, receiver) = ipc::channel().unwrap();
..Default::default() self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
}; browsing_context_id,
WebDriverScriptCommand::IsDocumentReadyStateComplete(sender),
))?;
// Step 8.13. Construct pointer up action. if wait_for_script_response(receiver)? {
// Step 8.14. Set a property button to 0 on pointer up action. self.load_status_receiver.recv().map_err(|_| {
let pointer_up_action = PointerUpAction { WebDriverError::new(
button: i16::from(MouseButton::Left) as u64, ErrorStatus::UnknownError,
..Default::default() "Failed to receive load status",
}; )
})?;
let action_sequence = ActionSequence { } else {
id: id.clone(), self.send_message_to_embedder(
actions: ActionsType::Pointer { WebDriverCommandMsg::RemoveLoadStatusSender(webview_id),
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)),
],
},
};
// Step 8.16. Dispatch a list of actions with session's current browsing context
let actions_by_tick = self.actions_by_tick_from_sequence(vec![action_sequence]);
if let Err(e) =
self.dispatch_actions(actions_by_tick, self.session()?.browsing_context_id)
{
log::error!("handle_element_click: dispatch_actions failed: {:?}", e);
} }
// Step 8.17 Remove an input source with input state and input id.
self.session_mut()?
.input_state_table
.borrow_mut()
.remove(&id);
// Step 13 // Step 13
Ok(WebDriverResponse::Void) Ok(WebDriverResponse::Void)
}, },
@ -1958,6 +1924,72 @@ impl Handler {
} }
} }
fn perform_element_click(&mut self, element: String) -> WebDriverResult<WebDriverResponse> {
// Step 8 for elements other than <option>
let id = Uuid::new_v4().to_string();
// Step 8.1
self.session_mut()?.input_state_table.borrow_mut().insert(
id.clone(),
InputSourceState::Pointer(PointerInputState::new(PointerType::Mouse)),
);
// Step 8.7. Construct a pointer move action.
// Step 8.8. Set a property x to 0 on pointer move action.
// Step 8.9. Set a property y to 0 on pointer move action.
// Step 8.10. Set a property origin to element on pointer move action.
let pointer_move_action = PointerMoveAction {
duration: None,
origin: PointerOrigin::Element(WebElement(element)),
x: 0.0,
y: 0.0,
..Default::default()
};
// Step 8.11. Construct pointer down action.
// Step 8.12. Set a property button to 0 on pointer down action.
let pointer_down_action = PointerDownAction {
button: i16::from(MouseButton::Left) as u64,
..Default::default()
};
// Step 8.13. Construct pointer up action.
// Step 8.14. Set a property button to 0 on pointer up action.
let pointer_up_action = PointerUpAction {
button: i16::from(MouseButton::Left) as u64,
..Default::default()
};
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)),
],
},
};
// Step 8.16. Dispatch a list of actions with session's current browsing context
let actions_by_tick = self.actions_by_tick_from_sequence(vec![action_sequence]);
if let Err(e) = self.dispatch_actions(actions_by_tick, self.session()?.browsing_context_id)
{
log::error!("handle_element_click: dispatch_actions failed: {:?}", e);
}
// Step 8.17 Remove an input source with input state and input id.
self.session_mut()?
.input_state_table
.borrow_mut()
.remove(&id);
Ok(WebDriverResponse::Void)
}
fn take_screenshot(&self, rect: Option<Rect<f32, CSSPixel>>) -> WebDriverResult<String> { fn take_screenshot(&self, rect: Option<Rect<f32, CSSPixel>>) -> WebDriverResult<String> {
// Step 1. If session's current top-level browsing context is no longer open, // Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window. // return error with error code no such window.

View file

@ -454,6 +454,12 @@ impl App {
warn!("Failed to send response of GetFocusedWebView: {error}"); warn!("Failed to send response of GetFocusedWebView: {error}");
}; };
}, },
WebDriverCommandMsg::AddLoadStatusSender(webview_id, load_status_sender) => {
running_state.set_load_status_sender(webview_id, load_status_sender);
},
WebDriverCommandMsg::RemoveLoadStatusSender(webview_id) => {
running_state.remove_load_status_sender(webview_id);
},
WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => { WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => {
if let Some(webview) = running_state.webview_by_id(webview_id) { if let Some(webview) = running_state.webview_by_id(webview_id) {
webview.load(url.into_url()); webview.load(url.into_url());

View file

@ -470,6 +470,13 @@ impl RunningAppState {
}); });
} }
} }
pub(crate) fn remove_load_status_sender(&self, webview_id: WebViewId) {
self.webdriver_senders
.borrow_mut()
.load_status_senders
.remove(&webview_id);
}
} }
struct ServoShellServoDelegate; struct ServoShellServoDelegate;