Skip to content

Commit a7ce03f

Browse files
authored
Merge pull request #12 from verisoft-ai/fix/window-handle-access
Fix/window handle access
2 parents c9d3529 + d281444 commit a7ce03f

File tree

6 files changed

+64
-5
lines changed

6 files changed

+64
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ prerun | An object containing either `script` or `command` key. The value of eac
4949
postrun | An object containing either `script` or `command` key. The value of each key must be a valid PowerShell script or command to be executed after WinAppDriver session is stopped. See [Power Shell commands execution](#power-shell-commands-execution) for more details.
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"}`.
52+
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`.
5253

5354
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.
5455

lib/commands/app.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export async function getWindowHandle(this: NovaWindowsDriver): Promise<string>
9191
}
9292

9393
export async function getWindowHandles(this: NovaWindowsDriver): Promise<string[]> {
94+
if (this.appProcessIds.length > 0 && !this.caps.returnAllWindowHandles) {
95+
const handles = getWindowAllHandlesForProcessIds(this.appProcessIds);
96+
return handles.map((h) => `0x${h.toString(16).padStart(8, '0')}`);
97+
}
98+
9499
const result = await this.sendPowerShellCommand(AutomationElement.rootElement.findAll(TreeScope.CHILDREN, new TrueCondition()).buildCommand());
95100
const elIds = result.split('\n').map((x) => x.trim()).filter(Boolean);
96101
const nativeWindowHandles: string[] = [];
@@ -163,6 +168,11 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
163168
if (elementId.trim() !== '') {
164169
await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new FoundAutomationElement(elementId).buildCommand()}`);
165170
trySetForegroundWindow(nativeWindowHandle);
171+
const pidResult = await this.sendPowerShellCommand(`$rootElement.Current.ProcessId`);
172+
const pid = Number(pidResult.trim());
173+
if (!isNaN(pid) && pid > 0) {
174+
this.appProcessIds = [pid];
175+
}
166176
return;
167177
}
168178

@@ -177,7 +187,12 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
177187
const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name 'ApplicationFrameHost').Id`);
178188
const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number);
179189
this.log.debug(`Process IDs of ApplicationFrameHost processes (${processIds.length}): ` + processIds.join(', '));
190+
this.appProcessIds = processIds;
180191
await this.attachToApplicationWindow(processIds);
192+
const attachedPid = Number((await this.sendPowerShellCommand(`$rootElement.Current.ProcessId`)).trim());
193+
if (!isNaN(attachedPid) && attachedPid > 0) {
194+
this.appProcessIds = [attachedPid];
195+
}
181196
} else {
182197
this.log.debug('Detected app path to be in the classic format.');
183198
const normalizedPath = normalize(path);
@@ -188,7 +203,12 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
188203
const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name '${processName}' | Sort-Object StartTime -Descending).Id`);
189204
const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number);
190205
this.log.debug(`Process IDs of '${processName}' processes: ` + processIds.join(', '));
206+
this.appProcessIds = processIds;
191207
await this.attachToApplicationWindow(processIds);
208+
const attachedPid = Number((await this.sendPowerShellCommand(`$rootElement.Current.ProcessId`)).trim());
209+
if (!isNaN(attachedPid) && attachedPid > 0) {
210+
this.appProcessIds = [attachedPid];
211+
}
192212
}
193213
}
194214

lib/constraints.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export const UI_AUTOMATION_DRIVER_CONSTRAINTS = {
4545
'ms:forcequit': {
4646
isBoolean: true,
4747
},
48+
returnAllWindowHandles: {
49+
isBoolean: true,
50+
},
4851
} as const satisfies Constraints;
4952

5053
export default UI_AUTOMATION_DRIVER_CONSTRAINTS;

lib/driver.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export class NovaWindowsDriver extends BaseDriver<NovaWindowsDriverConstraints,
6060
powerShell?: ChildProcessWithoutNullStreams;
6161
powerShellStdOut: string = '';
6262
powerShellStdErr: string = '';
63+
appProcessIds: number[] = [];
6364
keyboardState: KeyboardState = {
6465
pressed: new Set(),
6566
alt: false,

test/e2e/window.e2e.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
22
import type { Browser } from 'webdriverio';
3-
import { createCalculatorSession, createRootSession, quitSession } from './helpers/session.js';
3+
import { closeAllTestApps, createCalculatorSession, createRootSession, quitSession } from './helpers/session.js';
44

55
describe('Window and app management commands', () => {
66
let calc: Browser;
7+
let calcAllHandles: Browser;
78
let root: Browser;
89

910
beforeAll(async () => {
1011
calc = await createCalculatorSession();
12+
calcAllHandles = await createCalculatorSession({ 'appium:returnAllWindowHandles': true });
1113
root = await createRootSession();
1214
});
1315

1416
afterAll(async () => {
1517
await quitSession(calc);
18+
await quitSession(calcAllHandles);
1619
await quitSession(root);
20+
closeAllTestApps();
1721
});
1822

1923
describe('getWindowHandle', () => {
@@ -30,13 +34,39 @@ describe('Window and app management commands', () => {
3034
});
3135

3236
describe('getWindowHandles', () => {
33-
it('returns an array from the Root session', async () => {
34-
const handles = await root.getWindowHandles();
35-
expect(Array.isArray(handles)).toBe(true);
37+
it('(app session, default) returns only the app windows — not all desktop windows', async () => {
38+
const appHandles = await calc.getWindowHandles();
39+
expect(appHandles.length).toBeGreaterThanOrEqual(1);
40+
});
41+
42+
it('(app session, default) includes the current window handle', async () => {
43+
const current = await calc.getWindowHandle();
44+
const handles = await calc.getWindowHandles();
45+
expect(handles).toContain(current);
46+
});
47+
48+
it('(app session, default) all returned handles match the 0x hex format', async () => {
49+
const handles = await calc.getWindowHandles();
50+
for (const h of handles) {
51+
expect(h).toMatch(/^0x[0-9a-fA-F]{8}$/);
52+
}
3653
});
3754

38-
it('returns at least one window handle from the desktop', async () => {
55+
it('(returnAllWindowHandles=true) returns all desktop windows, same count as root session', async () => {
56+
const appAllHandles = await calcAllHandles.getWindowHandles();
57+
const rootHandles = await root.getWindowHandles();
58+
expect(appAllHandles.length).toBe(rootHandles.length);
59+
});
60+
61+
it('(returnAllWindowHandles=true) includes the current app window handle', async () => {
62+
const current = await calc.getWindowHandle();
63+
const appAllHandles = await calcAllHandles.getWindowHandles();
64+
expect(appAllHandles).toContain(current);
65+
});
66+
67+
it('(root session) returns an array of at least one window handle', async () => {
3968
const handles = await root.getWindowHandles();
69+
expect(Array.isArray(handles)).toBe(true);
4070
expect(handles.length).toBeGreaterThanOrEqual(1);
4171
});
4272
});

test/fixtures/driver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface MockDriver {
88
sendPowerShellCommand: ReturnType<typeof vi.fn>;
99
log: { debug: ReturnType<typeof vi.fn>; info?: ReturnType<typeof vi.fn> };
1010
assertFeatureEnabled: ReturnType<typeof vi.fn>;
11+
appProcessIds: number[];
12+
caps: Record<string, unknown>;
1113
}
1214

1315
export function createMockDriver(overrides?: Partial<MockDriver>): MockDriver {
@@ -18,6 +20,8 @@ export function createMockDriver(overrides?: Partial<MockDriver>): MockDriver {
1820
sendPowerShellCommand,
1921
log,
2022
assertFeatureEnabled,
23+
appProcessIds: [],
24+
caps: {},
2125
...overrides,
2226
};
2327
return driver;

0 commit comments

Comments
 (0)