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.
- package/dist/common/src/schemas/schemas.json +15966 -4090
- package/dist/common/src/types/generated/closeSurface_v3.d.ts +2 -2
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +1 -1
- package/dist/common/src/types/generated/goTo_v3.d.ts +1 -1
- package/dist/common/src/types/generated/record_v3.d.ts +1 -1
- package/dist/common/src/types/generated/runBrowserScript_v3.d.ts +1 -1
- package/dist/common/src/types/generated/screenshot_v3.d.ts +1 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts +317 -3
- package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/step_v3.d.ts +329 -15
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/surface_v3.d.ts +1 -1
- package/dist/common/src/types/generated/swipe_v3.d.ts +2 -2
- package/dist/common/src/types/generated/test_v3.d.ts +669 -41
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/type_v3.d.ts +1 -1
- package/dist/core/resolveTests.d.ts.map +1 -1
- package/dist/core/resolveTests.js +8 -1
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/tests/appGestures.d.ts +7 -1
- package/dist/core/tests/appGestures.d.ts.map +1 -1
- package/dist/core/tests/appGestures.js +24 -4
- package/dist/core/tests/appGestures.js.map +1 -1
- package/dist/core/tests/appSurface.d.ts +13 -1
- package/dist/core/tests/appSurface.d.ts.map +1 -1
- package/dist/core/tests/appSurface.js +42 -10
- package/dist/core/tests/appSurface.js.map +1 -1
- package/dist/core/tests/appWindows.d.ts +45 -0
- package/dist/core/tests/appWindows.d.ts.map +1 -0
- package/dist/core/tests/appWindows.js +638 -0
- package/dist/core/tests/appWindows.js.map +1 -0
- package/dist/core/tests/browserSessions.d.ts +25 -3
- package/dist/core/tests/browserSessions.d.ts.map +1 -1
- package/dist/core/tests/browserSessions.js +63 -27
- package/dist/core/tests/browserSessions.js.map +1 -1
- package/dist/core/tests/browserSurface.d.ts.map +1 -1
- package/dist/core/tests/browserSurface.js +5 -2
- package/dist/core/tests/browserSurface.js.map +1 -1
- package/dist/core/tests/closeSurface.d.ts.map +1 -1
- package/dist/core/tests/closeSurface.js +25 -3
- package/dist/core/tests/closeSurface.js.map +1 -1
- package/dist/core/tests/ffmpegRecorder.d.ts +1 -7
- package/dist/core/tests/ffmpegRecorder.d.ts.map +1 -1
- package/dist/core/tests/ffmpegRecorder.js +5 -22
- package/dist/core/tests/ffmpegRecorder.js.map +1 -1
- package/dist/core/tests/findElement.d.ts.map +1 -1
- package/dist/core/tests/findElement.js +27 -5
- package/dist/core/tests/findElement.js.map +1 -1
- package/dist/core/tests/processSurface.d.ts +22 -0
- package/dist/core/tests/processSurface.d.ts.map +1 -0
- package/dist/core/tests/processSurface.js +105 -0
- package/dist/core/tests/processSurface.js.map +1 -0
- package/dist/core/tests/runShell.d.ts.map +1 -1
- package/dist/core/tests/runShell.js +23 -94
- package/dist/core/tests/runShell.js.map +1 -1
- package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
- package/dist/core/tests/saveScreenshot.js +27 -6
- package/dist/core/tests/saveScreenshot.js.map +1 -1
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +29 -14
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/startSurface.d.ts +17 -0
- package/dist/core/tests/startSurface.d.ts.map +1 -0
- package/dist/core/tests/startSurface.js +275 -0
- package/dist/core/tests/startSurface.js.map +1 -0
- package/dist/core/tests/swipe.d.ts.map +1 -1
- package/dist/core/tests/swipe.js +30 -5
- package/dist/core/tests/swipe.js.map +1 -1
- package/dist/core/tests/typeKeys.d.ts.map +1 -1
- package/dist/core/tests/typeKeys.js +22 -0
- package/dist/core/tests/typeKeys.js.map +1 -1
- package/dist/core/tests.d.ts +1 -0
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +61 -32
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +10 -5
- package/dist/core/utils.js.map +1 -1
- package/dist/index.cjs +17493 -4755
- package/dist/runtime/browserStepKeys.d.ts +22 -0
- package/dist/runtime/browserStepKeys.d.ts.map +1 -1
- package/dist/runtime/browserStepKeys.js +31 -0
- package/dist/runtime/browserStepKeys.js.map +1 -1
- package/dist/runtime/inferRuntimeNeeds.d.ts.map +1 -1
- package/dist/runtime/inferRuntimeNeeds.js +23 -3
- package/dist/runtime/inferRuntimeNeeds.js.map +1 -1
- 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
|