appium-desktop-driver 1.7.2-preview.1 → 1.7.2

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 (111) hide show
  1. package/README.md +2 -0
  2. package/build/eslint.config.d.mts +3 -0
  3. package/build/eslint.config.d.mts.map +1 -0
  4. package/build/eslint.config.mjs +22 -0
  5. package/build/eslint.config.mjs.map +1 -0
  6. package/build/lib/commands/actions.d.ts +3 -5
  7. package/build/lib/commands/actions.d.ts.map +1 -1
  8. package/build/lib/commands/actions.js +91 -83
  9. package/build/lib/commands/actions.js.map +1 -1
  10. package/build/lib/commands/app.d.ts +1 -53
  11. package/build/lib/commands/app.d.ts.map +1 -1
  12. package/build/lib/commands/app.js +147 -415
  13. package/build/lib/commands/app.js.map +1 -1
  14. package/build/lib/commands/device.d.ts +8 -0
  15. package/build/lib/commands/device.d.ts.map +1 -1
  16. package/build/lib/commands/device.js +229 -4
  17. package/build/lib/commands/device.js.map +1 -1
  18. package/build/lib/commands/element.d.ts +3 -0
  19. package/build/lib/commands/element.d.ts.map +1 -1
  20. package/build/lib/commands/element.js +53 -102
  21. package/build/lib/commands/element.js.map +1 -1
  22. package/build/lib/commands/extension.d.ts +0 -2
  23. package/build/lib/commands/extension.d.ts.map +1 -1
  24. package/build/lib/commands/extension.js +184 -111
  25. package/build/lib/commands/extension.js.map +1 -1
  26. package/build/lib/commands/index.d.ts +17 -28
  27. package/build/lib/commands/index.d.ts.map +1 -1
  28. package/build/lib/commands/index.js +2 -4
  29. package/build/lib/commands/index.js.map +1 -1
  30. package/build/lib/commands/powershell.d.ts +6 -0
  31. package/build/lib/commands/powershell.d.ts.map +1 -0
  32. package/build/lib/commands/powershell.js +202 -0
  33. package/build/lib/commands/powershell.js.map +1 -0
  34. package/build/lib/commands/screen-recorder.d.ts +3 -3
  35. package/build/lib/commands/screen-recorder.d.ts.map +1 -1
  36. package/build/lib/commands/screen-recorder.js +24 -33
  37. package/build/lib/commands/screen-recorder.js.map +1 -1
  38. package/build/lib/constraints.d.ts +7 -26
  39. package/build/lib/constraints.d.ts.map +1 -1
  40. package/build/lib/constraints.js +6 -25
  41. package/build/lib/constraints.js.map +1 -1
  42. package/build/lib/driver.d.ts +13 -22
  43. package/build/lib/driver.d.ts.map +1 -1
  44. package/build/lib/driver.js +49 -130
  45. package/build/lib/driver.js.map +1 -1
  46. package/build/lib/mcp/tools/index.d.ts.map +1 -1
  47. package/build/lib/mcp/tools/index.js +0 -2
  48. package/build/lib/mcp/tools/index.js.map +1 -1
  49. package/build/lib/powershell/conditions.d.ts.map +1 -1
  50. package/build/lib/powershell/conditions.js +0 -7
  51. package/build/lib/powershell/conditions.js.map +1 -1
  52. package/build/lib/util.d.ts +1 -11
  53. package/build/lib/util.d.ts.map +1 -1
  54. package/build/lib/util.js +7 -158
  55. package/build/lib/util.js.map +1 -1
  56. package/build/lib/winapi/user32.d.ts +6 -5
  57. package/build/lib/winapi/user32.d.ts.map +1 -1
  58. package/build/lib/winapi/user32.js +88 -131
  59. package/build/lib/winapi/user32.js.map +1 -1
  60. package/build/lib/xpath/core.d.ts +4 -10
  61. package/build/lib/xpath/core.d.ts.map +1 -1
  62. package/build/lib/xpath/core.js +224 -205
  63. package/build/lib/xpath/core.js.map +1 -1
  64. package/build/lib/xpath/functions.d.ts +2 -7
  65. package/build/lib/xpath/functions.d.ts.map +1 -1
  66. package/build/lib/xpath/functions.js +25 -49
  67. package/build/lib/xpath/functions.js.map +1 -1
  68. package/build/tsconfig.tsbuildinfo +1 -0
  69. package/package.json +12 -18
  70. package/CHANGELOG.md +0 -249
  71. package/build/lib/commands/contexts.d.ts +0 -64
  72. package/build/lib/commands/contexts.d.ts.map +0 -1
  73. package/build/lib/commands/contexts.js +0 -166
  74. package/build/lib/commands/contexts.js.map +0 -1
  75. package/build/lib/commands/server-session.d.ts +0 -9
  76. package/build/lib/commands/server-session.d.ts.map +0 -1
  77. package/build/lib/commands/server-session.js +0 -158
  78. package/build/lib/commands/server-session.js.map +0 -1
  79. package/build/lib/log-file.d.ts +0 -10
  80. package/build/lib/log-file.d.ts.map +0 -1
  81. package/build/lib/log-file.js +0 -86
  82. package/build/lib/log-file.js.map +0 -1
  83. package/build/lib/mcp/tools/context.d.ts +0 -4
  84. package/build/lib/mcp/tools/context.d.ts.map +0 -1
  85. package/build/lib/mcp/tools/context.js +0 -49
  86. package/build/lib/mcp/tools/context.js.map +0 -1
  87. package/build/lib/server/client.d.ts +0 -15
  88. package/build/lib/server/client.d.ts.map +0 -1
  89. package/build/lib/server/client.js +0 -169
  90. package/build/lib/server/client.js.map +0 -1
  91. package/build/lib/server/conditions.d.ts +0 -13
  92. package/build/lib/server/conditions.d.ts.map +0 -1
  93. package/build/lib/server/conditions.js +0 -71
  94. package/build/lib/server/conditions.js.map +0 -1
  95. package/build/lib/server/converter-bridge.d.ts +0 -26
  96. package/build/lib/server/converter-bridge.d.ts.map +0 -1
  97. package/build/lib/server/converter-bridge.js +0 -171
  98. package/build/lib/server/converter-bridge.js.map +0 -1
  99. package/build/lib/server/index.d.ts +0 -5
  100. package/build/lib/server/index.d.ts.map +0 -1
  101. package/build/lib/server/index.js +0 -25
  102. package/build/lib/server/index.js.map +0 -1
  103. package/build/lib/server/protocol.d.ts +0 -40
  104. package/build/lib/server/protocol.d.ts.map +0 -1
  105. package/build/lib/server/protocol.js +0 -3
  106. package/build/lib/server/protocol.js.map +0 -1
  107. package/build/lib/version.d.ts +0 -2
  108. package/build/lib/version.d.ts.map +0 -1
  109. package/build/lib/version.js +0 -20
  110. package/build/lib/version.js.map +0 -1
  111. package/scripts/postinstall.js +0 -39
@@ -12,141 +12,123 @@ exports.changeRootElement = changeRootElement;
12
12
  exports.back = back;
13
13
  exports.forward = forward;
14
14
  exports.title = title;
15
- exports.maximizeWindow = maximizeWindow;
16
- exports.minimizeWindow = minimizeWindow;
17
15
  exports.setWindowRect = setWindowRect;
18
- exports.waitForNewWindow = waitForNewWindow;
19
- exports.attachToWindowHandles = attachToWindowHandles;
20
16
  exports.attachToApplicationWindow = attachToApplicationWindow;
21
- exports.waitForMainWindow = waitForMainWindow;
22
17
  const node_path_1 = require("node:path");
23
- const conditions_1 = require("../server/conditions");
18
+ const powershell_1 = require("../powershell");
24
19
  const util_1 = require("../util");
25
20
  const base_driver_1 = require("@appium/base-driver");
26
21
  const user32_1 = require("../winapi/user32");
27
22
  const enums_1 = require("../enums");
28
- /**
29
- * Polling interval used when waiting for windows/elements to appear during
30
- * app launch and window switching.
31
- *
32
- * This is deliberately kept low (200ms) because the app launch flow has
33
- * multiple sequential retry phases that each sleep on failure:
34
- *
35
- * changeRootElement (outer loop)
36
- * → attachToApplicationWindow
37
- * → waitForNewWindow (polls for Win32 window handle)
38
- * → findElement loop (polls UIAutomation tree for the element)
39
- *
40
- * At 500ms per poll, 10 failed attempts in any one phase cost 5 seconds,
41
- * and the phases compound. At 200ms the same 10 attempts cost 2 seconds,
42
- * cutting overall launch latency from 10-15s down to ~3-5s for typical
43
- * UWP apps like Calculator.
44
- */
45
- const POLL_INTERVAL_MS = 200;
46
- /**
47
- * Maximum number of polling attempts per retry phase.
48
- * Combined with POLL_INTERVAL_MS this gives a ~6s window per phase,
49
- * which is enough for most apps to register in the UIAutomation tree.
50
- */
51
- const MAX_POLL_ATTEMPTS = 30;
52
- // setWindow is called often during a session. If a handle doesn't exist,
53
- // we fail fast (~400ms) instead of waiting 6s. Session-attach loops below
54
- // still use MAX_POLL_ATTEMPTS.
55
- const SET_WINDOW_MAX_POLL_ATTEMPTS = 2;
56
- /**
57
- * Normalizes the `ms:waitForAppLaunch` capability to milliseconds.
58
- *
59
- * WinAppDriver's spec says the value is in seconds (max 50), but many users
60
- * pass milliseconds (e.g. 120000 meaning "120 seconds"). We auto-detect:
61
- * values > 120 are assumed to already be in milliseconds; values ≤ 120 are
62
- * treated as seconds and converted to ms.
63
- */
64
- function normalizeWaitForAppLaunchMs(raw) {
65
- if (raw == null || raw <= 0) {
66
- return 0;
23
+ const GET_PAGE_SOURCE_COMMAND = (0, powershell_1.pwsh$ /* ps1 */) `
24
+ $el = ${0}
25
+
26
+ if ($el -eq $null) {
27
+ $dummy = [xml]'<DummyRoot></DummyRoot>'
28
+ return $dummy.OuterXml
67
29
  }
68
- // Values > 120 are almost certainly milliseconds (120 seconds is already very generous;
69
- // WinAppDriver caps at 50s). Values ≤ 120 are seconds per the original spec.
70
- return raw > 120 ? raw : raw * 1000;
71
- }
30
+
31
+ Get-PageSource $el |
32
+ ForEach-Object { $_.OuterXml }
33
+ `;
34
+ const GET_SCREENSHOT_COMMAND = (0, powershell_1.pwsh /* ps1 */) `
35
+ if ($rootElement -eq $null) {
36
+ $bitmap = New-Object Drawing.Bitmap 1,1
37
+ $stream = New-Object IO.MemoryStream
38
+ $bitmap.Save($stream, [Drawing.Imaging.ImageFormat]::Png)
39
+ $bitmap.Dispose()
40
+ return [Convert]::ToBase64String($stream.ToArray())
41
+ }
42
+
43
+ $rect = $rootElement.Current.BoundingRectangle
44
+ $bitmap = New-Object Drawing.Bitmap([int32]$rect.Width, [int32]$rect.Height)
45
+
46
+ $graphics = [Drawing.Graphics]::FromImage($bitmap)
47
+ $graphics.CopyFromScreen([int32]$rect.Left, [int32]$rect.Top, 0, 0, $bitmap.Size)
48
+ $graphics.Dispose()
49
+
50
+ $stream = New-Object IO.MemoryStream
51
+ $bitmap.Save($stream, [Drawing.Imaging.ImageFormat]::Png)
52
+ $bitmap.Dispose()
53
+
54
+ [Convert]::ToBase64String($stream.ToArray())
55
+ `;
56
+ const SLEEP_INTERVAL_MS = 500;
72
57
  async function getPageSource() {
73
- return await this.sendCommand('getPageSource', {});
58
+ return await this.sendPowerShellCommand(GET_PAGE_SOURCE_COMMAND.format(powershell_1.AutomationElement.automationRoot));
74
59
  }
75
60
  async function getScreenshot() {
76
- const automationRootId = await this.sendCommand('saveRootElementToTable', {});
61
+ const automationRootId = await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildCommand());
77
62
  if (this.caps.app && this.caps.app.toLowerCase() !== 'root') {
78
63
  try {
79
64
  await this.focusElement({
80
- [base_driver_1.W3C_ELEMENT_KEY]: automationRootId?.trim(),
65
+ [base_driver_1.W3C_ELEMENT_KEY]: automationRootId.trim(),
81
66
  });
82
67
  }
83
68
  catch {
84
69
  // noop
85
70
  }
86
71
  }
87
- return await this.sendCommand('getScreenshot', {});
72
+ return await this.sendPowerShellCommand(GET_SCREENSHOT_COMMAND);
88
73
  }
89
74
  async function getWindowRect() {
90
- return await this.sendCommand('getRootRect', {});
75
+ const result = await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildGetElementRectCommand());
76
+ return JSON.parse(result.replaceAll(/(?:infinity)/gi, 0x7FFFFFFF.toString()));
91
77
  }
92
78
  async function getWindowHandle() {
93
- const rootId = await this.sendCommand('saveRootElementToTable', {});
94
- const nativeWindowHandle = await this.sendCommand('getProperty', { elementId: rootId, property: 'NativeWindowHandle' });
79
+ const nativeWindowHandle = await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildGetPropertyCommand(powershell_1.Property.NATIVE_WINDOW_HANDLE));
95
80
  return `0x${Number(nativeWindowHandle).toString(16).padStart(8, '0')}`;
96
81
  }
97
82
  async function getWindowHandles() {
98
- // Search from desktop root (RootElement), not the session root
99
- const elIds = await this.sendCommand('findElements', {
100
- scope: 'children',
101
- condition: (0, conditions_1.trueCondition)(),
102
- contextElementId: null,
103
- });
83
+ if (this.appProcessIds.length > 0 && !this.caps.returnAllWindowHandles) {
84
+ const handles = (0, user32_1.getWindowAllHandlesForProcessIds)(this.appProcessIds);
85
+ return handles.map((h) => `0x${h.toString(16).padStart(8, '0')}`);
86
+ }
87
+ const result = await this.sendPowerShellCommand(powershell_1.AutomationElement.rootElement.findAll(powershell_1.TreeScope.CHILDREN, new powershell_1.TrueCondition()).buildCommand());
88
+ const elIds = result.split('\n').map((x) => x.trim()).filter(Boolean);
104
89
  const nativeWindowHandles = [];
105
- for (const elId of elIds ?? []) {
106
- const nativeWindowHandle = await this.sendCommand('getProperty', { elementId: elId, property: 'NativeWindowHandle' });
90
+ for (const elId of elIds) {
91
+ const nativeWindowHandle = await this.sendPowerShellCommand(new powershell_1.FoundAutomationElement(elId).buildGetPropertyCommand(powershell_1.Property.NATIVE_WINDOW_HANDLE));
107
92
  nativeWindowHandles.push(`0x${Number(nativeWindowHandle).toString(16).padStart(8, '0')}`);
108
93
  }
109
94
  return nativeWindowHandles;
110
95
  }
111
96
  async function setWindow(nameOrHandle) {
112
97
  const handle = Number(nameOrHandle);
113
- for (let i = 1; i <= SET_WINDOW_MAX_POLL_ATTEMPTS; i++) {
98
+ const maxRetries = this.caps['ms:windowSwitchRetries'] ?? 20;
99
+ const sleepInterval = this.caps['ms:windowSwitchInterval'] ?? SLEEP_INTERVAL_MS;
100
+ for (let i = 1; i <= maxRetries; i++) {
114
101
  if (!isNaN(handle)) {
115
- const elementId = await this.sendCommand('findElement', {
116
- scope: 'child-or-self',
117
- condition: (0, conditions_1.propertyCondition)('NativeWindowHandle', handle),
118
- contextElementId: null,
119
- });
120
- if (elementId && elementId.trim() !== '') {
121
- await this.sendCommand('setRootElementFromElementId', { elementId });
102
+ const condition = new powershell_1.PropertyCondition(powershell_1.Property.NATIVE_WINDOW_HANDLE, new powershell_1.PSInt32(handle));
103
+ const elementId = await this.sendPowerShellCommand(powershell_1.AutomationElement.rootElement.findFirst(powershell_1.TreeScope.CHILDREN_OR_SELF, condition).buildCommand());
104
+ if (elementId.trim() !== '') {
105
+ await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new powershell_1.FoundAutomationElement(elementId).buildCommand()}`);
122
106
  (0, user32_1.trySetForegroundWindow)(handle);
123
107
  return;
124
108
  }
125
109
  }
126
110
  const name = nameOrHandle;
127
- const elementId = await this.sendCommand('findElement', {
128
- scope: 'children',
129
- condition: (0, conditions_1.propertyCondition)('Name', name),
130
- contextElementId: null,
131
- });
132
- if (elementId && elementId.trim() !== '') {
111
+ const condition = new powershell_1.PropertyCondition(powershell_1.Property.NAME, new powershell_1.PSString(name));
112
+ const elementId = await this.sendPowerShellCommand(powershell_1.AutomationElement.rootElement.findFirst(powershell_1.TreeScope.CHILDREN, condition).buildCommand());
113
+ if (elementId.trim() !== '') {
133
114
  this.log.info(`Found window with name '${name}'. Setting it as the root element.`);
134
- await this.sendCommand('setRootElementFromElementId', { elementId });
115
+ await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new powershell_1.FoundAutomationElement(elementId).buildCommand()}`);
135
116
  (0, user32_1.trySetForegroundWindow)(handle);
136
117
  return;
137
118
  }
138
- this.log.info(`Failed to locate window with name '${name}'. Sleeping for ${POLL_INTERVAL_MS}ms and retrying... (${i}/${SET_WINDOW_MAX_POLL_ATTEMPTS})`);
139
- await (0, util_1.sleep)(POLL_INTERVAL_MS);
119
+ this.log.info(`Failed to locate window with name '${name}'. Sleeping for ${sleepInterval} milliseconds and retrying... (${i}/${maxRetries})`);
120
+ await (0, util_1.sleep)(sleepInterval);
140
121
  }
141
122
  throw new base_driver_1.errors.NoSuchWindowError(`No window was found with name or handle '${nameOrHandle}'.`);
142
123
  }
143
124
  async function closeApp() {
144
- const rootId = await this.sendCommand('saveRootElementToTable', {});
145
- if (!rootId) {
125
+ const result = await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildCommand());
126
+ const elementId = result.split('\n').map((id) => id.trim()).filter(Boolean)[0];
127
+ if (!elementId) {
146
128
  throw new base_driver_1.errors.NoSuchWindowError('No active app window is found for this session.');
147
129
  }
148
- await this.sendCommand('closeWindow', { elementId: rootId });
149
- await this.sendCommand('setRootElementNull', {});
130
+ await this.sendPowerShellCommand(new powershell_1.FoundAutomationElement(elementId).buildCloseCommand());
131
+ await this.sendPowerShellCommand(/* ps1 */ `$rootElement = $null`);
150
132
  }
151
133
  async function launchApp() {
152
134
  if (!this.caps.app || ['root', 'none'].includes(this.caps.app.toLowerCase())) {
@@ -157,103 +139,55 @@ async function launchApp() {
157
139
  async function changeRootElement(pathOrNativeWindowHandle) {
158
140
  if (typeof pathOrNativeWindowHandle === 'number') {
159
141
  const nativeWindowHandle = pathOrNativeWindowHandle;
160
- const elementId = await this.sendCommand('setRootElementFromHandle', { handle: nativeWindowHandle });
161
- if (elementId) {
142
+ const condition = new powershell_1.PropertyCondition(powershell_1.Property.NATIVE_WINDOW_HANDLE, new powershell_1.PSInt32(nativeWindowHandle));
143
+ const elementId = await this.sendPowerShellCommand(powershell_1.AutomationElement.rootElement.findFirst(powershell_1.TreeScope.CHILDREN_OR_SELF, condition).buildCommand());
144
+ if (elementId.trim() !== '') {
145
+ await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new powershell_1.FoundAutomationElement(elementId).buildCommand()}`);
162
146
  (0, user32_1.trySetForegroundWindow)(nativeWindowHandle);
147
+ const pidResult = await this.sendPowerShellCommand(`$rootElement.Current.ProcessId`);
148
+ const pid = Number(pidResult.trim());
149
+ if (!isNaN(pid) && pid > 0) {
150
+ this.appProcessIds = [pid];
151
+ }
163
152
  return;
164
153
  }
165
154
  throw new base_driver_1.errors.UnknownError('Failed to locate top level window with that window handle.');
166
155
  }
167
156
  const path = pathOrNativeWindowHandle;
168
- const isUwp = path.includes('!') && path.includes('_') && !(path.includes('/') || path.includes('\\'));
169
- const waitForAppLaunchMs = normalizeWaitForAppLaunchMs(this.caps['ms:waitForAppLaunch']);
170
- // When ms:waitForAppLaunch is set, use it as an overall deadline for
171
- // all retry loops. When not set, fall back to fixed iteration counts.
172
- const deadline = waitForAppLaunchMs ? Date.now() + waitForAppLaunchMs : null;
173
- if (isUwp) {
157
+ if ((0, util_1.isUwpAppId)(path)) {
174
158
  this.log.debug('Detected app path to be in the UWP format.');
175
- // Snapshot all visible windows before launch so we can detect new ones.
176
- // WinUI 3 packaged apps (e.g. Windows 11 Calculator) run in their own
177
- // process rather than being hosted inside ApplicationFrameHost like
178
- // legacy UWP apps. The snapshot lets us tell apart the new app window
179
- // from pre-existing windows that belong to other apps or to
180
- // ApplicationFrameHost itself.
181
- const handlesBeforeLaunch = new Set((0, user32_1.getAllWindowHandles)());
182
- await this.sendCommand('startProcess', {
183
- path: 'explorer.exe',
184
- arguments: `shell:AppsFolder\\${path}`,
185
- });
186
- // Outer retry loop: polls until the app window is found or the
187
- // deadline / attempt limit is reached. Each iteration is one
188
- // POLL_INTERVAL_MS tick — no blind sleep up front.
189
- for (let i = 1; deadline ? Date.now() < deadline : i <= 10; i++) {
190
- // Strategy 1: new window detection.
191
- // Works for WinUI 3 / standalone packaged apps that spawn a new
192
- // top-level window in their own process.
193
- const newHandles = (0, user32_1.getAllWindowHandles)().filter((h) => !handlesBeforeLaunch.has(h));
194
- if (newHandles.length > 0) {
195
- this.log.debug(`Found ${newHandles.length} new window(s) since launch: ${newHandles.map((h) => `0x${h.toString(16).padStart(8, '0')}`).join(', ')}`);
196
- try {
197
- if (await this.attachToWindowHandles(newHandles)) {
198
- return;
199
- }
200
- }
201
- catch (err) {
202
- if (err instanceof Error) {
203
- this.log.debug(`New-window strategy failed: ${err.message}`);
204
- }
205
- }
206
- }
207
- // Strategy 2: ApplicationFrameHost.
208
- // Works for legacy hosted UWP apps. Prefer newly appeared
209
- // ApplicationFrameHost windows (i.e. the frame that opened for
210
- // this specific launch) to avoid accidentally attaching to a
211
- // pre-existing frame that belongs to another UWP app.
212
- const processIds = await this.sendCommand('getProcessIds', { processName: 'ApplicationFrameHost' });
213
- if (processIds.length > 0) {
214
- const afhHandles = (0, user32_1.getWindowAllHandlesForProcessIds)(processIds);
215
- const newAfhHandles = afhHandles.filter((h) => !handlesBeforeLaunch.has(h));
216
- const handlesToTry = newAfhHandles.length > 0 ? newAfhHandles : afhHandles;
217
- this.log.debug(`ApplicationFrameHost strategy: trying ${handlesToTry.length} handle(s).`);
218
- try {
219
- if (await this.attachToWindowHandles(handlesToTry)) {
220
- return;
221
- }
222
- }
223
- catch (err) {
224
- if (err instanceof Error) {
225
- this.log.debug(`ApplicationFrameHost strategy failed: ${err.message}`);
226
- }
227
- }
228
- }
229
- this.log.info(`Failed to locate window of the app. Sleeping for ${POLL_INTERVAL_MS}ms and retrying... (attempt ${i})`);
230
- await (0, util_1.sleep)(POLL_INTERVAL_MS);
159
+ await this.sendPowerShellCommand(/* ps1 */ `Start-Process 'explorer.exe' 'shell:AppsFolder\\${path}'${this.caps.appArguments ? ` -ArgumentList '${this.caps.appArguments}'` : ''}`);
160
+ const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name 'ApplicationFrameHost').Id`);
161
+ const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number);
162
+ this.log.debug(`Process IDs of ApplicationFrameHost processes (${processIds.length}): ` + processIds.join(', '));
163
+ this.appProcessIds = processIds;
164
+ await this.attachToApplicationWindow(processIds);
165
+ const attachedPid = Number((await this.sendPowerShellCommand(`$rootElement.Current.ProcessId`)).trim());
166
+ if (!isNaN(attachedPid) && attachedPid > 0) {
167
+ this.appProcessIds = [attachedPid];
231
168
  }
232
169
  }
233
170
  else {
234
171
  this.log.debug('Detected app path to be in the classic format.');
235
172
  const normalizedPath = (0, node_path_1.normalize)(path);
236
- const launcherPid = await this.sendCommand('startProcess', {
237
- path: normalizedPath,
238
- arguments: this.caps.appArguments ?? null,
239
- workingDir: this.caps.appWorkingDir ?? null,
240
- waitForAppLaunchMs: waitForAppLaunchMs || null,
241
- });
242
- if (!launcherPid) {
243
- throw new base_driver_1.errors.UnknownError('Process did not start (no PID returned).');
173
+ await this.sendPowerShellCommand(/* ps1 */ `Start-Process '${normalizedPath}'${this.caps.appArguments ? ` -ArgumentList '${this.caps.appArguments}'` : ''}`);
174
+ const breadcrumbs = normalizedPath.toLowerCase().split('\\').flatMap((x) => x.split('/'));
175
+ const executable = breadcrumbs[breadcrumbs.length - 1];
176
+ const processName = executable.endsWith('.exe') ? executable.slice(0, executable.length - 4) : executable;
177
+ const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name '${processName}' | Sort-Object StartTime -Descending).Id`);
178
+ const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number);
179
+ this.log.debug(`Process IDs of '${processName}' processes: ` + processIds.join(', '));
180
+ this.appProcessIds = processIds;
181
+ await this.attachToApplicationWindow(processIds);
182
+ const attachedPid = Number((await this.sendPowerShellCommand(`$rootElement.Current.ProcessId`)).trim());
183
+ if (!isNaN(attachedPid) && attachedPid > 0) {
184
+ this.appProcessIds = [attachedPid];
244
185
  }
245
- const result = await this.attachToApplicationWindow(launcherPid, { deadline });
246
- if (!result.focused && deadline && Date.now() < deadline) {
247
- this.log.info('Attached to a window that cannot receive focus (likely a splash screen). Waiting for the main window...');
248
- await this.waitForMainWindow(result.knownPids, deadline);
249
- }
250
- return;
251
186
  }
252
- throw new base_driver_1.errors.UnknownError('Failed to locate window of the app.');
253
187
  }
254
188
  async function back() {
255
- const rootId = (await this.sendCommand('saveRootElementToTable', {}))?.trim();
256
- if (!rootId) {
189
+ const elementId = (await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildCommand())).trim();
190
+ if (!elementId) {
257
191
  throw new base_driver_1.errors.NoSuchWindowError('No active window found for this session.');
258
192
  }
259
193
  (0, user32_1.keyDown)(enums_1.Key.ALT);
@@ -262,8 +196,8 @@ async function back() {
262
196
  (0, user32_1.keyUp)(enums_1.Key.ALT);
263
197
  }
264
198
  async function forward() {
265
- const rootId = (await this.sendCommand('saveRootElementToTable', {}))?.trim();
266
- if (!rootId) {
199
+ const elementId = (await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildCommand())).trim();
200
+ if (!elementId) {
267
201
  throw new base_driver_1.errors.NoSuchWindowError('No active window found for this session.');
268
202
  }
269
203
  (0, user32_1.keyDown)(enums_1.Key.ALT);
@@ -272,27 +206,11 @@ async function forward() {
272
206
  (0, user32_1.keyUp)(enums_1.Key.ALT);
273
207
  }
274
208
  async function title() {
275
- const rootId = (await this.sendCommand('saveRootElementToTable', {}))?.trim();
276
- if (!rootId) {
277
- throw new base_driver_1.errors.NoSuchWindowError('No active window found for this session.');
278
- }
279
- return await this.sendCommand('getProperty', { elementId: rootId, property: 'Name' });
280
- }
281
- async function maximizeWindow() {
282
- const elementId = (await this.sendCommand('saveRootElementToTable', {}))?.trim();
283
- if (!elementId) {
284
- throw new base_driver_1.errors.NoSuchWindowError('No active window found for this session.');
285
- }
286
- await this.sendCommand('maximizeWindow', { elementId });
287
- return await this.getWindowRect();
288
- }
289
- async function minimizeWindow() {
290
- const elementId = (await this.sendCommand('saveRootElementToTable', {}))?.trim();
209
+ const elementId = (await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildCommand())).trim();
291
210
  if (!elementId) {
292
211
  throw new base_driver_1.errors.NoSuchWindowError('No active window found for this session.');
293
212
  }
294
- await this.sendCommand('minimizeWindow', { elementId });
295
- return await this.getWindowRect();
213
+ return await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildGetPropertyCommand(powershell_1.Property.NAME));
296
214
  }
297
215
  async function setWindowRect(x, y, width, height) {
298
216
  if (width !== null && width < 0) {
@@ -301,253 +219,67 @@ async function setWindowRect(x, y, width, height) {
301
219
  if (height !== null && height < 0) {
302
220
  throw new base_driver_1.errors.InvalidArgumentError('height must be a non-negative integer.');
303
221
  }
304
- const elementId = (await this.sendCommand('saveRootElementToTable', {}))?.trim();
222
+ const elementId = (await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildCommand())).trim();
305
223
  if (!elementId) {
306
224
  throw new base_driver_1.errors.NoSuchWindowError('No active window found for this session.');
307
225
  }
308
- // TransformPattern.Move/Resize fail when window is maximized or minimized;
309
- // restore to normal state first so CanMove/CanResize are true.
310
- await this.sendCommand('restoreWindow', { elementId });
226
+ const el = new powershell_1.FoundAutomationElement(elementId);
311
227
  if (x !== null && y !== null) {
312
- await this.sendCommand('moveWindow', { elementId, x, y });
228
+ await this.sendPowerShellCommand(el.buildMoveCommand(x, y));
313
229
  }
314
230
  if (width !== null && height !== null) {
315
- await this.sendCommand('resizeWindow', { elementId, width, height });
231
+ await this.sendPowerShellCommand(el.buildResizeCommand(width, height));
316
232
  }
317
233
  return await this.getWindowRect();
318
234
  }
319
- /**
320
- * Polls for a visible Win32 window in the process tree rooted at launcherPid.
321
- *
322
- * On each iteration, discovers children of every known PID via
323
- * getChildProcessIds and accumulates them so they survive if the launcher
324
- * exits before the next poll. This lets stub-launcher apps (e.g. the
325
- * notepad.exe that immediately re-execs the real one in a child process) be
326
- * detected without waiting out the full timeout on a windowless PID.
327
- *
328
- * Returns the HWND of the found window together with all discovered PIDs so
329
- * the caller can use them for subsequent ProcessId-based searches without a
330
- * separate getProcessIds round-trip.
331
- */
332
- async function waitForNewWindow(launcherPid, timeout) {
235
+ async function attachToApplicationWindow(processIds) {
236
+ const trackedPids = new Set(processIds);
237
+ this.log.debug(`Attaching to application window. Process IDs: [${[...trackedPids].join(', ')}]`);
238
+ const timeout = (this.caps['ms:waitForAppLaunch'] ?? 0) * 1000 || SLEEP_INTERVAL_MS * 20;
333
239
  const start = Date.now();
334
240
  let attempts = 0;
335
- const knownPids = new Set([launcherPid]);
336
241
  while (Date.now() - start < timeout) {
337
- for (const pid of [...knownPids]) {
338
- const childPids = await this.sendCommand('getChildProcessIds', { parentPid: pid });
339
- for (const child of childPids) {
340
- knownPids.add(child);
242
+ // Discover child processes of all currently-tracked PIDs
243
+ const pidList = [...trackedPids].join(', ');
244
+ const childPidResult = await this.sendPowerShellCommand(
245
+ /* ps1 */ `@(Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -in @(${pidList}) }).ProcessId`);
246
+ for (const token of childPidResult.split('\n').map((s) => s.trim()).filter(Boolean)) {
247
+ const childPid = Number(token);
248
+ if (!isNaN(childPid) && childPid > 0 && !trackedPids.has(childPid)) {
249
+ this.log.debug(`Discovered child process PID ${childPid} spawned by tracked PIDs`);
250
+ trackedPids.add(childPid);
341
251
  }
342
252
  }
343
- const handles = (0, user32_1.getWindowAllHandlesForProcessIds)([...knownPids]);
253
+ const currentPids = [...trackedPids];
254
+ const handles = (0, user32_1.getWindowAllHandlesForProcessIds)(currentPids);
344
255
  if (handles.length > 0) {
345
- return { handle: handles[handles.length - 1], knownPids: [...knownPids] };
346
- }
347
- this.log.debug(`Waiting for the process window to appear... (${++attempts}/${Math.floor(timeout / POLL_INTERVAL_MS)})`);
348
- await (0, util_1.sleep)(POLL_INTERVAL_MS);
349
- }
350
- throw new Error('Timed out waiting for window.');
351
- }
352
- /**
353
- * Iterates a list of Win32 window handles, finds the first one whose UIA
354
- * subtree has at least two keyboard-focusable descendants (indicating it is a
355
- * real, loaded app window rather than a splash or empty frame), sets it as the
356
- * session root, and brings it to the foreground.
357
- *
358
- * Returns true if a window was successfully attached, false if none qualified.
359
- */
360
- async function attachToWindowHandles(handles) {
361
- let fallbackElementId = '';
362
- for (const hwnd of handles) {
363
- let candidateId = '';
364
- try {
365
- candidateId = await this.sendCommand('elementFromHandle', { handle: hwnd }) ?? '';
366
- }
367
- catch {
368
- continue;
369
- }
370
- if (!candidateId) {
371
- continue;
372
- }
373
- if (!fallbackElementId) {
374
- fallbackElementId = candidateId;
375
- }
376
- const focusables = await this.sendCommand('findElements', {
377
- scope: 'descendants',
378
- condition: (0, conditions_1.propertyCondition)('IsKeyboardFocusable', true),
379
- contextElementId: candidateId,
380
- });
381
- if (focusables && focusables.length >= 2) {
382
- this.log.info(`Attaching to window 0x${hwnd.toString(16).padStart(8, '0')} (${focusables.length} focusable descendants).`);
383
- await this.sendCommand('setRootElementFromElementId', { elementId: candidateId });
384
- const isNotNull = await this.sendCommand('checkRootElementNotNull', {});
385
- if (isNotNull) {
386
- const rootId = await this.sendCommand('saveRootElementToTable', {});
387
- const nwh = Number(await this.sendCommand('getProperty', { elementId: rootId, property: 'NativeWindowHandle' }));
388
- (0, user32_1.trySetForegroundWindow)(nwh);
389
- }
390
- return true;
391
- }
392
- }
393
- return false;
394
- }
395
- /**
396
- * Attaches the driver session to the window spawned by launcherPid.
397
- *
398
- * Delegates process-tree discovery to waitForNewWindow, then resolves the
399
- * Win32 HWND to a UIA element via elementFromHandle (with a short ProcessId
400
- * fallback for the rare case where UIA registration lags the Win32 window).
401
- * Returns focused=false when the attached window looks like a splash screen
402
- * (fewer than 2 keyboard-focusable descendants), signalling the caller to
403
- * run waitForMainWindow.
404
- */
405
- async function attachToApplicationWindow(launcherPid, options = {}) {
406
- const { deadline = null } = options;
407
- const waitForAppLaunchMs = normalizeWaitForAppLaunchMs(this.caps['ms:waitForAppLaunch']) || POLL_INTERVAL_MS * MAX_POLL_ATTEMPTS;
408
- const windowTimeout = deadline
409
- ? Math.min(waitForAppLaunchMs, Math.max(0, deadline - Date.now()))
410
- : waitForAppLaunchMs;
411
- const { handle: nativeWindowHandle, knownPids } = await waitForNewWindow.call(this, launcherPid, windowTimeout);
412
- let elementId = '';
413
- // Short retry for UIA registration lag: the Win32 window exists (waitForNewWindow
414
- // confirmed it), but the UIA COM provider may take a moment to initialise.
415
- // 5 attempts × 200 ms = 1 s — enough for any real app; avoids the old 6 s burn.
416
- for (let i = 1; i <= 5; i++) {
417
- try {
418
- elementId = await this.sendCommand('elementFromHandle', { handle: nativeWindowHandle }) ?? '';
419
- }
420
- catch {
421
- elementId = '';
422
- }
423
- if (!elementId) {
424
- for (const pid of knownPids) {
425
- const foundId = await this.sendCommand('findElement', {
426
- scope: 'children',
427
- condition: (0, conditions_1.propertyCondition)('ProcessId', pid),
428
- contextElementId: null,
429
- }) ?? '';
430
- if (foundId) {
431
- this.log.info(`Found window by ProcessId ${pid} (handle mismatch: Win32=0x${nativeWindowHandle.toString(16).padStart(8, '0')})`);
432
- elementId = foundId;
433
- break;
434
- }
435
- }
436
- }
437
- if (elementId) {
438
- break;
439
- }
440
- this.log.debug(`Window 0x${nativeWindowHandle.toString(16).padStart(8, '0')} not yet in UIA tree, retry ${i}/5...`);
441
- await (0, util_1.sleep)(POLL_INTERVAL_MS);
442
- }
443
- if (!elementId) {
444
- throw new base_driver_1.errors.UnknownError(`Window 0x${nativeWindowHandle.toString(16).padStart(8, '0')} did not appear in the UI Automation tree.`);
445
- }
446
- await this.sendCommand('setRootElementFromElementId', { elementId });
447
- const isNotNull = await this.sendCommand('checkRootElementNotNull', {});
448
- if (isNotNull) {
449
- const rootId = await this.sendCommand('saveRootElementToTable', {});
450
- const nwh = Number(await this.sendCommand('getProperty', { elementId: rootId, property: 'NativeWindowHandle' }));
451
- let focused = (0, user32_1.trySetForegroundWindow)(nwh);
452
- if (!focused) {
453
- try {
454
- await this.focusElement({ [base_driver_1.W3C_ELEMENT_KEY]: elementId });
455
- focused = true;
456
- }
457
- catch {
458
- // Window cannot receive focus (e.g. splash screen)
459
- }
460
- }
461
- // Splash-screen guard: probe for focusable descendants.
462
- // Splash screens have 0–1; real app windows have several.
463
- if (focused) {
464
- try {
465
- const focusables = await this.sendCommand('findElements', {
466
- scope: 'descendants',
467
- condition: (0, conditions_1.propertyCondition)('IsKeyboardFocusable', true),
468
- contextElementId: elementId,
469
- });
470
- if (!focusables || focusables.length < 2) {
471
- this.log.info(`Attached window has only ${focusables?.length ?? 0} focusable descendant(s); treating as a splash screen.`);
472
- focused = false;
256
+ this.log.debug(`Found ${handles.length} window handle(s) for PIDs [${currentPids.join(', ')}]: ${handles.map((h) => `0x${h.toString(16).padStart(8, '0')}`).join(', ')}`);
257
+ for (const handle of handles) {
258
+ const elementId = await this.sendPowerShellCommand(powershell_1.AutomationElement.rootElement.findFirst(powershell_1.TreeScope.CHILDREN, new powershell_1.PropertyCondition(powershell_1.Property.NATIVE_WINDOW_HANDLE, new powershell_1.PSInt32(handle))).buildCommand());
259
+ if (elementId.trim()) {
260
+ await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new powershell_1.FoundAutomationElement(elementId).buildCommand()}`);
261
+ if ((await this.sendPowerShellCommand(/* ps1 */ `$null -ne $rootElement`)).toLowerCase() === 'true') {
262
+ const confirmedHandle = Number(await this.sendPowerShellCommand(powershell_1.AutomationElement.automationRoot.buildGetPropertyCommand(powershell_1.Property.NATIVE_WINDOW_HANDLE)));
263
+ this.log.info(`Successfully attached to window. Native window handle: 0x${confirmedHandle.toString(16).padStart(8, '0')}`);
264
+ this.appProcessIds = currentPids;
265
+ if (!(0, user32_1.trySetForegroundWindow)(confirmedHandle)) {
266
+ try {
267
+ await this.focusElement({
268
+ [base_driver_1.W3C_ELEMENT_KEY]: elementId,
269
+ });
270
+ }
271
+ catch (e) {
272
+ this.log.warn(`Could not focus app window: ${e.message}`);
273
+ }
274
+ }
275
+ return;
276
+ }
473
277
  }
474
278
  }
475
- catch {
476
- // leave focused as-is
477
- }
478
- }
479
- return { focused, knownPids };
480
- }
481
- return { focused: false, knownPids };
482
- }
483
- /**
484
- * Waits for a splash screen to disappear and re-attaches to the app's main window.
485
- *
486
- * After `attachToApplicationWindow` attaches to a window that can't receive focus
487
- * (a splash screen), this function polls until the current root element becomes
488
- * stale (splash screen closed) or a new, different window handle appears for the
489
- * same process. It then re-attaches to the new main window.
490
- */
491
- async function waitForMainWindow(knownPids, deadline) {
492
- // Capture the splash screen's window handle so we can detect when a new window appears
493
- let splashHandle = null;
494
- try {
495
- const rootId = await this.sendCommand('saveRootElementToTable', {});
496
- splashHandle = Number(await this.sendCommand('getProperty', { elementId: rootId, property: 'NativeWindowHandle' }));
497
- }
498
- catch {
499
- // If we can't get the handle, we'll just watch for staleness
500
- }
501
- this.log.debug(`Splash screen handle: ${splashHandle != null ? `0x${splashHandle.toString(16).padStart(8, '0')}` : 'unknown'}. Polling for main window...`);
502
- let attempt = 0;
503
- while (Date.now() < deadline) {
504
- attempt++;
505
- await (0, util_1.sleep)(POLL_INTERVAL_MS);
506
- // Check if the current root element is still alive
507
- let rootStale = false;
508
- try {
509
- const isNotNull = await this.sendCommand('checkRootElementNotNull', {});
510
- if (!isNotNull) {
511
- rootStale = true;
512
- }
513
- else {
514
- // Try to read a property — if the element is stale, this throws
515
- const rootId = await this.sendCommand('saveRootElementToTable', {});
516
- await this.sendCommand('getProperty', { elementId: rootId, property: 'Name' });
517
- }
518
- }
519
- catch {
520
- rootStale = true;
521
- }
522
- if (rootStale) {
523
- this.log.info(`Splash screen closed (root element stale after ${attempt} polls). Re-attaching to main window via attachToApplicationWindow...`);
524
- // Clear the stale root so attachToApplicationWindow starts fresh
525
- await this.sendCommand('setRootElementNull', {});
526
- // attachToApplicationWindow has its own polling loop for the UIA tree,
527
- // so it will wait until the new main window registers.
528
- const result = await this.attachToApplicationWindow(knownPids[0], { deadline });
529
- if (result.focused) {
530
- this.log.info('Successfully attached to the main application window after splash screen.');
531
- }
532
- else {
533
- this.log.info('Attached to main window (could not focus, but window found).');
534
- }
535
- return;
536
- }
537
- // Root is still alive — check if the process now has a different (new) window
538
- if (splashHandle != null) {
539
- const handles = (0, user32_1.getWindowAllHandlesForProcessIds)(knownPids);
540
- const newHandle = handles.find((h) => h !== splashHandle);
541
- if (newHandle) {
542
- this.log.info(`New window detected (0x${newHandle.toString(16).padStart(8, '0')}) alongside splash screen (attempt ${attempt}). Waiting for splash to close...`);
543
- // Don't re-attach yet — let the splash finish naturally.
544
- // Once it closes, the root-stale path above will handle re-attach.
545
- }
546
- }
547
- if (attempt % 10 === 0) {
548
- this.log.debug(`Still waiting for splash screen to close... (attempt ${attempt})`);
549
279
  }
280
+ this.log.debug(`No attachable window found yet. Sleeping for ${SLEEP_INTERVAL_MS} milliseconds and retrying... (${++attempts}/${Math.floor(timeout / SLEEP_INTERVAL_MS)})`);
281
+ await (0, util_1.sleep)(SLEEP_INTERVAL_MS);
550
282
  }
551
- this.log.warn('Deadline reached while waiting for splash screen to close. Session is attached to the splash screen window.');
283
+ throw new Error('Timed out waiting to attach to application window.');
552
284
  }
553
285
  //# sourceMappingURL=app.js.map