fix(cua): refuse keys before system panel input is ready

This commit is contained in:
creatixchu committed 2026-09-24 11:30:18 +08:00
1 parent c612a97e47
commit 73c80201f0
3 files changed
+23 -4

No files matched your search

+3 -1
View File
@@ -26,7 +26,9 @@ Element click uses AXPress only. Coordinate click, scroll and drag validate wind
The responsible host/helper needs macOS Accessibility permission for AX state and input, and Screen Recording permission for screenshots. Capability preflight and permission errors do not grant permissions. Capture refuses a window that moves or resizes while the image is being read. Unassociated system panels and accessory-only applications are not selectable.
Stable error codes are `INVALID_REQUEST` (malformed envelope), `INVALID_ARGUMENT` (invalid parameters), `UNSUPPORTED` (unknown operation), `APP_UNAVAILABLE`, `AMBIGUOUS_APP`, `STALE_APP`, `STALE_SNAPSHOT`, `WINDOW_UNAVAILABLE`, `WINDOW_CHANGED`, `PERMISSION_DENIED`, and `NATIVE_ERROR` (remaining operating-system failures). An error never promises that a partially dispatched input was rolled back. The host must not retry input automatically.
Stable error codes are `INVALID_REQUEST` (malformed envelope), `INVALID_ARGUMENT` (invalid parameters), `UNSUPPORTED` (unknown operation), `APP_UNAVAILABLE`, `AMBIGUOUS_APP`, `STALE_APP`, `STALE_SNAPSHOT`, `WINDOW_UNAVAILABLE`, `WINDOW_CHANGED`, `PERMISSION_DENIED`, `PANEL_NOT_READY` (system panel recipient unavailable before input), and `NATIVE_ERROR` (remaining operating-system failures). An error never promises that a partially dispatched input was rolled back. The host must not retry input automatically.
Use `--panels-only` to isolate the system file-panel checks. Fixture windows can join application sets so Stage Manager does not shrink them during background input.
Run `python3 packages/experimental/cua/native/tests/e2e.py` after building to exercise an isolated two-window AppKit fixture. It independently records button counts, Unicode text, actual scroll position and drag release position. The check also verifies a real NSOpenPanel sheet, path-entry keyboard routing and the selected file URL. It covers scoped snapshot replacement, screenshot dimensions, stale app cleanup and EOF shutdown. `--artifacts DIRECTORY` writes fixture-only screenshots and a private protocol transcript; the transcript contains the running-app inventory and is not a publication artifact.
@@ -106,7 +106,10 @@ pub fn keyboard_pid(pid: i32) -> Result<i32> {
})
.collect();
let Some((recipient, remote_window)) = panel_keyboard_target(pid, &targets)? else {
return Ok(pid);
return Err(crate::failure(
"PANEL_NOT_READY",
"System file panel has no ready input recipient; read fresh state before input",
));
};
let service = Element(unsafe { AXUIElementCreateApplication(recipient) });
let mut focus = service.attribute("AXFocusedWindow");
@@ -117,7 +120,10 @@ pub fn keyboard_pid(pid: i32) -> Result<i32> {
}
focus = focused.attribute("AXParent");
}
bail!("System file panel focus changed; activate the application and read fresh state");
return Err(crate::failure(
"PANEL_NOT_READY",
"System file panel focus changed; activate the application and read fresh state",
));
}
current = element.attribute("AXParent");
}
+12 -1
View File
@@ -41,7 +41,18 @@ def check_file_panel(call, owner, primary_params, primary, directory, wait_state
assert "Increment primary" not in scoped["tree"], scoped
call("activate", owner)
call("getState", {**primary_params, "windowId": sheet_id, "maxElements": 2000})
call("pressKey", {**owner, "key": "g", "modifiers": ["command", "shift"]})
deadline = time.monotonic() + 5
while True:
try:
call("pressKey", {**owner, "key": "g", "modifiers": ["command", "shift"]})
break
except AssertionError as error:
# PANEL_NOT_READY guarantees no key was dispatched while ViewBridge starts.
reply = error.args[0] if error.args else None
if not isinstance(reply, dict) or reply.get("error", {}).get("code") != "PANEL_NOT_READY" or time.monotonic() >= deadline:
raise
call("getState", {**primary_params, "windowId": sheet_id, "maxElements": 2000})
time.sleep(0.025)
goto = wait_observation(lambda observed: any(e["identifier"] == "PathTextField" for e in observed["elements"]),
"Command+Shift+G did not reach the system panel")
path_field = next(e for e in goto["elements"] if e["identifier"] == "PathTextField")