fix(click): wait for paint to settle so SPA renders complete before next command
Closes a real-world race that broke X multi-tweet thread composition (and similar SPA flows): clicking "Add post" returned immediately, inserttext fired before React had committed the new textarea, the keystroke landed on the dialog wrapper, and X interpreted the stray input as a request to dismiss the modal. After mouseReleased we now wait for two requestAnimationFrame ticks plus a microtask boundary (~33ms at 60fps, bounded). That's enough for React/Vue/Svelte to commit any state update scheduled by the click handler. Errors during the wait are swallowed — a click never fails because of post-processing. Opt out for perf-sensitive scripts that don't drive SPA UIs: AGENT_BROWSER_CLICK_WAIT_STABLE=0
This commit is contained in:
@@ -884,6 +884,38 @@ pub async fn tap_touch(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// After a click is dispatched, give the page two animation frames + a
|
||||||
|
/// microtask boundary to let React/Vue/Svelte commit any state update
|
||||||
|
/// scheduled by the click handler. Without this wait, follow-up commands
|
||||||
|
/// (e.g. `inserttext` against the textbox the click was supposed to mount)
|
||||||
|
/// race the renderer and can land on stale or wrong elements.
|
||||||
|
///
|
||||||
|
/// The wait is bounded to ~33ms in the common case (two RAFs at 60fps) and
|
||||||
|
/// returns immediately on any error — never an exception path.
|
||||||
|
///
|
||||||
|
/// Set `AGENT_BROWSER_CLICK_WAIT_STABLE=0` to disable for perf-sensitive
|
||||||
|
/// scripts that don't drive SPA UIs.
|
||||||
|
async fn wait_for_paint_settled(client: &CdpClient, session_id: &str) {
|
||||||
|
if std::env::var("AGENT_BROWSER_CLICK_WAIT_STABLE").as_deref() == Ok("0") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let script = "new Promise(resolve => \
|
||||||
|
requestAnimationFrame(() => \
|
||||||
|
requestAnimationFrame(() => \
|
||||||
|
queueMicrotask(() => resolve(true)))))";
|
||||||
|
let _ = client
|
||||||
|
.send_command_typed::<_, Value>(
|
||||||
|
"Runtime.evaluate",
|
||||||
|
&EvaluateParams {
|
||||||
|
expression: script.to_string(),
|
||||||
|
return_by_value: Some(true),
|
||||||
|
await_promise: Some(true),
|
||||||
|
},
|
||||||
|
Some(session_id),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn dispatch_click(
|
async fn dispatch_click(
|
||||||
client: &CdpClient,
|
client: &CdpClient,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
@@ -955,6 +987,7 @@ async fn dispatch_click(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
wait_for_paint_settled(client, session_id).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user