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,
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::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
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::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
@ -1717,3 +1719,20 @@ pub(crate) fn handle_is_selected(
)
.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<(), ()>>,
),
GetAlertText(WebViewId, IpcSender<Result<String, ()>>),
AddLoadStatusSender(WebViewId, IpcSender<WebDriverLoadStatus>),
RemoveLoadStatusSender(WebViewId),
}
#[derive(Debug, Deserialize, Serialize)]
@ -202,6 +204,7 @@ pub enum WebDriverScriptCommand {
GetTitle(IpcSender<String>),
/// Match the element type before sending the event for webdriver `element send keys`.
WillSendKeys(String, String, bool, IpcSender<Result<bool, ErrorStatus>>),
IsDocumentReadyStateComplete(IpcSender<bool>),
}
#[derive(Clone, Debug, Deserialize, Serialize)]

View file

@ -1871,9 +1871,11 @@ impl Handler {
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> {
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>
let cmd = WebDriverScriptCommand::ElementClick(element.to_string(), sender);
@ -1882,8 +1884,50 @@ impl Handler {
match wait_for_script_response(receiver)? {
Ok(element_id) => match element_id {
Some(element_id) => {
let id = Uuid::new_v4().to_string();
// Load status sender should be set up before we dispatch actions
self.send_message_to_embedder(WebDriverCommandMsg::AddLoadStatusSender(
webview_id,
self.load_status_sender.clone(),
))?;
self.perform_element_click(element_id)?;
// Step 11. Try to wait for navigation to complete with session.
// The most reliable way to try to wait for a potential navigation
// which is caused by element click to check with script thread
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
WebDriverScriptCommand::IsDocumentReadyStateComplete(sender),
))?;
if wait_for_script_response(receiver)? {
self.load_status_receiver.recv().map_err(|_| {
WebDriverError::new(
ErrorStatus::UnknownError,
"Failed to receive load status",
)
})?;
} else {
self.send_message_to_embedder(
WebDriverCommandMsg::RemoveLoadStatusSender(webview_id),
)?;
}
// Step 13
Ok(WebDriverResponse::Void)
},
// Step 13
None => Ok(WebDriverResponse::Void),
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
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(),
@ -1896,7 +1940,7 @@ impl Handler {
// 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)),
origin: PointerOrigin::Element(WebElement(element)),
x: 0.0,
y: 0.0,
..Default::default()
@ -1923,12 +1967,8 @@ impl Handler {
pointer_type: PointerType::Mouse,
},
actions: vec![
PointerActionItem::Pointer(PointerAction::Move(
pointer_move_action,
)),
PointerActionItem::Pointer(PointerAction::Down(
pointer_down_action,
)),
PointerActionItem::Pointer(PointerAction::Move(pointer_move_action)),
PointerActionItem::Pointer(PointerAction::Down(pointer_down_action)),
PointerActionItem::Pointer(PointerAction::Up(pointer_up_action)),
],
},
@ -1936,8 +1976,7 @@ impl Handler {
// 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)
if let Err(e) = self.dispatch_actions(actions_by_tick, self.session()?.browsing_context_id)
{
log::error!("handle_element_click: dispatch_actions failed: {:?}", e);
}
@ -1948,14 +1987,7 @@ impl Handler {
.borrow_mut()
.remove(&id);
// Step 13
Ok(WebDriverResponse::Void)
},
// Step 13
None => Ok(WebDriverResponse::Void),
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
fn take_screenshot(&self, rect: Option<Rect<f32, CSSPixel>>) -> WebDriverResult<String> {

View file

@ -454,6 +454,12 @@ impl App {
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) => {
if let Some(webview) = running_state.webview_by_id(webview_id) {
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;