561a547b70
Menu-bar utility that fixes cross-display keyboard focus loss: a listen-only CGEvent tap locates the window under the cursor and reasserts app activation + AX focus on it, with debounce, tap auto-recovery, and an Accessibility permission gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
3.6 KiB
Markdown
98 lines
3.6 KiB
Markdown
# Manual test checklist — focus behaviour
|
|
|
|
The event tap and Accessibility side-effects can't be unit tested. Run this
|
|
checklist by hand after any change to `EventTapController`, `WindowLocator`,
|
|
or `FocusEnforcer`, and whenever behaviour changes update it alongside the
|
|
code.
|
|
|
|
Prerequisites: FocusFixer built and running as a `.app` bundle (not via
|
|
`swift run`), Accessibility permission granted in System Settings →
|
|
Privacy & Security → Accessibility.
|
|
|
|
## Basic cross-display focus
|
|
|
|
1. Open a text editor window on display A and a terminal window on display B.
|
|
2. Click into the terminal on display B, then click the text editor on
|
|
display A.
|
|
3. Type immediately after the click. Confirm keystrokes land in the text
|
|
editor, not the terminal.
|
|
4. Repeat in the opposite direction (A → B).
|
|
|
|
## DisplayLink virtual display
|
|
|
|
1. Connect a DisplayLink virtual display (or enable one if already paired).
|
|
2. Repeat the "Basic cross-display focus" steps with one of the two windows
|
|
on the virtual display.
|
|
3. Confirm focus follows clicks with no perceptible lag and no dropped
|
|
keystrokes.
|
|
|
|
## No focus thrashing on the active app
|
|
|
|
1. Click repeatedly inside the already-frontmost window (same app, same
|
|
display).
|
|
2. Confirm no visible activation flicker and no window re-raise animation —
|
|
`NSRunningApplication.isActive` should short-circuit these clicks.
|
|
|
|
## Debounce
|
|
|
|
1. Rapidly double-click and drag-select across two different windows.
|
|
2. Confirm only one focus reassertion happens per interaction, not one per
|
|
raw mouse-down.
|
|
|
|
## Excluded apps
|
|
|
|
1. Add an app's bundle ID to the excluded list (`excludedBundleIDs` in
|
|
`UserDefaults` for `com.local.focusfixer`).
|
|
2. Click into that app's window from another display.
|
|
3. Confirm FocusFixer does not activate or raise it.
|
|
|
|
## System UI is left alone
|
|
|
|
1. Click the Dock, a Spotlight search result, and Control Center.
|
|
2. Confirm none of these trigger FocusFixer activation/raise logic (check
|
|
logs — see below — for absence of unexpected activation entries).
|
|
|
|
## Self-activation guard
|
|
|
|
1. Click the FocusFixer menu-bar icon.
|
|
2. Confirm FocusFixer never tries to activate/raise itself (it has no
|
|
normal windows, but verify no AX errors logged for its own PID).
|
|
|
|
## Tap recovery
|
|
|
|
1. Trigger sustained system load (e.g. a busy `yes > /dev/null` loop or a
|
|
heavy build) to risk `kCGEventTapDisabledByTimeout`.
|
|
2. Continue clicking across displays during the load.
|
|
3. Confirm focus reassertion keeps working — tail the logs (below) for a
|
|
"re-enabling" message, which indicates recovery kicked in.
|
|
|
|
## Permission revoked mid-session
|
|
|
|
1. While FocusFixer is running, run:
|
|
`tccutil reset Accessibility com.local.focusfixer`
|
|
2. Click across displays; confirm FocusFixer degrades silently (no dialog,
|
|
no crash) and logs the AX failures.
|
|
3. Re-grant Accessibility permission in System Settings and confirm
|
|
FocusFixer resumes without a relaunch (permission polling should pick it
|
|
back up if the app is still waiting; otherwise relaunch and confirm the
|
|
prompt reappears correctly).
|
|
|
|
## Toggling from the menu
|
|
|
|
1. Use the menu-bar "Enabled" checkbox to disable FocusFixer.
|
|
2. Confirm clicks across displays no longer reassert focus.
|
|
3. Re-enable and confirm behaviour resumes.
|
|
4. Toggle "Launch at Login", quit and relog (or use
|
|
`sfltool` / System Settings → General → Login Items) to confirm the
|
|
registration took effect.
|
|
|
|
## Logs
|
|
|
|
```bash
|
|
log stream --predicate 'subsystem == "com.local.focusfixer"' --level debug
|
|
```
|
|
|
|
Watch for repeated `tapDisabledByTimeout`/`tapDisabledByUserInput` messages
|
|
(indicates the tap callback is doing too much work) and AX error codes
|
|
logged by `FocusEnforcer`.
|