1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Add desktop autotype unittests for windows (#16710)

* Add desktop autotype unittests for windows

* lint

* fix TODO comment

* feedback coltonhurst: rename trait
This commit is contained in:
neuronull
2025-10-23 09:42:48 -07:00
committed by GitHub
parent 660e452ba1
commit 2c13236550
6 changed files with 565 additions and 145 deletions

View File

@@ -343,6 +343,8 @@ name = "autotype"
version = "0.0.0"
dependencies = [
"anyhow",
"mockall",
"serial_test",
"tracing",
"windows 0.61.1",
"windows-core 0.61.0",
@@ -1070,6 +1072,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "downcast"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -1288,6 +1296,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fragile"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
[[package]]
name = "fs-err"
version = "2.11.0"
@@ -1943,6 +1957,32 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "mockall"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"mockall_derive",
"predicates",
"predicates-tree",
]
[[package]]
name = "mockall_derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi"
version = "2.16.17"
@@ -2575,6 +2615,32 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "primeorder"
version = "0.13.6"
@@ -2877,6 +2943,15 @@ dependencies = [
"cipher",
]
[[package]]
name = "scc"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
dependencies = [
"sdd",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2920,6 +2995,12 @@ dependencies = [
"sha2",
]
[[package]]
name = "sdd"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "sec1"
version = "0.7.3"
@@ -3024,6 +3105,31 @@ dependencies = [
"syn",
]
[[package]]
name = "serial_test"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
dependencies = [
"futures",
"log",
"once_cell",
"parking_lot",
"scc",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -3263,6 +3369,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "textwrap"
version = "0.16.2"

View File

@@ -9,6 +9,8 @@ publish.workspace = true
anyhow = { workspace = true }
[target.'cfg(windows)'.dependencies]
mockall = "=0.13.1"
serial_test = "=3.2.0"
tracing.workspace = true
windows = { workspace = true, features = [
"Win32_UI_Input_KeyboardAndMouse",

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "macos", path = "macos.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_os = "windows", path = "windows/mod.rs")]
mod windowing;
/// Gets the title bar string for the foreground window.
@@ -20,12 +20,13 @@ pub fn get_foreground_window_title() -> Result<String> {
///
/// # Arguments
///
/// * `input` must be an array of utf-16 encoded characters to insert.
/// * `input` an array of utf-16 encoded characters to insert.
/// * `keyboard_shortcut` a vector of valid shortcut keys: Control, Alt, Super, Shift, letters a - Z
///
/// # Errors
///
/// This function returns an `anyhow::Error` if there is any
/// issue obtaining the window title. Detailed reasons will
/// issue in typing the input. Detailed reasons will
/// vary based on platform implementation.
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
windowing::type_input(input, keyboard_shortcut)

View File

@@ -0,0 +1,41 @@
use anyhow::Result;
use tracing::debug;
use windows::Win32::Foundation::{GetLastError, SetLastError, WIN32_ERROR};
mod type_input;
mod window_title;
/// The error code from Win32 API that represents a non-error.
const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
/// `ErrorOperations` provides an interface to the Win32 API for dealing with
/// win32 errors.
#[cfg_attr(test, mockall::automock)]
trait ErrorOperations {
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror
fn set_last_error(err: u32) {
debug!(err, "Calling SetLastError");
unsafe {
SetLastError(WIN32_ERROR(err));
}
}
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
fn get_last_error() -> WIN32_ERROR {
let last_err = unsafe { GetLastError() };
debug!("GetLastError(): {}", last_err.to_hresult().message());
last_err
}
}
/// Default implementation for Win32 API errors.
struct Win32ErrorOperations;
impl ErrorOperations for Win32ErrorOperations {}
pub fn get_foreground_window_title() -> Result<String> {
window_title::get_foreground_window_title()
}
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
type_input::type_input(input, keyboard_shortcut)
}

View File

@@ -1,136 +1,42 @@
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
use anyhow::{anyhow, Result};
use tracing::{debug, error, warn};
use windows::Win32::{
Foundation::{GetLastError, SetLastError, HWND, WIN32_ERROR},
UI::{
Input::KeyboardAndMouse::{
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP,
KEYEVENTF_UNICODE, VIRTUAL_KEY,
},
WindowsAndMessaging::{GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW},
},
use tracing::{debug, error};
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE,
VIRTUAL_KEY,
};
const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
use super::{ErrorOperations, Win32ErrorOperations};
fn clear_last_error() {
debug!("Clearing last error with SetLastError.");
unsafe {
SetLastError(WIN32_ERROR(0));
}
}
fn get_last_error() -> WIN32_ERROR {
let last_err = unsafe { GetLastError() };
debug!("GetLastError(): {}", last_err.to_hresult().message());
last_err
}
// The handle should be validated before any unsafe calls referencing it.
fn validate_window_handle(handle: &HWND) -> Result<()> {
if handle.is_invalid() {
error!("Window handle is invalid.");
return Err(anyhow!("Window handle is invalid."));
}
Ok(())
}
// ---------- Window title --------------
/// Gets the title bar string for the foreground window.
pub fn get_foreground_window_title() -> Result<String> {
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
let window_handle = unsafe { GetForegroundWindow() };
debug!("GetForegroundWindow() called.");
validate_window_handle(&window_handle)?;
get_window_title(&window_handle)
}
/// Gets the length of the window title bar text.
/// `InputOperations` provides an interface to Window32 API for
/// working with inputs.
#[cfg_attr(test, mockall::automock)]
trait InputOperations {
/// Attempts to type the provided input wherever the user's cursor is.
///
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
fn get_window_title_length(window_handle: &HWND) -> Result<usize> {
// GetWindowTextLengthW does not itself clear the last error so we must do it ourselves.
clear_last_error();
validate_window_handle(window_handle)?;
let length = unsafe { GetWindowTextLengthW(*window_handle) };
let length = usize::try_from(length)?;
debug!(length, "window text length retrieved from handle.");
if length == 0 {
// attempt to retreive win32 error
let last_err = get_last_error();
if last_err != WIN32_SUCCESS {
let last_err = last_err.to_hresult().message();
error!(last_err, "Error getting window text length.");
return Err(anyhow!("Error getting window text length: {last_err}"));
}
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
fn send_input(inputs: &[INPUT]) -> u32;
}
Ok(length)
struct Win32InputOperations;
impl InputOperations for Win32InputOperations {
fn send_input(inputs: &[INPUT]) -> u32 {
const INPUT_STRUCT_SIZE: i32 = std::mem::size_of::<INPUT>() as i32;
let insert_count = unsafe { SendInput(inputs, INPUT_STRUCT_SIZE) };
debug!(insert_count, "SendInput() called.");
insert_count
}
/// Gets the window title bar title.
///
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
fn get_window_title(window_handle: &HWND) -> Result<String> {
let expected_window_title_length = get_window_title_length(window_handle)?;
// This isn't considered an error by the windows API, but in practice it means we can't
// match against the title so we'll stop here.
// The upstream will make a contains comparison on what we return, so an empty string
// will not result on a match.
if expected_window_title_length == 0 {
warn!("Window title length is zero.");
return Ok(String::from(""));
}
let mut buffer: Vec<u16> = vec![0; expected_window_title_length + 1]; // add extra space for the null character
validate_window_handle(window_handle)?;
let actual_window_title_length = unsafe { GetWindowTextW(*window_handle, &mut buffer) };
debug!(actual_window_title_length, "window title retrieved.");
if actual_window_title_length == 0 {
// attempt to retreive win32 error
let last_err = get_last_error();
if last_err != WIN32_SUCCESS {
let last_err = last_err.to_hresult().message();
error!(last_err, "Error retrieving window title.");
return Err(anyhow!("Error retrieving window title. {last_err}"));
}
// in practice, we should not get to the below code, since we asserted the len > 0
// above. but it is an extra protection in case the windows API didn't set an error.
warn!(expected_window_title_length, "No window title retrieved.");
}
let window_title = OsString::from_wide(&buffer);
Ok(window_title.to_string_lossy().into_owned())
}
// ---------- Type Input --------------
/// Attempts to type the input text wherever the user's cursor is.
///
/// `input` must be a vector of utf-16 encoded characters to insert.
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z
///
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
const TAB_KEY: u8 = 9;
pub(super) fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
// the length of this vec is always shortcut keys to release + (2x length of input chars)
let mut keyboard_inputs: Vec<INPUT> =
Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2));
@@ -142,25 +48,31 @@ pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()>
keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?);
}
add_input(&input, &mut keyboard_inputs);
send_input::<Win32InputOperations, Win32ErrorOperations>(keyboard_inputs)
}
// Add key "down" and "up" inputs for the input
// (currently in this form: {username}/t{password})
fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
const TAB_KEY: u8 = 9;
for i in input {
let next_down_input = if i == TAB_KEY.into() {
build_virtual_key_input(InputKeyPress::Down, i as u8)
let next_down_input = if *i == TAB_KEY.into() {
build_virtual_key_input(InputKeyPress::Down, *i as u8)
} else {
build_unicode_input(InputKeyPress::Down, i)
build_unicode_input(InputKeyPress::Down, *i)
};
let next_up_input = if i == TAB_KEY.into() {
build_virtual_key_input(InputKeyPress::Up, i as u8)
let next_up_input = if *i == TAB_KEY.into() {
build_virtual_key_input(InputKeyPress::Up, *i as u8)
} else {
build_unicode_input(InputKeyPress::Up, i)
build_unicode_input(InputKeyPress::Up, *i)
};
keyboard_inputs.push(next_down_input);
keyboard_inputs.push(next_up_input);
}
send_input(keyboard_inputs)
}
/// Converts a valid shortcut key to an "up" keyboard input.
@@ -294,21 +206,20 @@ fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
}
}
/// Attempts to type the provided input wherever the user's cursor is.
///
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
fn send_input(inputs: Vec<INPUT>) -> Result<()> {
let insert_count = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
debug!("SendInput() called.");
fn send_input<I, E>(inputs: Vec<INPUT>) -> Result<()>
where
I: InputOperations,
E: ErrorOperations,
{
let insert_count = I::send_input(&inputs);
if insert_count == 0 {
let last_err = get_last_error().to_hresult().message();
let last_err = E::get_last_error().to_hresult().message();
error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread.");
return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}"));
} else if insert_count != inputs.len() as u32 {
let last_err = get_last_error().to_hresult().message();
let last_err = E::get_last_error().to_hresult().message();
error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err,
"SendInput sent does not match expected."
);
@@ -318,17 +229,23 @@ fn send_input(inputs: Vec<INPUT>) -> Result<()> {
));
}
debug!(insert_count, "Autotype sent input.");
Ok(())
}
#[cfg(test)]
mod tests {
//! For the mocking of the traits that are static methods, we need to use the `serial_test` crate
//! in order to mock those, since the mock expectations set have to be global in absence of a `self`.
//! More info: https://docs.rs/mockall/latest/mockall/#static-methods
use super::*;
use crate::windowing::MockErrorOperations;
use serial_test::serial;
use windows::Win32::Foundation::WIN32_ERROR;
#[test]
fn get_alphabetic_hot_key_happy() {
fn get_alphabetic_hot_key_succeeds() {
for c in ('a'..='z').chain('A'..='Z') {
let letter = c.to_string();
let converted = get_alphabetic_hotkey(letter).unwrap();
@@ -349,4 +266,53 @@ mod tests {
let letter = String::from("}");
get_alphabetic_hotkey(letter).unwrap();
}
#[test]
#[serial]
fn send_input_succeeds() {
let ctxi = MockInputOperations::send_input_context();
ctxi.expect().returning(|_| 1);
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
InputKeyPress::Up,
0,
)])
.unwrap();
}
#[test]
#[serial]
#[should_panic(
expected = "SendInput sent 0 inputs. Input was blocked by another thread. GetLastError:"
)]
fn send_input_fails_sent_zero() {
let ctxi = MockInputOperations::send_input_context();
ctxi.expect().returning(|_| 0);
let ctxge = MockErrorOperations::get_last_error_context();
ctxge.expect().returning(|| WIN32_ERROR(1));
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
InputKeyPress::Up,
0,
)])
.unwrap();
}
#[test]
#[serial]
#[should_panic(expected = "SendInput does not match expected. sent: 2, expected: 1")]
fn send_input_fails_sent_mismatch() {
let ctxi = MockInputOperations::send_input_context();
ctxi.expect().returning(|_| 2);
let ctxge = MockErrorOperations::get_last_error_context();
ctxge.expect().returning(|| WIN32_ERROR(1));
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
InputKeyPress::Up,
0,
)])
.unwrap();
}
}

View File

@@ -0,0 +1,298 @@
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
use anyhow::{anyhow, Result};
use tracing::{debug, error, warn};
use windows::Win32::{
Foundation::HWND,
UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW},
};
use super::{ErrorOperations, Win32ErrorOperations, WIN32_SUCCESS};
#[cfg_attr(test, mockall::automock)]
trait WindowHandleOperations {
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
fn get_window_text_length_w(&self) -> Result<i32>;
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
fn get_window_text_w(&self, buffer: &mut Vec<u16>) -> Result<i32>;
}
/// `WindowHandle` provides a light wrapper over the `HWND` (which is just a void *).
/// The raw pointer can become invalid during runtime so it's validity must be checked
/// before usage.
struct WindowHandle {
handle: HWND,
}
impl WindowHandle {
/// Create a new `WindowHandle`
fn new(handle: HWND) -> Self {
Self { handle }
}
/// Assert that the raw pointer is valid.
fn validate(&self) -> Result<()> {
if self.handle.is_invalid() {
error!("Window handle is invalid.");
return Err(anyhow!("Window handle is invalid."));
}
Ok(())
}
}
impl WindowHandleOperations for WindowHandle {
fn get_window_text_length_w(&self) -> Result<i32> {
self.validate()?;
let length = unsafe { GetWindowTextLengthW(self.handle) };
Ok(length)
}
fn get_window_text_w(&self, buffer: &mut Vec<u16>) -> Result<i32> {
self.validate()?;
let len_written = unsafe { GetWindowTextW(self.handle, buffer) };
Ok(len_written)
}
}
/// Gets the title bar string for the foreground window.
pub(super) fn get_foreground_window_title() -> Result<String> {
let window_handle = get_foreground_window_handle()?;
let expected_window_title_length =
get_window_title_length::<WindowHandle, Win32ErrorOperations>(&window_handle)?;
get_window_title::<WindowHandle, Win32ErrorOperations>(
&window_handle,
expected_window_title_length,
)
}
/// Retrieves the foreground window handle and validates it.
fn get_foreground_window_handle() -> Result<WindowHandle> {
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
let handle = unsafe { GetForegroundWindow() };
debug!("GetForegroundWindow() called.");
let window_handle = WindowHandle::new(handle);
window_handle.validate()?;
Ok(window_handle)
}
/// # Returns
///
/// The length of the window title.
///
/// # Errors
///
/// - If the length zero and GetLastError() != 0, return the GetLastError() message.
fn get_window_title_length<H, E>(window_handle: &H) -> Result<usize>
where
H: WindowHandleOperations,
E: ErrorOperations,
{
// GetWindowTextLengthW does not itself clear the last error so we must do it ourselves.
E::set_last_error(0);
let length = window_handle.get_window_text_length_w()?;
let length = usize::try_from(length)?;
debug!(length, "window text length retrieved from handle.");
if length == 0 {
// attempt to retreive win32 error
let last_err = E::get_last_error();
if last_err != WIN32_SUCCESS {
let last_err = last_err.to_hresult().message();
error!(last_err, "Error getting window text length.");
return Err(anyhow!("Error getting window text length: {last_err}"));
}
}
Ok(length)
}
/// Gets the window title bar title using the expected length to determine size of buffer
/// to store it.
///
/// # Returns
///
/// If the `expected_title_length` is zero, return an Ok result containing empty string. It
/// Isn't considered an error by the Win32 API.
///
/// Otherwise, return the retrieved window title string.
///
/// # Errors
///
/// - If the actual window title length (what the win32 API declares was written into the
/// buffer), is length zero and GetLastError() != 0 , return the GetLastError() message.
fn get_window_title<H, E>(window_handle: &H, expected_title_length: usize) -> Result<String>
where
H: WindowHandleOperations,
E: ErrorOperations,
{
if expected_title_length == 0 {
// This isn't considered an error by the windows API, but in practice it means we can't
// match against the title so we'll stop here.
// The upstream will make a contains comparison on what we return, so an empty string
// will not result on a match.
warn!("Window title length is zero.");
return Ok(String::from(""));
}
let mut buffer: Vec<u16> = vec![0; expected_title_length + 1]; // add extra space for the null character
let actual_window_title_length = window_handle.get_window_text_w(&mut buffer)?;
debug!(actual_window_title_length, "window title retrieved.");
if actual_window_title_length == 0 {
// attempt to retreive win32 error
let last_err = E::get_last_error();
if last_err != WIN32_SUCCESS {
let last_err = last_err.to_hresult().message();
error!(last_err, "Error retrieving window title.");
return Err(anyhow!("Error retrieving window title: {last_err}"));
}
// in practice, we should not get to the below code, since we asserted the len > 0
// above. but it is an extra protection in case the windows API didn't set an error.
warn!(expected_title_length, "No window title retrieved.");
}
let window_title = OsString::from_wide(&buffer);
Ok(window_title.to_string_lossy().into_owned())
}
#[cfg(test)]
mod tests {
//! For the mocking of the traits that are static methods, we need to use the `serial_test` crate
//! in order to mock those, since the mock expectations set have to be global in absence of a `self`.
//! More info: https://docs.rs/mockall/latest/mockall/#static-methods
use super::*;
use crate::windowing::MockErrorOperations;
use mockall::predicate;
use serial_test::serial;
use windows::Win32::Foundation::WIN32_ERROR;
#[test]
#[serial]
fn get_window_title_length_can_be_zero() {
let mut mock_handle = MockWindowHandleOperations::new();
let ctxse = MockErrorOperations::set_last_error_context();
ctxse
.expect()
.once()
.with(predicate::eq(0))
.returning(|_| {});
mock_handle
.expect_get_window_text_length_w()
.once()
.returning(|| Ok(0));
let ctxge = MockErrorOperations::get_last_error_context();
ctxge.expect().returning(|| WIN32_ERROR(0));
let len = get_window_title_length::<MockWindowHandleOperations, MockErrorOperations>(
&mock_handle,
)
.unwrap();
assert_eq!(len, 0);
}
#[test]
#[serial]
#[should_panic(expected = "Error getting window text length:")]
fn get_window_title_length_fails() {
let mut mock_handle = MockWindowHandleOperations::new();
let ctxse = MockErrorOperations::set_last_error_context();
ctxse.expect().with(predicate::eq(0)).returning(|_| {});
mock_handle
.expect_get_window_text_length_w()
.once()
.returning(|| Ok(0));
let ctxge = MockErrorOperations::get_last_error_context();
ctxge.expect().returning(|| WIN32_ERROR(1));
get_window_title_length::<MockWindowHandleOperations, MockErrorOperations>(&mock_handle)
.unwrap();
}
#[test]
fn get_window_title_succeeds() {
let mut mock_handle = MockWindowHandleOperations::new();
mock_handle
.expect_get_window_text_w()
.once()
.returning(|buffer| {
buffer.fill_with(|| 42); // because why not
Ok(42)
});
let title =
get_window_title::<MockWindowHandleOperations, MockErrorOperations>(&mock_handle, 42)
.unwrap();
assert_eq!(title.len(), 43); // That extra slot in the buffer for null char
assert_eq!(title, "*******************************************");
}
#[test]
fn get_window_title_returns_empty_string() {
let mock_handle = MockWindowHandleOperations::new();
let title =
get_window_title::<MockWindowHandleOperations, MockErrorOperations>(&mock_handle, 0)
.unwrap();
assert_eq!(title, "");
}
#[test]
#[serial]
#[should_panic(expected = "Error retrieving window title:")]
fn get_window_title_fails_with_last_error() {
let mut mock_handle = MockWindowHandleOperations::new();
mock_handle
.expect_get_window_text_w()
.once()
.returning(|_| Ok(0));
let ctxge = MockErrorOperations::get_last_error_context();
ctxge.expect().returning(|| WIN32_ERROR(1));
get_window_title::<MockWindowHandleOperations, MockErrorOperations>(&mock_handle, 42)
.unwrap();
}
#[test]
#[serial]
fn get_window_title_doesnt_fail_but_reads_zero() {
let mut mock_handle = MockWindowHandleOperations::new();
mock_handle
.expect_get_window_text_w()
.once()
.returning(|_| Ok(0));
let ctxge = MockErrorOperations::get_last_error_context();
ctxge.expect().returning(|| WIN32_ERROR(0));
get_window_title::<MockWindowHandleOperations, MockErrorOperations>(&mock_handle, 42)
.unwrap();
}
}