fix(cua): refuse keys before system panel input is ready
This commit is contained in:
1 parent
c612a97e47
commit
73c80201f0
3 files changed
+23
-4
No files matched your search
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user