Skip to content

cua-driver: add launch_app visible-window test, remove stale integration tests - #1438

Merged
ddupont808 merged 3 commits into
mainfrom
cua-driver/test-cleanup
May 4, 2026
Merged

cua-driver: add launch_app visible-window test, remove stale integration tests#1438
ddupont808 merged 3 commits into
mainfrom
cua-driver/test-cleanup

Conversation

@ddupont808

@ddupont808 ddupont808 commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add test_launch_app_visible.py covering the launch_app background-visible-window contract:
    • TextEdit cold launch → on-screen window, 0 focus losses (activates=false holds)
    • Calculator cold launch → on-screen window, focus-steal suppressor restores prior foreground
    • Finder no-URL launch → on-screen window via home-directory fallback, 0 focus losses
  • Remove test_blender_background.py, test_double_click_delivery.py, test_pixel_click_delivery.py — no longer maintained

Test plan

  • scripts/test.sh test_launch_app_visible → 3 tests pass

🤖 Generated with Claude Code

Summary by CodeRabbit

Tests

  • Removed integration test suites for background app interactions and input delivery operations
  • Added integration test suite validating app launch window visibility and focus retention across cold launches

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview May 4, 2026 9:44pm

Request Review

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25923abc-fb0f-4e5e-a39c-18fe02b9d55f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR restructures integration tests for the CUA driver on macOS. Three existing test modules that validated focus preservation during background operations (Blender interaction, double-click delivery, pixel-click delivery) are removed, and a new test module validating launch_app behavior is added. The new tests confirm that launching applications opens visible windows without stealing focus.

Changes

Integration Test Restructuring

Layer / File(s) Summary
Removed Focus-Preservation Tests
libs/cua-driver/Tests/integration/test_blender_background.py, test_double_click_delivery.py, test_pixel_click_delivery.py
Three integration test suites removed: Blender background interaction tests (5 test methods, 276 lines), double-click delivery tests (2 test methods, 296 lines), and pixel-click delivery tests (1 test method, 240 lines). Each used FocusMonitorApp to assert that driver operations did not steal focus.
New Launch App Visibility Tests
libs/cua-driver/Tests/integration/test_launch_app_visible.py
New test class TestLaunchAppVisible with three test methods validating that launch_app() opens visible windows without focus-stealing: cold-launch tests for TextEdit and Calculator, and a test for Finder with no URLs (home-directory fallback). Includes shared helpers to build/launch FocusMonitorApp, activate focus monitoring, read focus-loss counts, and filter on-screen windows. (336 lines)

Possibly Related PRs

  • trycua/cua#1375: Updates FocusMonitorApp behavior and adds new integration tests (overlay z-order, hermes form-fill) using the same test helper app and focus-loss artifacts.
  • trycua/cua#1388: Changes launch_app functionality (AppLauncher.launch and LaunchAppTool), which the new integration tests directly validate.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three tests hop away, their focus guards fade,
While launch\_app now blooms in a fresh test parade—
Windows visible, focus preserved with care,
No stealing allowed in the macOS air! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding a new visible-window test for launch_app and removing three stale integration tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cua-driver/test-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
libs/cua-driver/Tests/integration/test_launch_app_visible.py (1)

217-222: ⚡ Quick win

Assertion message f-strings eagerly evaluate call_tool — extra IPC call on every passing test

Python evaluates all arguments to assertGreater (including the message) before the assertion logic runs. The self.client.call_tool('list_windows', ...) inside the f-string is therefore invoked on every test execution, not only on failure. The same pattern repeats at lines 272–277 and 320–325.

A simple fix is to capture all_windows from the same list_windows result already fetched inside _on_screen_windows, or by refactoring _on_screen_windows to also return the unfiltered list:

♻️ Example refactor for `test_01` (apply the same pattern to `test_02`/`test_03`)
-        on_screen = _on_screen_windows(self.client, pid)
-        losses = _read_focus_losses() - losses_before
-
-        self.assertGreater(
-            len(on_screen), 0,
-            f"launch_app(TextEdit) must produce at least one on-screen window. "
-            f"pid={pid}. All windows: "
-            f"{self.client.call_tool('list_windows', {'pid': pid}).get('structuredContent', {}).get('windows', [])}"
-        )
+        raw = self.client.call_tool("list_windows", {"pid": pid})
+        all_windows = raw.get("structuredContent", {}).get("windows", [])
+        on_screen = [
+            w for w in all_windows
+            if w.get("is_on_screen")
+            and (w.get("bounds", {}).get("width", 0) or 0) > 50
+            and (w.get("bounds", {}).get("height", 0) or 0) > 50
+        ]
+        losses = _read_focus_losses() - losses_before
+
+        self.assertGreater(
+            len(on_screen), 0,
+            f"launch_app(TextEdit) must produce at least one on-screen window. "
+            f"pid={pid}. All windows: {all_windows}"
+        )

If the inline filter duplication is a concern, extend _on_screen_windows to return a (on_screen, all_windows) tuple instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/Tests/integration/test_launch_app_visible.py` around lines
217 - 222, The assertion message eagerly calls
self.client.call_tool('list_windows', ...) inside the f-string causing extra IPC
on every passing test; modify the test to avoid side-effectful message
construction by having _on_screen_windows return both (on_screen, all_windows)
or by capturing all_windows from the same list_windows result before the assert,
then call self.assertGreater(len(on_screen), 0) and only build the detailed
failure message (including all_windows) when the assertion fails (e.g., inside
an if not on_screen: raise AssertionError(...)); update references in
test_launch_app_visible.py (tests test_01/test_02/test_03 and the helper
_on_screen_windows) accordingly so the assert message no longer invokes
call_tool unconditionally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/cua-driver/Tests/integration/test_launch_app_visible.py`:
- Around line 163-170: After calling DriverClient(cls.binary).__enter__() in
setUpClass, immediately register a class cleanup that will call
cls.client.__exit__ so the client is always closed even if later setup (e.g.,
_build_focus_app or _launch_focus_app) raises; specifically, after the __enter__
call invoke cls.addClassCleanup(cls.client.__exit__, None, None, None) (or
equivalent) so DriverClient cleanup is guaranteed, and remove or adjust any
duplicate explicit __exit__ calls in tearDownClass to avoid double-cleanup.
- Around line 84-91: The test currently uses blocking proc.stdout.readline() in
a loop which can hang indefinitely; change the wait to a deadline-based
non-blocking read using select on proc.stdout.fileno() (or equivalent) to
enforce the timeout: repeatedly use select.select([proc.stdout], [], [],
remaining_timeout) and only call proc.stdout.readline() when select indicates
data is ready, then parse for the "FOCUS_PID=" line (the code that extracts pid
with line.split("=", 1)[1]); if the deadline elapses, terminate proc and raise
the same RuntimeError about FocusMonitorApp not printing FOCUS_PID in time.
Ensure you update the function that returns (proc, pid) to use this select-based
waiting instead of the blocking readline loop.

---

Nitpick comments:
In `@libs/cua-driver/Tests/integration/test_launch_app_visible.py`:
- Around line 217-222: The assertion message eagerly calls
self.client.call_tool('list_windows', ...) inside the f-string causing extra IPC
on every passing test; modify the test to avoid side-effectful message
construction by having _on_screen_windows return both (on_screen, all_windows)
or by capturing all_windows from the same list_windows result before the assert,
then call self.assertGreater(len(on_screen), 0) and only build the detailed
failure message (including all_windows) when the assertion fails (e.g., inside
an if not on_screen: raise AssertionError(...)); update references in
test_launch_app_visible.py (tests test_01/test_02/test_03 and the helper
_on_screen_windows) accordingly so the assert message no longer invokes
call_tool unconditionally.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6618c776-16f8-43a0-aec6-3e340214607b

📥 Commits

Reviewing files that changed from the base of the PR and between b329c31 and 2eaf868.

📒 Files selected for processing (4)
  • libs/cua-driver/Tests/integration/test_blender_background.py
  • libs/cua-driver/Tests/integration/test_double_click_delivery.py
  • libs/cua-driver/Tests/integration/test_launch_app_visible.py
  • libs/cua-driver/Tests/integration/test_pixel_click_delivery.py
💤 Files with no reviewable changes (3)
  • libs/cua-driver/Tests/integration/test_pixel_click_delivery.py
  • libs/cua-driver/Tests/integration/test_double_click_delivery.py
  • libs/cua-driver/Tests/integration/test_blender_background.py

Comment on lines +84 to +91
for _ in range(40):
line = proc.stdout.readline().strip()
if line.startswith("FOCUS_PID="):
pid = int(line.split("=", 1)[1])
return proc, pid
time.sleep(0.1)
proc.terminate()
raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

readline() is a blocking call — the 4-second "timeout" is illusory and the test runner can hang indefinitely

time.sleep(0.1) is only reached after readline() returns. If FocusMonitorApp starts but hangs before printing FOCUS_PID= (e.g., stalls on Accessibility permission dialogs, resource contention, etc.), readline() blocks forever and the test runner is stuck permanently. The 40-iteration loop provides no actual deadline.

🔒 Proposed fix: deadline-based loop with `select`
+import select as _select
+
 def _launch_focus_app() -> tuple[subprocess.Popen, int]:
     proc = subprocess.Popen(
         [_FOCUS_APP_EXE],
         stdout=subprocess.PIPE,
         stderr=subprocess.DEVNULL,
         text=True,
     )
-    for _ in range(40):
-        line = proc.stdout.readline().strip()
-        if line.startswith("FOCUS_PID="):
-            pid = int(line.split("=", 1)[1])
-            return proc, pid
-        time.sleep(0.1)
-    proc.terminate()
+    deadline = time.monotonic() + 4.0
+    while time.monotonic() < deadline:
+        remaining = deadline - time.monotonic()
+        ready, _, _ = _select.select([proc.stdout], [], [], max(0.0, remaining))
+        if not ready:
+            break
+        line = proc.stdout.readline().strip()
+        if line.startswith("FOCUS_PID="):
+            pid = int(line.split("=", 1)[1])
+            return proc, pid
+    proc.terminate()
     raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _ in range(40):
line = proc.stdout.readline().strip()
if line.startswith("FOCUS_PID="):
pid = int(line.split("=", 1)[1])
return proc, pid
time.sleep(0.1)
proc.terminate()
raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time")
deadline = time.monotonic() + 4.0
while time.monotonic() < deadline:
remaining = deadline - time.monotonic()
ready, _, _ = _select.select([proc.stdout], [], [], max(0.0, remaining))
if not ready:
break
line = proc.stdout.readline().strip()
if line.startswith("FOCUS_PID="):
pid = int(line.split("=", 1)[1])
return proc, pid
proc.terminate()
raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/Tests/integration/test_launch_app_visible.py` around lines 84
- 91, The test currently uses blocking proc.stdout.readline() in a loop which
can hang indefinitely; change the wait to a deadline-based non-blocking read
using select on proc.stdout.fileno() (or equivalent) to enforce the timeout:
repeatedly use select.select([proc.stdout], [], [], remaining_timeout) and only
call proc.stdout.readline() when select indicates data is ready, then parse for
the "FOCUS_PID=" line (the code that extracts pid with line.split("=", 1)[1]);
if the deadline elapses, terminate proc and raise the same RuntimeError about
FocusMonitorApp not printing FOCUS_PID in time. Ensure you update the function
that returns (proc, pid) to use this select-based waiting instead of the
blocking readline loop.

Comment on lines +163 to +170
cls.binary = default_binary_path()
cls.client = DriverClient(cls.binary).__enter__()

# Build + launch FocusMonitorApp BEFORE launch_app so it owns the
# foreground while we measure focus losses.
_build_focus_app()
cls.fm_proc, cls.fm_pid = _launch_focus_app()
time.sleep(0.8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

DriverClient leaks if setUpClass raises after __enter__

Python's unittest skips tearDownClass when setUpClass raises. If _build_focus_app() or _launch_focus_app() throws after line 164, cls.client.__exit__ is never called, leaking whatever resources the DriverClient holds.

🛡️ Proposed fix: guard `DriverClient` cleanup on setup failure
         cls.binary = default_binary_path()
         cls.client = DriverClient(cls.binary).__enter__()
 
-        # Build + launch FocusMonitorApp BEFORE launch_app so it owns the
-        # foreground while we measure focus losses.
-        _build_focus_app()
-        cls.fm_proc, cls.fm_pid = _launch_focus_app()
-        time.sleep(0.8)
+        try:
+            # Build + launch FocusMonitorApp BEFORE launch_app so it owns the
+            # foreground while we measure focus losses.
+            _build_focus_app()
+            cls.fm_proc, cls.fm_pid = _launch_focus_app()
+            time.sleep(0.8)
+        except Exception:
+            cls.client.__exit__(None, None, None)
+            raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/Tests/integration/test_launch_app_visible.py` around lines
163 - 170, After calling DriverClient(cls.binary).__enter__() in setUpClass,
immediately register a class cleanup that will call cls.client.__exit__ so the
client is always closed even if later setup (e.g., _build_focus_app or
_launch_focus_app) raises; specifically, after the __enter__ call invoke
cls.addClassCleanup(cls.client.__exit__, None, None, None) (or equivalent) so
DriverClient cleanup is guaranteed, and remove or adjust any duplicate explicit
__exit__ calls in tearDownClass to avoid double-cleanup.

…ion tests

Add test_launch_app_visible.py covering:
- TextEdit cold launch → on-screen window, 0 focus losses
- Calculator cold launch → on-screen window, focus-steal suppressor restores prior foreground
- Finder no-URL launch → on-screen window via home-directory fallback, 0 focus losses

Remove test_blender_background.py, test_double_click_delivery.py,
test_pixel_click_delivery.py — no longer maintained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cua and others added 2 commits May 4, 2026 14:44
Swift changes:
- FocusWithoutRaise: add withMenuShortcutActivation() — saves/restores
  frontmost PSN around key delivery so NSMenu shortcuts fire on backgrounded
  apps without any observable focus change (< 1ms, invisible to UXMonitor)
- SkyLightEventPost: add setFrontProcessNoWindows, getProcessPSN SPIs
- KeyboardInput: add attachAuthMessage param to route via IOHIDPostEvent
- HotkeyTool: use withMenuShortcutActivation for window_id path
- PressKeyTool: same NSMenu-safe path
- ListWindowsTool / ListAppsTool: add structuredContent output
- TypeTextTool: CGEvent fallback for browser web inputs
- SetValueTool: AXPopUpButton child-pick + JS injection for Safari selects

Test harness:
- New v2 harness (harness/driver.py, monitor.py, tree.py, cv.py)
- conftest.py: focus_monitor, ux_guard, html_server, tauri/electron fixtures
- test_safari.py: 10 tests, all UX-guarded
- test_chrome.py: 6 tests; probe_ax fallback + URL-bar skip in find_text_field
- test_launch_app_visible.py: 3 tests
- test_background_menu_shortcut.py: 2 tests, both ux_guard-clean

Bug fixes:
- driver.py: fix tree extraction regex (was r'- \[' missing indented lines);
  now r'\n\n(- .+)' captures full tree block from text content
- find_text_field: skip Chrome "Address and search bar" alongside Safari's
  "smart search field"
- find_window: add probe_ax option to fall back to window with AX content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files kept on disk (untracked); will land in a separate PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ddupont808
ddupont808 force-pushed the cua-driver/test-cleanup branch from b25a73b to de2feaf Compare May 4, 2026 21:44
@ddupont808
ddupont808 merged commit ab409de into main May 4, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant