Skip to content

Commit be21beb

Browse files
committed
E2E: open windows without stealing OS focus
- Viewer and Settings windows open with `focus: false` when running under `CMDR_E2E_MODE=1`. The plugin drives the DOM via the Unix socket and doesn't need OS focus. - Settings re-open path skips the `focus-self` event in E2E too, so reopening an already-open Settings window doesn't grab focus either. - macOS `show_main_window` uses `[NSWindow orderFront:]` instead of `makeKeyAndOrderFront:` in E2E, so the initial main-window appearance doesn't steal focus from whatever the user is working in. - Prod/dev behavior unchanged.
1 parent 9693b28 commit be21beb

3 files changed

Lines changed: 35 additions & 5 deletions

File tree

apps/desktop/src-tauri/src/commands/ui.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ pub fn show_breadcrumb_context_menu<R: Runtime>(window: Window<R>, shortcut: Str
118118
#[tauri::command]
119119
#[specta::specta]
120120
pub fn show_main_window<R: Runtime>(window: Window<R>) -> Result<(), String> {
121+
// E2E: on macOS, use `orderFront:` instead of `makeKeyAndOrderFront:` so the
122+
// window appears without stealing focus from whatever the user is currently
123+
// working in. `window.show()` calls the latter on macOS, which always grabs
124+
// OS focus. Linux/Windows test runs happen in headless containers, so the
125+
// standard show is fine there.
126+
#[cfg(target_os = "macos")]
127+
if crate::test_mode::is_e2e_mode() {
128+
use objc2::msg_send;
129+
use objc2::runtime::AnyObject;
130+
let ns_window = window.ns_window().map_err(|e| e.to_string())? as *mut AnyObject;
131+
if ns_window.is_null() {
132+
return Err("NSWindow pointer is null".into());
133+
}
134+
unsafe {
135+
let _: () = msg_send![ns_window, orderFront: std::ptr::null_mut::<AnyObject>()];
136+
}
137+
return Ok(());
138+
}
121139
window.show().map_err(|e| e.to_string())
122140
}
123141

apps/desktop/src/lib/file-viewer/open-viewer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/** Opens a file viewer window for the given file path. Multiple viewers can be open at once. */
22
export async function openFileViewer(filePath: string): Promise<void> {
33
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
4-
const { decorateChildWindowTitle } = await import('$lib/app-mode')
4+
const { decorateChildWindowTitle, getAppMode } = await import('$lib/app-mode')
55

66
// Use a unique label per viewer instance (timestamp-based)
77
const label = `viewer-${String(Date.now())}`
88
const encodedPath = encodeURIComponent(filePath)
99

10+
// E2E suites open viewer windows repeatedly; stealing OS focus each time
11+
// makes the host machine unusable while tests run. The plugin reaches the
12+
// webview over a Unix socket, so it doesn't need OS focus to drive the DOM.
13+
const isE2e = getAppMode() === 'e2e'
14+
1015
new WebviewWindow(label, {
1116
url: `/viewer?path=${encodedPath}`,
1217
title: decorateChildWindowTitle(filePath.split('/').pop() ?? 'Viewer'),
@@ -18,6 +23,6 @@ export async function openFileViewer(filePath: string): Promise<void> {
1823
minimizable: true,
1924
maximizable: true,
2025
closable: true,
21-
focus: true,
26+
focus: !isE2e,
2227
})
2328
}

apps/desktop/src/lib/settings/settings-window.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
2121
import { emitTo } from '@tauri-apps/api/event'
2222
import { getAppLogger } from '$lib/logging/logger'
2323
import { getEffectiveScale } from '$lib/text-size.svelte'
24-
import { decorateChildWindowTitle } from '$lib/app-mode'
24+
import { decorateChildWindowTitle, getAppMode } from '$lib/app-mode'
2525

2626
const log = getAppLogger('settings')
2727

@@ -54,11 +54,18 @@ export const settingsMaxWidth = (scale: number): number =>
5454
* the matching section path (e.g., `['File systems', 'SMB/Network shares']`).
5555
*/
5656
export async function openSettingsWindow(section?: string[]): Promise<void> {
57+
// E2E suites re-open Settings many times; stealing OS focus each time
58+
// makes the host machine unusable while tests run. The plugin reaches the
59+
// webview over a Unix socket, so it doesn't need OS focus to drive the DOM.
60+
const isE2e = getAppMode() === 'e2e'
61+
5762
const existing = await WebviewWindow.getByLabel('settings')
5863
if (existing) {
5964
// Emit to the settings window so it can self-focus. Cross-window setFocus()
6065
// doesn't reliably bring a window to front on macOS.
61-
await emitTo('settings', 'focus-self')
66+
if (!isE2e) {
67+
await emitTo('settings', 'focus-self')
68+
}
6269
if (section) {
6370
await emitTo('settings', 'navigate-to-section', { section })
6471
}
@@ -84,6 +91,6 @@ export async function openSettingsWindow(section?: string[]): Promise<void> {
8491
center: true,
8592
resizable: true,
8693
decorations: true,
87-
focus: true,
94+
focus: !isE2e,
8895
})
8996
}

0 commit comments

Comments
 (0)