Date: Thu, 22 Jan 2026 08:58:17 -0500
Subject: [PATCH 25/26] [PM-30889] Remove clone option from archive item
desktop (#18457)
* remove clone option from archive item desktop for users who lose premium status
---
.../src/vault/app/vault/item-footer.component.html | 14 +++++---------
.../src/vault/app/vault/item-footer.component.ts | 13 +++++++++++++
2 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html
index a03f3e96b06..0af73bf7d8a 100644
--- a/apps/desktop/src/vault/app/vault/item-footer.component.html
+++ b/apps/desktop/src/vault/app/vault/item-footer.component.html
@@ -36,15 +36,11 @@
>
-
-
-
+ @if (showCloneOption) {
+
+
+
+ }
Date: Thu, 22 Jan 2026 06:38:26 -0800
Subject: [PATCH 26/26] Desktop Autotype windows integration tests (#17639)
---
.github/workflows/test.yml | 16 +-
.../desktop_native/autotype/Cargo.toml | 9 +
.../autotype/tests/integration_tests.rs | 324 ++++++++++++++++++
3 files changed, 343 insertions(+), 6 deletions(-)
create mode 100644 apps/desktop/desktop_native/autotype/tests/integration_tests.rs
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cf7251b259a..4280cabc812 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/apps/desktop/desktop_native/autotype/Cargo.toml b/apps/desktop/desktop_native/autotype/Cargo.toml
index a9c826af57d..59dd36c6c91 100644
--- a/apps/desktop/desktop_native/autotype/Cargo.toml
+++ b/apps/desktop/desktop_native/autotype/Cargo.toml
@@ -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
diff --git a/apps/desktop/desktop_native/autotype/tests/integration_tests.rs b/apps/desktop/desktop_native/autotype/tests/integration_tests.rs
new file mode 100644
index 00000000000..b87219f77fe
--- /dev/null
+++ b/apps/desktop/desktop_native/autotype/tests/integration_tests.rs
@@ -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,
+}
+
+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>>,
+}
+
+impl InputCapture {
+ fn new() -> Self {
+ Self {
+ chars: Arc::new(Mutex::new(Vec::new())),
+ }
+ }
+
+ fn get_chars(&self) -> Vec {
+ 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;
+
+//
+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 {
+ 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
+ //
+ 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 {
+ 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
+ //
+ 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
+ //
+ 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));
+}