|
19 | 19 | from ApplicationServices import kAXFocusedWindowAttribute |
20 | 20 | from ApplicationServices import kAXMainWindowAttribute |
21 | 21 | from ApplicationServices import kAXRoleAttribute |
| 22 | +from ApplicationServices import kAXTitleAttribute |
| 23 | +from ApplicationServices import kAXWindowsAttribute |
22 | 24 |
|
23 | 25 | REPO_ROOT = pathlib.Path(__file__).resolve().parents[4] |
24 | 26 |
|
@@ -146,10 +148,18 @@ def _web_appeared() -> bool: |
146 | 148 | stderr = f.read().decode(errors="replace") |
147 | 149 | except Exception: |
148 | 150 | pass |
| 151 | + ax_state = self._describe_ax_state() |
| 152 | + # Startup failures (Compositor/sandbox/IPC) log near the top of stderr, so keep the head too. |
| 153 | + if len(stderr) <= 6000: |
| 154 | + stderr_excerpt = stderr |
| 155 | + else: |
| 156 | + stderr_excerpt = stderr[:3000] + "\n...[truncated]...\n" + stderr[-3000:] |
149 | 157 | self.stop() |
150 | 158 | raise RuntimeError( |
151 | 159 | f"Ladybird did not register an AXWebArea on NSAccessibility within {startup_timeout:g}s.\n" |
152 | | - f"URL: {self._url}\nBinary: {self._binary}\nStderr: {stderr[-2000:]}" |
| 160 | + f"URL: {self._url}\nBinary: {self._binary}\n" |
| 161 | + f"AX surface at timeout:\n{ax_state}\n" |
| 162 | + f"Stderr:\n{stderr_excerpt}" |
153 | 163 | ) |
154 | 164 |
|
155 | 165 | def stop(self) -> None: |
@@ -189,6 +199,28 @@ def _find_web_area(self): |
189 | 199 | return None |
190 | 200 | return _find_by_role(window, "AXWebArea") |
191 | 201 |
|
| 202 | + def _describe_ax_state(self) -> str: |
| 203 | + """Best-effort snapshot of what IS on the AX surface, to diagnose a missing AXWebArea on timeout.""" |
| 204 | + lines = [] |
| 205 | + if self._process is not None: |
| 206 | + code = self._process.poll() |
| 207 | + state = "alive" if code is None else f"exited with code {code}" |
| 208 | + lines.append(f"process: {state} (pid {self._process.pid})") |
| 209 | + lines.append(f"app AXRole: {_ax_attr(self._app, kAXRoleAttribute)!r}") |
| 210 | + windows = _ax_attr(self._app, kAXWindowsAttribute) or [] |
| 211 | + focused = _ax_attr(self._app, kAXFocusedWindowAttribute) |
| 212 | + main = _ax_attr(self._app, kAXMainWindowAttribute) |
| 213 | + focused_str = "yes" if focused else "no" |
| 214 | + main_str = "yes" if main else "no" |
| 215 | + lines.append(f"windows: {len(windows)}; focused: {focused_str}; main: {main_str}") |
| 216 | + window = focused or main or (windows[0] if windows else None) |
| 217 | + if window is None: |
| 218 | + lines.append("no window on the AX surface to descend into") |
| 219 | + else: |
| 220 | + lines.append("window subtree (AXRole, depth<=4):") |
| 221 | + lines.extend(" " + line for line in _describe_subtree(window, max_depth=4)) |
| 222 | + return "\n".join(lines) |
| 223 | + |
192 | 224 | @property |
193 | 225 | def app(self): |
194 | 226 | """The application AXUIElement. Raises if start() hasn't completed.""" |
@@ -217,6 +249,24 @@ def _find_by_role(root, role_name: str, depth: int = 0): |
217 | 249 | return None |
218 | 250 |
|
219 | 251 |
|
| 252 | +def _describe_subtree(root, *, max_depth: int, depth: int = 0) -> list[str]: |
| 253 | + """Shallow AXRole/AXTitle dump of an AX subtree, depth-capped, for timeout diagnostics.""" |
| 254 | + if root is None: |
| 255 | + return [] |
| 256 | + role = _ax_attr(root, kAXRoleAttribute) |
| 257 | + title = _ax_attr(root, kAXTitleAttribute) |
| 258 | + label = (" " * depth) + str(role) + (f" {title!r}" if title else "") |
| 259 | + children = _ax_attr(root, kAXChildrenAttribute) or [] |
| 260 | + if depth >= max_depth: |
| 261 | + if children: |
| 262 | + return [label, (" " * (depth + 1)) + f"... {len(children)} children (depth-capped)"] |
| 263 | + return [label] |
| 264 | + out = [label] |
| 265 | + for child in children: |
| 266 | + out.extend(_describe_subtree(child, max_depth=max_depth, depth=depth + 1)) |
| 267 | + return out |
| 268 | + |
| 269 | + |
220 | 270 | @contextmanager |
221 | 271 | def ladybird_for_fixture(fixture_name: str, binary: Optional[pathlib.Path] = None): |
222 | 272 | """Context manager spawning Ladybird on input/<fixture_name>.""" |
|
0 commit comments