Skip to content

Commit a831b3a

Browse files
committed
feat(capability): Add capabilities windowSwitchRetries and windowSwitchInterval
Non-negative number of retries and timeout between retries expected asarguments
1 parent a7ce03f commit a831b3a

File tree

7 files changed

+73
-6
lines changed

7 files changed

+73
-6
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ postrun | An object containing either `script` or `command` key. The value of ea
5050
isolatedScriptExecution | Whether PowerShell scripts are executed in an isolated session. Default is `false`.
5151
appEnvironment | Optional object of custom environment variables to inject into the PowerShell session. The variables are only available for the lifetime of the session and do not affect the system environment. Example: `{"MY_VAR": "hello", "API_URL": "http://localhost:3000"}`.
5252
returnAllWindowHandles | When `true`, `getWindowHandles()` returns all top-level windows on the desktop (UIA root children) instead of only the windows belonging to the launched app. Useful for switching to arbitrary system windows. Default is `false`.
53+
ms:waitForAppLaunch | Time in seconds to wait for the app window to appear after launch. Default is `0` (falls back to 10 000 ms internal timeout).
54+
ms:windowSwitchRetries | Maximum number of retry attempts in `setWindow()` when the target window is not yet visible. Must be a non-negative integer. Default is `20`.
55+
ms:windowSwitchInterval | Sleep duration in milliseconds between each retry in `setWindow()`. Must be a non-negative integer. Default is `500`.
5356

5457
Please note that more capabilities will be added as the development of this driver progresses. Since it is still in its early stages, some features may be missing or subject to change. If you need a specific capability or encounter any issues, please feel free to open an issue.
5558

lib/commands/app.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ export async function getWindowHandles(this: NovaWindowsDriver): Promise<string[
110110

111111
export async function setWindow(this: NovaWindowsDriver, nameOrHandle: string): Promise<void> {
112112
const handle = Number(nameOrHandle);
113-
for (let i = 1; i <= 20; i++) { // TODO: make a setting for the number of retries or timeout
113+
const maxRetries = this.caps['ms:windowSwitchRetries'] ?? 20;
114+
const sleepInterval = this.caps['ms:windowSwitchInterval'] ?? SLEEP_INTERVAL_MS;
115+
for (let i = 1; i <= maxRetries; i++) {
114116
if (!isNaN(handle)) {
115117
const condition = new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(handle));
116118
const elementId = await this.sendPowerShellCommand(AutomationElement.rootElement.findFirst(TreeScope.CHILDREN_OR_SELF, condition).buildCommand());
@@ -133,8 +135,8 @@ export async function setWindow(this: NovaWindowsDriver, nameOrHandle: string):
133135
return;
134136
}
135137

136-
this.log.info(`Failed to locate window with name '${name}'. Sleeping for ${SLEEP_INTERVAL_MS} milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout
137-
await sleep(SLEEP_INTERVAL_MS); // TODO: make a setting for the sleep timeout
138+
this.log.info(`Failed to locate window with name '${name}'. Sleeping for ${sleepInterval} milliseconds and retrying... (${i}/${maxRetries})`);
139+
await sleep(sleepInterval);
138140
}
139141

140142
throw new errors.NoSuchWindowError(`No window was found with name or handle '${nameOrHandle}'.`);

lib/constraints.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export const UI_AUTOMATION_DRIVER_CONSTRAINTS = {
4848
returnAllWindowHandles: {
4949
isBoolean: true,
5050
},
51+
'ms:windowSwitchRetries': {
52+
isNumber: true,
53+
},
54+
'ms:windowSwitchInterval': {
55+
isNumber: true,
56+
},
5157
} as const satisfies Constraints;
5258

5359
export default UI_AUTOMATION_DRIVER_CONSTRAINTS;

lib/driver.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
convertStringToCondition,
2121
} from './powershell';
2222
import {
23-
assertSupportedEasingFunction
23+
assertIntegerCap,
24+
assertSupportedEasingFunction,
2425
} from './util';
2526
import { setDpiAwareness } from './winapi/user32';
2627
import { xpathToElIdOrIds } from './xpath';
@@ -187,6 +188,12 @@ export class NovaWindowsDriver extends BaseDriver<NovaWindowsDriverConstraints,
187188
);
188189
}
189190
}
191+
if (caps['ms:windowSwitchRetries'] !== undefined) {
192+
assertIntegerCap('ms:windowSwitchRetries', caps['ms:windowSwitchRetries'], 0);
193+
}
194+
if (caps['ms:windowSwitchInterval'] !== undefined) {
195+
assertIntegerCap('ms:windowSwitchInterval', caps['ms:windowSwitchInterval'], 0);
196+
}
190197
if (this.caps.shouldCloseApp === undefined) {
191198
this.caps.shouldCloseApp = true; // set default value
192199
}

lib/util.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export function assertSupportedEasingFunction(value: string) {
2626
}
2727
}
2828

29+
export function assertIntegerCap(capName: string, value: number, min: number): void {
30+
if (!Number.isInteger(value) || value < min) {
31+
throw new errors.InvalidArgumentError(
32+
`Invalid capability '${capName}': must be an integer >= ${min} (got ${value}).`
33+
);
34+
}
35+
}
36+
2937
export function sleep(ms: number): Promise<void> {
3038
return new Promise((resolve) => setTimeout(resolve, Math.max(ms, 0)));
3139
}

test/commands/app/app.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,23 @@ describe('setWindow', () => {
125125

126126
it('throws NoSuchWindowError when window is not found after retries', async () => {
127127
const driver = createMockDriver() as any;
128+
driver.caps['ms:windowSwitchRetries'] = 1;
129+
driver.caps['ms:windowSwitchInterval'] = 0;
128130
// All calls return empty (window not found)
129131
driver.sendPowerShellCommand.mockResolvedValue('');
130132

131133
await expect(setWindow.call(driver, 'NonExistentWindow')).rejects.toThrow('No window was found');
132-
}, 10000);
134+
});
135+
136+
it('respects ms:windowSwitchRetries cap', async () => {
137+
const driver2 = createMockDriver() as any;
138+
driver2.caps['ms:windowSwitchRetries'] = 2;
139+
driver2.caps['ms:windowSwitchInterval'] = 0;
140+
driver2.sendPowerShellCommand.mockResolvedValue('');
141+
142+
// Use a numeric handle string so both the handle search and name search run per iteration (2 PS calls each)
143+
await expect(setWindow.call(driver2, '99999')).rejects.toThrow('No window was found');
144+
// 2 retries × 2 PS commands (handle search + name search) = 4 calls
145+
expect(driver2.sendPowerShellCommand.mock.calls.length).toBe(4);
146+
});
133147
});

test/util.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,34 @@
22
* Unit tests for lib/util.ts
33
*/
44
import { describe, it, expect } from 'vitest';
5-
import { assertSupportedEasingFunction, $ } from '../lib/util';
5+
import { assertIntegerCap, assertSupportedEasingFunction, $ } from '../lib/util';
6+
7+
describe('assertIntegerCap', () => {
8+
it('accepts value equal to min', () => {
9+
expect(() => assertIntegerCap('x', 0, 0)).not.toThrow();
10+
expect(() => assertIntegerCap('x', 1, 1)).not.toThrow();
11+
});
12+
13+
it('accepts value above min', () => {
14+
expect(() => assertIntegerCap('x', 5, 1)).not.toThrow();
15+
expect(() => assertIntegerCap('x', 100, 0)).not.toThrow();
16+
});
17+
18+
it('throws for value below min', () => {
19+
expect(() => assertIntegerCap('ms:windowSwitchRetries', 0, 1)).toThrow('must be an integer >= 1');
20+
expect(() => assertIntegerCap('ms:windowSwitchInterval', -1, 0)).toThrow('must be an integer >= 0');
21+
});
22+
23+
it('throws for floats', () => {
24+
expect(() => assertIntegerCap('x', 1.5, 1)).toThrow('must be an integer');
25+
expect(() => assertIntegerCap('x', 0.1, 0)).toThrow('must be an integer');
26+
});
27+
28+
it('includes cap name and received value in error message', () => {
29+
expect(() => assertIntegerCap('ms:windowSwitchRetries', -3, 1)).toThrow("'ms:windowSwitchRetries'");
30+
expect(() => assertIntegerCap('ms:windowSwitchRetries', -3, 1)).toThrow('got -3');
31+
});
32+
});
633

734
describe('assertSupportedEasingFunction', () => {
835
it.each(['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'])(

0 commit comments

Comments
 (0)