1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Desktop Autotype windows integration tests (#17639)

This commit is contained in:
neuronull
2026-01-22 06:38:26 -08:00
committed by jaasen-livefront
parent db22a558e8
commit f496de02e1
3 changed files with 343 additions and 6 deletions

View File

@@ -111,7 +111,7 @@ jobs:
working-directory: ./apps/desktop/desktop_native
run: cargo build
- name: Test Ubuntu
- name: Linux unit tests
if: ${{ matrix.os=='ubuntu-22.04' }}
working-directory: ./apps/desktop/desktop_native
run: |
@@ -120,17 +120,21 @@ jobs:
mkdir -p ~/.local/share/keyrings
eval "$(printf '\n' | gnome-keyring-daemon --unlock)"
eval "$(printf '\n' | /usr/bin/gnome-keyring-daemon --start)"
cargo test -- --test-threads=1
cargo test --lib -- --test-threads=1
- name: Test macOS
- name: MacOS unit tests
if: ${{ matrix.os=='macos-14' }}
working-directory: ./apps/desktop/desktop_native
run: cargo test -- --test-threads=1
run: cargo test --lib -- --test-threads=1
- name: Test Windows
- name: Windows unit tests
if: ${{ matrix.os=='windows-2022'}}
working-directory: ./apps/desktop/desktop_native
run: cargo test --workspace --exclude=desktop_napi -- --test-threads=1
run: cargo test --lib --workspace --exclude=desktop_napi -- --test-threads=1
- name: Doc tests
working-directory: ./apps/desktop/desktop_native
run: cargo test --doc
rust-coverage:
name: Rust Coverage

View File

@@ -19,5 +19,14 @@ windows-core = { workspace = true }
[dependencies]
anyhow = { workspace = true }
[target.'cfg(windows)'.dev-dependencies]
windows = { workspace = true, features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation",
"Win32_System_LibraryLoader",
"Win32_Graphics_Gdi",
] }
[lints]
workspace = true

View File

@@ -0,0 +1,324 @@
#![cfg(target_os = "windows")]
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
use autotype::{get_foreground_window_title, type_input};
use serial_test::serial;
use tracing::debug;
use windows::Win32::{
Foundation::{COLORREF, HINSTANCE, HMODULE, HWND, LPARAM, LRESULT, WPARAM},
Graphics::Gdi::{CreateSolidBrush, UpdateWindow, ValidateRect, COLOR_WINDOW},
System::LibraryLoader::{GetModuleHandleA, GetModuleHandleW},
UI::WindowsAndMessaging::*,
};
use windows_core::{s, w, Result, PCSTR, PCWSTR};
struct TestWindow {
handle: HWND,
capture: Option<InputCapture>,
}
impl Drop for TestWindow {
fn drop(&mut self) {
// Clean up the InputCapture pointer
unsafe {
let capture_ptr = GetWindowLongPtrW(self.handle, GWLP_USERDATA) as *mut InputCapture;
if !capture_ptr.is_null() {
let _ = Box::from_raw(capture_ptr);
}
CloseWindow(self.handle).expect("window handle should be closeable");
DestroyWindow(self.handle).expect("window handle should be destroyable");
}
}
}
// state to capture keyboard input
#[derive(Clone)]
struct InputCapture {
chars: Arc<Mutex<Vec<char>>>,
}
impl InputCapture {
fn new() -> Self {
Self {
chars: Arc::new(Mutex::new(Vec::new())),
}
}
fn get_chars(&self) -> Vec<char> {
self.chars
.lock()
.expect("mutex should not be poisoned")
.clone()
}
}
// Custom window procedure that captures input
unsafe extern "system" fn capture_input_proc(
handle: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
WM_CREATE => {
// Store the InputCapture pointer in window data
let create_struct = lparam.0 as *const CREATESTRUCTW;
let capture_ptr = (*create_struct).lpCreateParams as *mut InputCapture;
SetWindowLongPtrW(handle, GWLP_USERDATA, capture_ptr as isize);
LRESULT(0)
}
WM_CHAR => {
// Get the InputCapture from window data
let capture_ptr = GetWindowLongPtrW(handle, GWLP_USERDATA) as *mut InputCapture;
if !capture_ptr.is_null() {
let capture = &*capture_ptr;
if let Some(ch) = char::from_u32(wparam.0 as u32) {
capture
.chars
.lock()
.expect("mutex should not be poisoned")
.push(ch);
}
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(handle, msg, wparam, lparam),
}
}
// A pointer to the window procedure
type ProcType = unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT;
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc>
extern "system" fn show_window_proc(
handle: HWND, // the window handle
message: u32, // the system message
wparam: WPARAM, /* additional message information. The contents of the wParam parameter
* depend on the value of the message parameter. */
lparam: LPARAM, /* additional message information. The contents of the lParam parameter
* depend on the value of the message parameter. */
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
debug!("WM_PAINT");
let res = ValidateRect(Some(handle), None);
debug_assert!(res.ok().is_ok());
LRESULT(0)
}
WM_DESTROY => {
debug!("WM_DESTROY");
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcA(handle, message, wparam, lparam),
}
}
}
impl TestWindow {
fn set_foreground(&self) -> Result<()> {
unsafe {
let _ = ShowWindow(self.handle, SW_SHOW);
let _ = SetForegroundWindow(self.handle);
let _ = UpdateWindow(self.handle);
let _ = SetForegroundWindow(self.handle);
}
std::thread::sleep(std::time::Duration::from_millis(100));
Ok(())
}
fn wait_for_input(&self, timeout_ms: u64) {
let start = std::time::Instant::now();
while start.elapsed().as_millis() < timeout_ms as u128 {
process_messages();
thread::sleep(Duration::from_millis(10));
}
}
}
fn process_messages() {
unsafe {
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
fn create_input_window(title: PCWSTR, proc_type: ProcType) -> Result<TestWindow> {
unsafe {
let instance = GetModuleHandleW(None).unwrap_or(HMODULE(std::ptr::null_mut()));
let instance: HINSTANCE = instance.into();
debug_assert!(!instance.is_invalid());
let window_class = w!("show_window");
// Register window class with our custom proc
let wc = WNDCLASSW {
lpfnWndProc: Some(proc_type),
hInstance: instance,
lpszClassName: window_class,
hbrBackground: CreateSolidBrush(COLORREF(
(COLOR_WINDOW.0 + 1).try_into().expect("i32 to fit in u32"),
)),
..Default::default()
};
let _atom = RegisterClassW(&wc);
let capture = InputCapture::new();
// Pass InputCapture as lpParam
let capture_ptr = Box::into_raw(Box::new(capture.clone()));
// Create window
// <https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window>
let handle = CreateWindowExW(
WINDOW_EX_STYLE(0),
window_class,
title,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
400,
300,
None,
None,
Some(instance),
Some(capture_ptr as *const _),
)
.expect("window should be created");
// Process pending messages
process_messages();
thread::sleep(Duration::from_millis(100));
Ok(TestWindow {
handle,
capture: Some(capture),
})
}
}
fn create_title_window(title: PCSTR, proc_type: ProcType) -> Result<TestWindow> {
unsafe {
let instance = GetModuleHandleA(None)?;
let instance: HINSTANCE = instance.into();
debug_assert!(!instance.is_invalid());
let window_class = s!("input_window");
// Register window class with our custom proc
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa>
let wc = WNDCLASSA {
hCursor: LoadCursorW(None, IDC_ARROW)?,
hInstance: instance,
lpszClassName: window_class,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(proc_type),
..Default::default()
};
let _atom = RegisterClassA(&wc);
// Create window
// <https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window>
let handle = CreateWindowExA(
WINDOW_EX_STYLE::default(),
window_class,
title,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
800,
600,
None,
None,
Some(instance),
None,
)
.expect("window should be created");
Ok(TestWindow {
handle,
capture: None,
})
}
}
#[serial]
#[test]
fn test_get_active_window_title_success() {
let title;
{
let window = create_title_window(s!("TITLE_FOOBAR"), show_window_proc).unwrap();
window.set_foreground().unwrap();
title = get_foreground_window_title().unwrap();
}
assert_eq!(title, "TITLE_FOOBAR\0".to_owned());
thread::sleep(Duration::from_millis(100));
}
#[serial]
#[test]
fn test_get_active_window_title_doesnt_fail_if_empty_title() {
let title;
{
let window = create_title_window(s!(""), show_window_proc).unwrap();
window.set_foreground().unwrap();
title = get_foreground_window_title();
}
assert_eq!(title.unwrap(), "".to_owned());
thread::sleep(Duration::from_millis(100));
}
#[serial]
#[test]
fn test_type_input_success() {
const TAB: u16 = 0x09;
let chars;
{
let window = create_input_window(w!("foo"), capture_input_proc).unwrap();
window.set_foreground().unwrap();
type_input(
&[
0x66, 0x6F, 0x6C, 0x6C, 0x6F, 0x77, 0x5F, 0x74, 0x68, 0x65, TAB, 0x77, 0x68, 0x69,
0x74, 0x65, 0x5F, 0x72, 0x61, 0x62, 0x62, 0x69, 0x74,
],
&["Control".to_owned(), "Alt".to_owned(), "B".to_owned()],
)
.unwrap();
// Wait for and process input messages
window.wait_for_input(250);
// Verify captured input
let capture = window.capture.as_ref().unwrap();
chars = capture.get_chars();
}
assert!(!chars.is_empty(), "No input captured");
let input_str = String::from_iter(chars.iter());
let input_str = input_str.replace("\t", "_");
assert_eq!(input_str, "follow_the_white_rabbit");
thread::sleep(Duration::from_millis(100));
}