Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions packages/accessibility-core/src/platform/msft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ impl WindowsAccessibility {
// Then bring window to foreground
let _ = unsafe { SetForegroundWindow(hwnd) };

// Small delay to let focus settle

Ok(())
}

Expand Down Expand Up @@ -943,9 +941,55 @@ impl AccessibilityReader for WindowsAccessibility {
y: f64,
button: MouseButton,
) -> Result<()> {
// Windows doesn't support process-targeted input like macOS, so pid is ignored
self.mouse_move(None, x, y).await?;
self.mouse_click_internal(button)
// Send move + down + up as one atomic `SendInput` batch with absolute
// coordinates on every event. Separate calls are flaky on UWP hosts
// because the OS can coalesce or reorder them, dispatching the down
// event before the cursor-tracking state has caught up.
let screen_width = unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) } as f64;
let screen_height = unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) } as f64;
let screen_x = unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) } as f64;
let screen_y = unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) } as f64;
if screen_width <= 0.0 || screen_height <= 0.0 {
bail!(
"Virtual desktop reports non-positive dimensions ({} x {})",
screen_width,
screen_height
);
}

let norm_x = ((x - screen_x) * 65535.0 / screen_width) as i32;
let norm_y = ((y - screen_y) * 65535.0 / screen_height) as i32;
let abs_flags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
let (down_flag, up_flag) = match button {
MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
};

let make = |flags| INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: norm_x,
dy: norm_y,
mouseData: 0,
dwFlags: flags | abs_flags,
time: 0,
dwExtraInfo: 0,
},
},
};
let inputs = [make(MOUSEEVENTF_MOVE), make(down_flag), make(up_flag)];

let inserted = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
if inserted as usize != inputs.len() {
bail!(
"SendInput inserted {}/{} mouse events",
inserted,
inputs.len()
);
}
Ok(())
}

async fn press_key(&mut self, _pid: Option<u32>, key: Code) -> Result<()> {
Expand Down
113 changes: 112 additions & 1 deletion packages/accessibility-core/tests/calculator_windows_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,93 @@ use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::mpsc;
use windows::Win32::Foundation::{HWND, LPARAM, POINT};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GA_ROOT, GetAncestor, GetClassNameW, GetWindowTextW, IsWindowVisible, SW_HIDE,
ShowWindow, WindowFromPoint,
};

// ============================================================================
// CI-only blocker dismissal
// ============================================================================
//
// The windows-11-arm runner image floats OOBE / "Microsoft account" sign-in
// windows above the desktop that intercept synthetic clicks at calculator's
// coordinates. They come from a few processes (Shell_OOBEProxy host,
// UserOOBEWindowClass with empty title, Windows.UI.Core.CoreWindow titled
// "Microsoft account") and uncover each other as we hide them. We hide them
// (rather than killing the host process) so the OS doesn't immediately respawn
// a fresh popup. Two passes — one over all top-level windows, one driven by
// what's actually under the click pixel — to handle the layered z-order.

/// A window matches if its title equals any string in `titles` OR its class
/// equals any string in `classes`.
struct BlockerSpec<'a> {
titles: &'a [&'a str],
classes: &'a [&'a str],
}

fn window_class(hwnd: HWND) -> String {
let mut buf = [0u16; 256];
let len = unsafe { GetClassNameW(hwnd, &mut buf) } as usize;
String::from_utf16_lossy(&buf[..len])
}

fn window_title(hwnd: HWND) -> String {
let mut buf = [0u16; 256];
let len = unsafe { GetWindowTextW(hwnd, &mut buf) } as usize;
String::from_utf16_lossy(&buf[..len])
}

fn matches_blocker(hwnd: HWND, spec: &BlockerSpec<'_>) -> bool {
spec.titles.iter().any(|t| *t == window_title(hwnd))
|| spec.classes.iter().any(|c| *c == window_class(hwnd))
}

/// Hide every visible top-level window matching `spec`.
fn hide_top_level_blockers(spec: &BlockerSpec<'_>) -> usize {
struct Ctx<'a> {
spec: &'a BlockerSpec<'a>,
hidden: usize,
}
let mut ctx = Ctx { spec, hidden: 0 };
unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> windows::core::BOOL {
let ctx = unsafe { &mut *(lparam.0 as *mut Ctx) };
if unsafe { IsWindowVisible(hwnd).as_bool() } && matches_blocker(hwnd, ctx.spec) {
let _ = unsafe { ShowWindow(hwnd, SW_HIDE) };
ctx.hidden += 1;
}
true.into()
}
let lparam = LPARAM(&mut ctx as *mut _ as isize);
let _ = unsafe { EnumWindows(Some(enum_proc), lparam) };
ctx.hidden
}

/// Repeatedly probe the window directly under `(x, y)` and hide its top-level
/// root if it matches `spec`. Stops once the window at the point is no longer
/// a blocker, or after six attempts.
fn hide_blockers_at_point(x: f64, y: f64, spec: &BlockerSpec<'_>) -> usize {
let pt = POINT {
x: x as i32,
y: y as i32,
};
let mut hidden = 0;
for _ in 0..6 {
let hwnd = unsafe { WindowFromPoint(pt) };
if hwnd.is_invalid() {
break;
}
let root = unsafe { GetAncestor(hwnd, GA_ROOT) };
let to_hide = if root.is_invalid() { hwnd } else { root };
if !matches_blocker(to_hide, spec) && !matches_blocker(hwnd, spec) {
break;
}
let _ = unsafe { ShowWindow(to_hide, SW_HIDE) };
hidden += 1;
}
hidden
}

// ============================================================================
// Helper Functions
Expand Down Expand Up @@ -444,7 +531,31 @@ async fn test_calculator_mouse_click() {
input
.focus_window(calc.pid)
.expect("Failed to focus Calculator");
tokio::time::sleep(Duration::from_millis(200)).await;

// The windows-11-arm runner image floats OOBE / "Microsoft account"
// sign-in windows above the desktop that intercept synthetic clicks at
// calculator's coordinates. They come in several flavours from a few
// processes (Shell_OOBEProxy, UserOOBEWindowClass with empty title,
// Windows.UI.Core.CoreWindow titled "Microsoft account") and uncover
// each other as we hide them. Match by title OR class to catch the
// empty-title OOBE frame, and combine an EnumWindows pass with a
// point-driven pass that hides whatever's actually under the click
// pixel. ShowWindow(SW_HIDE) keeps the host alive so the OS doesn't
// respawn a fresh popup.
let blockers = BlockerSpec {
titles: &["Microsoft account"],
classes: &["Shell_OOBEProxy", "UserOOBEWindowClass"],
};
let pre_hidden = hide_top_level_blockers(&blockers);
let point_hidden = hide_blockers_at_point(center.x, center.y, &blockers);
if pre_hidden + point_hidden > 0 {
println!(
"Hid {} blocker popup(s) before click ({} via enum, {} at click point)",
pre_hidden + point_hidden,
pre_hidden,
point_hidden
);
}

input
.mouse_click_at(None, center.x, center.y, MouseButton::Left)
Expand Down
Loading