doc-detective 4.25.1 → 4.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/common/src/schemas/schemas.json +15966 -4090
  2. package/dist/common/src/types/generated/closeSurface_v3.d.ts +2 -2
  3. package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +1 -1
  4. package/dist/common/src/types/generated/goTo_v3.d.ts +1 -1
  5. package/dist/common/src/types/generated/record_v3.d.ts +1 -1
  6. package/dist/common/src/types/generated/runBrowserScript_v3.d.ts +1 -1
  7. package/dist/common/src/types/generated/screenshot_v3.d.ts +1 -1
  8. package/dist/common/src/types/generated/startSurface_v3.d.ts +317 -3
  9. package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -1
  10. package/dist/common/src/types/generated/step_v3.d.ts +329 -15
  11. package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
  12. package/dist/common/src/types/generated/surface_v3.d.ts +1 -1
  13. package/dist/common/src/types/generated/swipe_v3.d.ts +2 -2
  14. package/dist/common/src/types/generated/test_v3.d.ts +669 -41
  15. package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
  16. package/dist/common/src/types/generated/type_v3.d.ts +1 -1
  17. package/dist/core/resolveTests.d.ts.map +1 -1
  18. package/dist/core/resolveTests.js +8 -1
  19. package/dist/core/resolveTests.js.map +1 -1
  20. package/dist/core/tests/appGestures.d.ts +7 -1
  21. package/dist/core/tests/appGestures.d.ts.map +1 -1
  22. package/dist/core/tests/appGestures.js +24 -4
  23. package/dist/core/tests/appGestures.js.map +1 -1
  24. package/dist/core/tests/appSurface.d.ts +13 -1
  25. package/dist/core/tests/appSurface.d.ts.map +1 -1
  26. package/dist/core/tests/appSurface.js +42 -10
  27. package/dist/core/tests/appSurface.js.map +1 -1
  28. package/dist/core/tests/appWindows.d.ts +45 -0
  29. package/dist/core/tests/appWindows.d.ts.map +1 -0
  30. package/dist/core/tests/appWindows.js +638 -0
  31. package/dist/core/tests/appWindows.js.map +1 -0
  32. package/dist/core/tests/browserSessions.d.ts +25 -3
  33. package/dist/core/tests/browserSessions.d.ts.map +1 -1
  34. package/dist/core/tests/browserSessions.js +63 -27
  35. package/dist/core/tests/browserSessions.js.map +1 -1
  36. package/dist/core/tests/browserSurface.d.ts.map +1 -1
  37. package/dist/core/tests/browserSurface.js +5 -2
  38. package/dist/core/tests/browserSurface.js.map +1 -1
  39. package/dist/core/tests/closeSurface.d.ts.map +1 -1
  40. package/dist/core/tests/closeSurface.js +25 -3
  41. package/dist/core/tests/closeSurface.js.map +1 -1
  42. package/dist/core/tests/ffmpegRecorder.d.ts +1 -7
  43. package/dist/core/tests/ffmpegRecorder.d.ts.map +1 -1
  44. package/dist/core/tests/ffmpegRecorder.js +5 -22
  45. package/dist/core/tests/ffmpegRecorder.js.map +1 -1
  46. package/dist/core/tests/findElement.d.ts.map +1 -1
  47. package/dist/core/tests/findElement.js +27 -5
  48. package/dist/core/tests/findElement.js.map +1 -1
  49. package/dist/core/tests/processSurface.d.ts +22 -0
  50. package/dist/core/tests/processSurface.d.ts.map +1 -0
  51. package/dist/core/tests/processSurface.js +105 -0
  52. package/dist/core/tests/processSurface.js.map +1 -0
  53. package/dist/core/tests/runShell.d.ts.map +1 -1
  54. package/dist/core/tests/runShell.js +23 -94
  55. package/dist/core/tests/runShell.js.map +1 -1
  56. package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
  57. package/dist/core/tests/saveScreenshot.js +27 -6
  58. package/dist/core/tests/saveScreenshot.js.map +1 -1
  59. package/dist/core/tests/startRecording.d.ts.map +1 -1
  60. package/dist/core/tests/startRecording.js +29 -14
  61. package/dist/core/tests/startRecording.js.map +1 -1
  62. package/dist/core/tests/startSurface.d.ts +17 -0
  63. package/dist/core/tests/startSurface.d.ts.map +1 -0
  64. package/dist/core/tests/startSurface.js +275 -0
  65. package/dist/core/tests/startSurface.js.map +1 -0
  66. package/dist/core/tests/swipe.d.ts.map +1 -1
  67. package/dist/core/tests/swipe.js +30 -5
  68. package/dist/core/tests/swipe.js.map +1 -1
  69. package/dist/core/tests/typeKeys.d.ts.map +1 -1
  70. package/dist/core/tests/typeKeys.js +22 -0
  71. package/dist/core/tests/typeKeys.js.map +1 -1
  72. package/dist/core/tests.d.ts +1 -0
  73. package/dist/core/tests.d.ts.map +1 -1
  74. package/dist/core/tests.js +61 -32
  75. package/dist/core/tests.js.map +1 -1
  76. package/dist/core/utils.d.ts.map +1 -1
  77. package/dist/core/utils.js +10 -5
  78. package/dist/core/utils.js.map +1 -1
  79. package/dist/index.cjs +17493 -4755
  80. package/dist/runtime/browserStepKeys.d.ts +22 -0
  81. package/dist/runtime/browserStepKeys.d.ts.map +1 -1
  82. package/dist/runtime/browserStepKeys.js +31 -0
  83. package/dist/runtime/browserStepKeys.js.map +1 -1
  84. package/dist/runtime/inferRuntimeNeeds.d.ts.map +1 -1
  85. package/dist/runtime/inferRuntimeNeeds.js +23 -3
  86. package/dist/runtime/inferRuntimeNeeds.js.map +1 -1
  87. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ export { snapshotAppWindows, resolveAppWindow, activeAppWindow, defaultAppWindow, appWindowRect, appWindowScreenshot, scopedFindRoot, closeAppWindow, unsupportedWindowSelectorMessage, rewriteXPathForScopedFind, };
2
+ export type AppWindowTarget = {
3
+ kind: "switched";
4
+ } | {
5
+ kind: "element";
6
+ element: any;
7
+ title: string;
8
+ };
9
+ type ResolveResult = {
10
+ ok: true;
11
+ target: AppWindowTarget;
12
+ } | {
13
+ ok: false;
14
+ message: string;
15
+ };
16
+ type CloseResult = {
17
+ ok: true;
18
+ closed: boolean;
19
+ } | {
20
+ ok: false;
21
+ message: string;
22
+ };
23
+ declare function unsupportedWindowSelectorMessage(platform: string): string;
24
+ declare function rewriteXPathForScopedFind(selector: string): string;
25
+ declare function snapshotAppWindows(entry: any): Promise<void>;
26
+ declare function resolveAppWindow({ entry, selector, timeoutMs, }: {
27
+ entry: any;
28
+ selector: any;
29
+ timeoutMs?: number;
30
+ }): Promise<ResolveResult>;
31
+ declare function activeAppWindow(entry: any): Promise<AppWindowTarget | null>;
32
+ declare function defaultAppWindow(entry: any): Promise<AppWindowTarget | null>;
33
+ declare function appWindowRect(entry: any, target?: AppWindowTarget | null): Promise<{
34
+ x: number;
35
+ y: number;
36
+ w: number;
37
+ h: number;
38
+ } | null>;
39
+ declare function appWindowScreenshot(entry: any, target: AppWindowTarget | null | undefined, filePath: string): Promise<void>;
40
+ declare function scopedFindRoot(entry: any, target: AppWindowTarget | null): any;
41
+ declare function closeAppWindow({ entry, selector, }: {
42
+ entry: any;
43
+ selector: any;
44
+ }): Promise<CloseResult>;
45
+ //# sourceMappingURL=appWindows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appWindows.d.ts","sourceRoot":"","sources":["../../../src/core/tests/appWindows.ts"],"names":[],"mappings":"AAgCA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gCAAgC,EAChC,yBAAyB,GAC1B,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,KAAK,aAAa,GACd;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC,KAAK,WAAW,GACZ;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAgBnC,iBAAS,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;AA4BD,iBAAS,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM3D;AAyZD,iBAAe,kBAAkB,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B3D;AAID,iBAAe,gBAAgB,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,SAAgB,GACjB,EAAE;IACD,KAAK,EAAE,GAAG,CAAC;IACX,QAAQ,EAAE,GAAG,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,aAAa,CAAC,CAezB;AAKD,iBAAe,eAAe,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA6B1E;AAKD,iBAAe,gBAAgB,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAe3E;AAKD,iBAAe,aAAa,CAC1B,KAAK,EAAE,GAAG,EACV,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,GAC9B,OAAO,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuChE;AAKD,iBAAe,mBAAmB,CAChC,KAAK,EAAE,GAAG,EACV,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC1C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAgBf;AAID,iBAAS,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,GAAG,CAGvE;AAQD,iBAAe,cAAc,CAAC,EAC5B,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,GAAG,CAAC;IACX,QAAQ,EAAE,GAAG,CAAC;CACf,GAAG,OAAO,CAAC,WAAW,CAAC,CAcvB"}
@@ -0,0 +1,638 @@
1
+ // App window selectors on desktop app surfaces (ADR 01036).
2
+ //
3
+ // Two driver models, one grammar (the shared window selector: title string |
4
+ // {name}/{title} criteria with /regex/ | negative index for "newest"):
5
+ //
6
+ // - Windows (NovaWindows) — "switch-then-act". The session has ONE root
7
+ // window; switchToWindow(handle) re-roots it and everything (finds, rect,
8
+ // screenshot) follows. Handles are DESKTOP-GLOBAL in Z-order, so windows
9
+ // are adopted into per-surface state filtered by the root element's
10
+ // ProcessId against the app's pid (best-effort — unfiltered adoption when
11
+ // the attribute can't be read). The driver's switch-by-TITLE branch is
12
+ // never used: it has a trySetForegroundWindow(NaN) bug and burns a 20×500ms
13
+ // internal retry on misses — we probe by handle and match titles ourselves.
14
+ //
15
+ // - macOS (Mac2) — "window-as-element". There are no window routes at all;
16
+ // an app's windows are XCUIElementTypeWindow elements of the app-rooted
17
+ // tree. A selected window is a held element: finds chain under it, its
18
+ // rect is the window rect (absolute points), element screenshot captures
19
+ // it. Elements go stale when windows close — the sticky state revalidates
20
+ // and re-resolves by stored title once.
21
+ //
22
+ // - Mobile (android/ios) — single-window; every selector FAILs with one
23
+ // shared message.
24
+ //
25
+ // Sticky semantics (the shared surface contract): resolving a window updates
26
+ // the surface's ACTIVE window; later steps without `window` act on it. On
27
+ // Windows the session root is inherently sticky; on macOS the element is
28
+ // stored on the surface entry.
29
+ import { matchesExpectedOutput } from "../utils.js";
30
+ import { isMobileTargetPlatform } from "./mobilePlatform.js";
31
+ export { snapshotAppWindows, resolveAppWindow, activeAppWindow, defaultAppWindow, appWindowRect, appWindowScreenshot, scopedFindRoot, closeAppWindow, unsupportedWindowSelectorMessage, rewriteXPathForScopedFind, };
32
+ const MAC_WINDOW_XPATH = "//XCUIElementTypeWindow";
33
+ const MAC_CLOSE_BUTTON_XPATH = './/XCUIElementTypeButton[@identifier="_XCUI:CloseWindow"]';
34
+ // Mac2 "macos: keys" modifier flag for Command (XCUIKeyModifierCommand).
35
+ const MAC_COMMAND_MODIFIER = 1 << 20;
36
+ const RESOLVE_POLL_MS = 250;
37
+ function sleep(ms) {
38
+ return new Promise((r) => setTimeout(r, ms));
39
+ }
40
+ // One wording source for the mobile single-window rejection, so every
41
+ // consumer (find/type/screenshot/record/swipe/closeSurface) FAILs with the
42
+ // same sentence and tests pin one string.
43
+ function unsupportedWindowSelectorMessage(platform) {
44
+ return `${platform} app surfaces are single-window; window selectors aren't supported there — omit \`window\` to act on the app.`;
45
+ }
46
+ function windowsIndexUnsupportedMessage() {
47
+ return `Window index selectors aren't supported on Windows app surfaces — the Windows driver can't enumerate an app's windows in creation order. Select by title (e.g. {"title": "/Open/"}) or use -1 for the newest window.`;
48
+ }
49
+ function noMatchMessage(selector, entryName, timeoutMs, seenTitles) {
50
+ // "Window titles seen" (not "Windows seen") — this helper is shared by the
51
+ // Windows and macOS resolvers, so a capital-W "Windows" would read as the OS
52
+ // name in a macOS failure message.
53
+ return `No app window matched ${JSON.stringify(selector)} on surface "${entryName}" within ${timeoutMs}ms. Window titles seen: ${seenTitles.length > 0 ? seenTitles.join(", ") : "(none)"}.`;
54
+ }
55
+ function lastWindowRefusalMessage(entryName) {
56
+ return `Refusing to close the last window of app surface "${entryName}" — that would end the app. Close the whole surface instead ({"closeSurface": {"app": "${entryName}"}}).`;
57
+ }
58
+ // Anchor an absolute XPath to a scope element: a W3C element-scoped find
59
+ // evaluates `//X` against the WHOLE document, so the compiled app locators
60
+ // (which all start with `//`) must become `.//` to stay inside the window
61
+ // subtree. Non-XPath selectors (accessibility id `~…`) pass through.
62
+ function rewriteXPathForScopedFind(selector) {
63
+ if (typeof selector !== "string")
64
+ return selector;
65
+ if (selector.startsWith(".//"))
66
+ return selector;
67
+ if (selector.startsWith("//"))
68
+ return `.${selector}`;
69
+ if (selector.startsWith("(//"))
70
+ return `(.${selector.slice(1)}`;
71
+ return selector;
72
+ }
73
+ // A selector form we can't honor on Windows: non-negative index (there is no
74
+ // app-scoped creation order to index into).
75
+ function isNonNegativeIndexSelector(selector) {
76
+ if (typeof selector === "number" && selector >= 0)
77
+ return true;
78
+ if (selector &&
79
+ typeof selector === "object" &&
80
+ typeof selector.index === "number" &&
81
+ selector.index >= 0) {
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Windows (NovaWindows) — switch-then-act
88
+ // ---------------------------------------------------------------------------
89
+ // Read the ProcessId of the CURRENT root window, best-effort.
90
+ async function readCurrentRootPid(driver) {
91
+ try {
92
+ const root = await driver.$("/*");
93
+ const raw = await root.getAttribute("ProcessId");
94
+ const pid = Number(raw);
95
+ return Number.isFinite(pid) && raw !== null && raw !== "" ? pid : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ // Adopt new desktop windows that belong to this app (pid-filtered,
102
+ // best-effort) into entry.knownWindows; everything else is remembered as
103
+ // foreign so it's never probed again. Restores the original root.
104
+ async function syncWindowsHandles(entry) {
105
+ const driver = entry.driver;
106
+ entry.knownWindows = entry.knownWindows ?? [];
107
+ entry.foreignWindows = entry.foreignWindows ?? new Set();
108
+ const all = await driver.getWindowHandles();
109
+ // Drop dead handles from the known list (closed windows).
110
+ entry.knownWindows = entry.knownWindows.filter((h) => all.includes(h));
111
+ const unknown = all.filter((h) => !entry.knownWindows.includes(h) && !entry.foreignWindows.has(h));
112
+ if (unknown.length === 0)
113
+ return;
114
+ const original = await driver.getWindowHandle();
115
+ for (const handle of unknown) {
116
+ try {
117
+ await driver.switchToWindow(handle);
118
+ const pid = await readCurrentRootPid(driver);
119
+ // Adopt on pid match — or best-effort when either side is unreadable
120
+ // (documented caveat: adoption degrades to an unfiltered diff).
121
+ if (pid === null || entry.appPid == null || pid === entry.appPid) {
122
+ // knownWindows is adoption-ordered (`-1` = newest = last). A window
123
+ // that already existed at the surface snapshot is OLD by definition:
124
+ // insert it right after the main window so it never shadows a
125
+ // genuinely new dialog probed in the same sweep.
126
+ if (entry.baselineWindowHandles?.has(handle)) {
127
+ const mainIdx = entry.knownWindows.indexOf(entry.mainWindowHandle);
128
+ entry.knownWindows.splice(mainIdx >= 0 ? mainIdx + 1 : 0, 0, handle);
129
+ }
130
+ else {
131
+ entry.knownWindows.push(handle);
132
+ }
133
+ }
134
+ else {
135
+ entry.foreignWindows.add(handle);
136
+ }
137
+ entry.baselineWindowHandles?.delete(handle);
138
+ }
139
+ catch {
140
+ // Window vanished mid-probe — ignore.
141
+ }
142
+ }
143
+ try {
144
+ await driver.switchToWindow(original);
145
+ }
146
+ catch {
147
+ // The original root itself vanished; stay where we are.
148
+ }
149
+ }
150
+ // Match the selector against this app's known windows. Switches through
151
+ // candidates to read titles; on success LEAVES the session rooted at the
152
+ // match (sticky by construction). Returns the matched handle or null, plus
153
+ // the titles seen (for the no-match message).
154
+ async function matchWindowsSelector(entry, selector) {
155
+ const driver = entry.driver;
156
+ const known = entry.knownWindows ?? [];
157
+ const seenTitles = [];
158
+ // Negative index: from the end of the adoption-ordered known list — the
159
+ // most recently adopted window is the newest (-1, the dialog case).
160
+ if (typeof selector === "number" && selector < 0) {
161
+ const handle = known.at(selector) ?? null;
162
+ return { handle, seenTitles };
163
+ }
164
+ const wantExact = typeof selector === "string"
165
+ ? selector.trim()
166
+ : typeof selector?.name === "string"
167
+ ? selector.name
168
+ : null;
169
+ const wantTitle = selector && typeof selector === "object" && selector.title !== undefined
170
+ ? selector.title
171
+ : null;
172
+ for (const handle of known) {
173
+ try {
174
+ await driver.switchToWindow(handle);
175
+ const title = String(await driver.getTitle());
176
+ seenTitles.push(title);
177
+ if (wantExact !== null && title !== wantExact)
178
+ continue;
179
+ if (wantTitle !== null && !matchesExpectedOutput(title, wantTitle))
180
+ continue;
181
+ if (wantExact === null && wantTitle === null)
182
+ continue;
183
+ return { handle, seenTitles };
184
+ }
185
+ catch {
186
+ // Window vanished — skip.
187
+ }
188
+ }
189
+ return { handle: null, seenTitles };
190
+ }
191
+ async function resolveWindowsWindow({ entry, selector, timeoutMs, }) {
192
+ if (isNonNegativeIndexSelector(selector)) {
193
+ return { ok: false, message: windowsIndexUnsupportedMessage() };
194
+ }
195
+ const driver = entry.driver;
196
+ const original = await driver.getWindowHandle();
197
+ const deadline = Date.now() + timeoutMs;
198
+ let seenTitles = [];
199
+ for (;;) {
200
+ await syncWindowsHandles(entry);
201
+ const match = await matchWindowsSelector(entry, selector);
202
+ if (match.handle) {
203
+ await driver.switchToWindow(match.handle);
204
+ entry.activeWindow = { handle: match.handle };
205
+ return { ok: true, target: { kind: "switched" } };
206
+ }
207
+ seenTitles = match.seenTitles;
208
+ // Restore the original root between attempts (and before failing) so a
209
+ // no-match probe never leaves the session rooted somewhere surprising.
210
+ try {
211
+ await driver.switchToWindow(original);
212
+ }
213
+ catch {
214
+ /* original gone */
215
+ }
216
+ if (Date.now() >= deadline)
217
+ break;
218
+ await sleep(RESOLVE_POLL_MS);
219
+ }
220
+ return {
221
+ ok: false,
222
+ message: noMatchMessage(selector, entry.name, timeoutMs, seenTitles),
223
+ };
224
+ }
225
+ async function closeWindowsWindow({ entry, selector, }) {
226
+ if (isNonNegativeIndexSelector(selector)) {
227
+ return { ok: false, message: windowsIndexUnsupportedMessage() };
228
+ }
229
+ const driver = entry.driver;
230
+ const original = await driver.getWindowHandle();
231
+ await syncWindowsHandles(entry);
232
+ const match = await matchWindowsSelector(entry, selector);
233
+ if (!match.handle) {
234
+ // Idempotent: the window is already gone (or never existed).
235
+ try {
236
+ await driver.switchToWindow(original);
237
+ }
238
+ catch {
239
+ /* original gone */
240
+ }
241
+ return { ok: true, closed: false };
242
+ }
243
+ if ((entry.knownWindows?.length ?? 0) <= 1) {
244
+ try {
245
+ await driver.switchToWindow(original);
246
+ }
247
+ catch {
248
+ /* original gone */
249
+ }
250
+ return { ok: false, message: lastWindowRefusalMessage(entry.name) };
251
+ }
252
+ // Root the session at the match before closing. matchWindowsSelector only
253
+ // leaves the session there for the title/regex path (it switches through
254
+ // candidates); the negative-index path returns the handle WITHOUT switching,
255
+ // and syncWindowsHandles restored the session to `original` — so without this
256
+ // explicit switch, `windows: closeApp` would close the wrong (original)
257
+ // window. Mirrors resolveWindowsWindow's post-match switch.
258
+ try {
259
+ await driver.switchToWindow(match.handle);
260
+ }
261
+ catch {
262
+ // The matched window vanished between sync and close — restore the root
263
+ // and treat it as an idempotent no-op.
264
+ try {
265
+ await driver.switchToWindow(original);
266
+ }
267
+ catch {
268
+ /* original gone too */
269
+ }
270
+ return { ok: true, closed: false };
271
+ }
272
+ await driver.execute("windows: closeApp");
273
+ entry.knownWindows = (entry.knownWindows ?? []).filter((h) => h !== match.handle);
274
+ if (entry.activeWindow?.handle === match.handle)
275
+ delete entry.activeWindow;
276
+ // The main window can be closed too (only the LAST window is refused).
277
+ // Drop the stale handle so the teardown re-root guard doesn't switch to a
278
+ // dead window before deleteSession.
279
+ if (entry.mainWindowHandle === match.handle)
280
+ delete entry.mainWindowHandle;
281
+ // Switch to a survivor: prefer the app's main window, else the most
282
+ // recently adopted survivor. Teardown closes whatever the root is, so the
283
+ // session must never be left rooted at a dead handle.
284
+ const survivor = entry.mainWindowHandle &&
285
+ entry.knownWindows.includes(entry.mainWindowHandle)
286
+ ? entry.mainWindowHandle
287
+ : entry.knownWindows.at(-1);
288
+ if (survivor) {
289
+ try {
290
+ await driver.switchToWindow(survivor);
291
+ }
292
+ catch {
293
+ /* best-effort */
294
+ }
295
+ }
296
+ return { ok: true, closed: true };
297
+ }
298
+ async function enumerateMacWindows(entry) {
299
+ const driver = entry.driver;
300
+ const elements = await driver.$$(MAC_WINDOW_XPATH);
301
+ const candidates = [];
302
+ for (const element of elements) {
303
+ try {
304
+ const title = String((await element.getAttribute("title")) ?? "");
305
+ let frameKey = "";
306
+ try {
307
+ const rect = await driver.getElementRect(element.elementId);
308
+ frameKey = JSON.stringify(rect);
309
+ }
310
+ catch {
311
+ /* rect unavailable — title-only key */
312
+ }
313
+ candidates.push({ element, title, frameKey });
314
+ }
315
+ catch {
316
+ // Stale element — skip.
317
+ }
318
+ }
319
+ return candidates;
320
+ }
321
+ function macBaselineKeys(candidates) {
322
+ return candidates.map((c) => `${c.title}\u0000${c.frameKey}`);
323
+ }
324
+ function matchMacSelector(candidates, selector, baseline) {
325
+ if (typeof selector === "number") {
326
+ if (selector >= 0)
327
+ return candidates[selector] ?? null;
328
+ if (selector === -1) {
329
+ // Newest = the window that's new since the baseline (set-diff on
330
+ // title+frame). Exactly one new → it; several → the last in query
331
+ // order; none → last in query order (best-effort, the browser -1
332
+ // semantics degraded to what the driver exposes).
333
+ const base = new Set(baseline ?? []);
334
+ const fresh = candidates.filter((c) => !base.has(`${c.title}\u0000${c.frameKey}`));
335
+ if (fresh.length > 0)
336
+ return fresh.at(-1);
337
+ return candidates.at(-1) ?? null;
338
+ }
339
+ return candidates.at(selector) ?? null;
340
+ }
341
+ const wantExact = typeof selector === "string"
342
+ ? selector.trim()
343
+ : typeof selector?.name === "string"
344
+ ? selector.name
345
+ : null;
346
+ const wantTitle = selector && typeof selector === "object" && selector.title !== undefined
347
+ ? selector.title
348
+ : null;
349
+ for (const candidate of candidates) {
350
+ if (wantExact !== null && candidate.title !== wantExact)
351
+ continue;
352
+ if (wantTitle !== null &&
353
+ !matchesExpectedOutput(candidate.title, wantTitle))
354
+ continue;
355
+ if (wantExact === null && wantTitle === null)
356
+ continue;
357
+ return candidate;
358
+ }
359
+ return null;
360
+ }
361
+ async function resolveMacWindow({ entry, selector, timeoutMs, }) {
362
+ const deadline = Date.now() + timeoutMs;
363
+ let seenTitles = [];
364
+ for (;;) {
365
+ const candidates = await enumerateMacWindows(entry);
366
+ seenTitles = candidates.map((c) => c.title);
367
+ const match = matchMacSelector(candidates, selector, entry.windowBaseline);
368
+ if (match) {
369
+ entry.activeWindow = { element: match.element, title: match.title };
370
+ return {
371
+ ok: true,
372
+ target: { kind: "element", element: match.element, title: match.title },
373
+ };
374
+ }
375
+ if (Date.now() >= deadline)
376
+ break;
377
+ await sleep(RESOLVE_POLL_MS);
378
+ }
379
+ return {
380
+ ok: false,
381
+ message: noMatchMessage(selector, entry.name, timeoutMs, seenTitles),
382
+ };
383
+ }
384
+ async function closeMacWindow({ entry, selector, }) {
385
+ const driver = entry.driver;
386
+ const candidates = await enumerateMacWindows(entry);
387
+ const match = matchMacSelector(candidates, selector, entry.windowBaseline);
388
+ if (!match)
389
+ return { ok: true, closed: false };
390
+ if (candidates.length <= 1) {
391
+ return { ok: false, message: lastWindowRefusalMessage(entry.name) };
392
+ }
393
+ // Preferred: the stoplight close button inside the window (the same
394
+ // `_XCUI:` identifier family WDA itself clicks for fullscreen/minimize).
395
+ let clicked = false;
396
+ try {
397
+ const button = await match.element.$(MAC_CLOSE_BUTTON_XPATH);
398
+ if (await button.isExisting()) {
399
+ await button.click();
400
+ clicked = true;
401
+ }
402
+ }
403
+ catch {
404
+ /* fall through to the keyboard fallback */
405
+ }
406
+ if (!clicked) {
407
+ // Fallback: focus the window via a title-bar click (the practical raise
408
+ // on macOS — there is no per-window AXRaise without insecure server
409
+ // features), then Cmd+W.
410
+ try {
411
+ const rect = await driver.getElementRect(match.element.elementId);
412
+ await driver.execute("macos: click", {
413
+ x: Math.round(rect.x + rect.width / 2),
414
+ y: Math.round(rect.y + 10),
415
+ });
416
+ }
417
+ catch {
418
+ /* best-effort raise */
419
+ }
420
+ await driver.execute("macos: keys", {
421
+ keys: [{ key: "w", modifierFlags: MAC_COMMAND_MODIFIER }],
422
+ });
423
+ }
424
+ if (entry.activeWindow?.title === match.title)
425
+ delete entry.activeWindow;
426
+ // Refresh the baseline so a later -1 doesn't resolve to a ghost.
427
+ try {
428
+ entry.windowBaseline = macBaselineKeys(await enumerateMacWindows(entry));
429
+ }
430
+ catch {
431
+ /* best-effort */
432
+ }
433
+ return { ok: true, closed: true };
434
+ }
435
+ // ---------------------------------------------------------------------------
436
+ // Shared API (platform dispatch)
437
+ // ---------------------------------------------------------------------------
438
+ // Capture the window baseline for a freshly opened app surface. Called by
439
+ // startAppSurface after registration; all fields are best-effort.
440
+ async function snapshotAppWindows(entry) {
441
+ const platform = entry.platform ?? "windows";
442
+ if (isMobileTargetPlatform(platform))
443
+ return;
444
+ try {
445
+ if (platform === "mac") {
446
+ entry.windowBaseline = macBaselineKeys(await enumerateMacWindows(entry));
447
+ return;
448
+ }
449
+ // Windows: the current root is the app's main window. Other desktop
450
+ // windows present at baseline are NOT pre-damned as foreign — the app may
451
+ // have launched with several of its own (splash + main, multiple
452
+ // documents). They're recorded as baseline handles and pid-probed lazily
453
+ // by syncWindowsHandles on the first selector use, so startSurface pays
454
+ // no probe sweep and the app's own launch windows stay selectable.
455
+ const driver = entry.driver;
456
+ const main = await driver.getWindowHandle();
457
+ entry.mainWindowHandle = main;
458
+ entry.knownWindows = [main];
459
+ entry.foreignWindows = new Set();
460
+ entry.baselineWindowHandles = new Set((await driver.getWindowHandles()).filter((h) => h !== main));
461
+ entry.appPid = await readCurrentRootPid(driver);
462
+ }
463
+ catch {
464
+ // Baseline capture is best-effort: without it, -1 degrades and pid
465
+ // filtering is skipped, both documented.
466
+ }
467
+ }
468
+ // Resolve a window selector on an app surface and make it sticky (the
469
+ // surface's active window). Retries until timeoutMs.
470
+ async function resolveAppWindow({ entry, selector, timeoutMs = 5000, }) {
471
+ const platform = entry.platform ?? "windows";
472
+ if (isMobileTargetPlatform(platform)) {
473
+ return { ok: false, message: unsupportedWindowSelectorMessage(platform) };
474
+ }
475
+ try {
476
+ if (platform === "mac")
477
+ return await resolveMacWindow({ entry, selector, timeoutMs });
478
+ return await resolveWindowsWindow({ entry, selector, timeoutMs });
479
+ }
480
+ catch (error) {
481
+ return {
482
+ ok: false,
483
+ message: `Couldn't resolve the app window selector ${JSON.stringify(selector)}: ${error?.message ?? error}`,
484
+ };
485
+ }
486
+ }
487
+ // The sticky window for steps WITHOUT a selector. Windows: null — the
488
+ // session root already IS the sticky window. macOS: revalidate the held
489
+ // element (stale → one re-resolve by stored exact title → else clear).
490
+ async function activeAppWindow(entry) {
491
+ const platform = entry.platform ?? "windows";
492
+ if (platform !== "mac")
493
+ return null;
494
+ const held = entry.activeWindow;
495
+ if (!held?.element)
496
+ return null;
497
+ try {
498
+ if (await held.element.isExisting()) {
499
+ return { kind: "element", element: held.element, title: held.title };
500
+ }
501
+ }
502
+ catch {
503
+ /* stale — re-resolve below */
504
+ }
505
+ // Degrade to null on an enumeration failure: selector-less callers await
506
+ // this without their own catch, so a driver hiccup must not bubble as an
507
+ // unhandled rejection. The held window is kept — the failure is transient
508
+ // (unlike the found-nothing case below, which proves the window is gone).
509
+ let candidates;
510
+ try {
511
+ candidates = await enumerateMacWindows(entry);
512
+ }
513
+ catch {
514
+ return null;
515
+ }
516
+ const again = candidates.find((c) => c.title === held.title);
517
+ if (again) {
518
+ entry.activeWindow = { element: again.element, title: again.title };
519
+ return { kind: "element", element: again.element, title: again.title };
520
+ }
521
+ delete entry.activeWindow;
522
+ return null;
523
+ }
524
+ // The window a selector-less capture/crop should use. macOS: the sticky
525
+ // window, else the app's first window element (documented heuristic).
526
+ // Windows: null — the current root is already the window.
527
+ async function defaultAppWindow(entry) {
528
+ const platform = entry.platform ?? "windows";
529
+ if (platform !== "mac")
530
+ return null;
531
+ const active = await activeAppWindow(entry);
532
+ if (active)
533
+ return active;
534
+ // Same never-reject contract as activeAppWindow: null, not a rejection.
535
+ let candidates;
536
+ try {
537
+ candidates = await enumerateMacWindows(entry);
538
+ }
539
+ catch {
540
+ return null;
541
+ }
542
+ const first = candidates[0];
543
+ if (!first)
544
+ return null;
545
+ return { kind: "element", element: first.element, title: first.title };
546
+ }
547
+ // The selected window's rect in the driver's units — Windows: current root
548
+ // via getWindowRect (physical px, scale 1); macOS: the window ELEMENT's rect
549
+ // (absolute points; the caller applies the capture-frame-derived scale).
550
+ async function appWindowRect(entry, target) {
551
+ const platform = entry.platform ?? "windows";
552
+ if (isMobileTargetPlatform(platform))
553
+ return null;
554
+ const driver = entry.driver;
555
+ let rect;
556
+ if (platform === "mac") {
557
+ const fallback = target && target.kind === "element" ? null : await defaultAppWindow(entry);
558
+ const element = target && target.kind === "element"
559
+ ? target.element
560
+ : fallback && fallback.kind === "element"
561
+ ? fallback.element
562
+ : null;
563
+ if (!element)
564
+ return null;
565
+ try {
566
+ rect = await driver.getElementRect(element.elementId);
567
+ }
568
+ catch {
569
+ // The window went away between resolution and the rect read — same
570
+ // null contract as the geometry validation below.
571
+ return null;
572
+ }
573
+ }
574
+ else {
575
+ rect = await driver.getWindowRect();
576
+ }
577
+ const finite = (v) => typeof v === "number" && Number.isFinite(v);
578
+ if (!rect ||
579
+ !finite(rect.x) ||
580
+ !finite(rect.y) ||
581
+ !finite(rect.width) ||
582
+ !finite(rect.height) ||
583
+ rect.width <= 0 ||
584
+ rect.height <= 0) {
585
+ return null;
586
+ }
587
+ return { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
588
+ }
589
+ // Capture the selected window to a file. Windows: the driver screenshot
590
+ // already captures exactly the current root window. macOS: element
591
+ // screenshot of the window element.
592
+ async function appWindowScreenshot(entry, target, filePath) {
593
+ const platform = entry.platform ?? "windows";
594
+ if (platform === "mac") {
595
+ const fallback = target && target.kind === "element" ? null : await defaultAppWindow(entry);
596
+ const element = target && target.kind === "element"
597
+ ? target.element
598
+ : fallback && fallback.kind === "element"
599
+ ? fallback.element
600
+ : null;
601
+ if (!element)
602
+ throw new Error("No app window is available to capture.");
603
+ await element.saveScreenshot(filePath);
604
+ return;
605
+ }
606
+ await entry.driver.saveScreenshot(filePath);
607
+ }
608
+ // The root for element finds scoped to the selected window: the window
609
+ // element on macOS; null on Windows (the session root IS the window).
610
+ function scopedFindRoot(entry, target) {
611
+ if (target && target.kind === "element")
612
+ return target.element;
613
+ return null;
614
+ }
615
+ // Close ONE window of an app surface. Absent selector match is an
616
+ // idempotent no-op ({closed: false}); closing the LAST window is refused
617
+ // (that would end the app — bare closeSurface does that). No timeout knob:
618
+ // unlike resolveAppWindow (which polls for a window that may still be
619
+ // opening), close targets a window that already exists — a miss is the
620
+ // designed no-op, not something to wait out.
621
+ async function closeAppWindow({ entry, selector, }) {
622
+ const platform = entry.platform ?? "windows";
623
+ if (isMobileTargetPlatform(platform)) {
624
+ return { ok: false, message: unsupportedWindowSelectorMessage(platform) };
625
+ }
626
+ try {
627
+ if (platform === "mac")
628
+ return await closeMacWindow({ entry, selector });
629
+ return await closeWindowsWindow({ entry, selector });
630
+ }
631
+ catch (error) {
632
+ return {
633
+ ok: false,
634
+ message: `Couldn't close the app window ${JSON.stringify(selector)}: ${error?.message ?? error}`,
635
+ };
636
+ }
637
+ }
638
+ //# sourceMappingURL=appWindows.js.map