Skip to content

Commit 073c566

Browse files
committed
fix(commands): match closeApp and launchApp implementation with appium windows driver
1 parent 9da1025 commit 073c566

File tree

7 files changed

+125
-231
lines changed

7 files changed

+125
-231
lines changed

lib/commands/app.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,23 @@ export async function setWindow(this: NovaWindowsDriver, nameOrHandle: string):
130130
throw new errors.NoSuchWindowError(`No window was found with name or handle '${nameOrHandle}'.`);
131131
}
132132

133+
export async function closeApp(this: NovaWindowsDriver): Promise<void> {
134+
const result = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand());
135+
const elementId = result.split('\n').map((id) => id.trim()).filter(Boolean)[0];
136+
if (!elementId) {
137+
throw new errors.NoSuchWindowError('No active app window is found for this session.');
138+
}
139+
await this.sendPowerShellCommand(new FoundAutomationElement(elementId).buildCloseCommand());
140+
await this.sendPowerShellCommand(/* ps1 */ `$rootElement = $null`);
141+
}
142+
143+
export async function launchApp(this: NovaWindowsDriver): Promise<void> {
144+
if (!this.caps.app || ['root', 'none'].includes(this.caps.app.toLowerCase())) {
145+
throw new errors.InvalidArgumentError('No app capability is set for this session.');
146+
}
147+
await this.changeRootElement(this.caps.app);
148+
}
149+
133150
export async function changeRootElement(this: NovaWindowsDriver, path: string): Promise<void>
134151
export async function changeRootElement(this: NovaWindowsDriver, nativeWindowHandle: number): Promise<void>
135152
export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWindowHandle: string | number): Promise<void> {

lib/commands/extension.ts

Lines changed: 7 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { W3C_ELEMENT_KEY, errors } from '@appium/base-driver';
22
import { Element, Rect } from '@appium/types';
33
import { tmpdir } from 'node:os';
4-
import { join, normalize } from 'node:path';
4+
import { join } from 'node:path';
55
import { POWER_SHELL_FEATURE } from '../constants';
66
import { NovaWindowsDriver } from '../driver';
77
import { ClickType, Enum, Key } from '../enums';
88
import {
99
AutomationElement,
1010
AutomationElementMode,
1111
FoundAutomationElement,
12-
PSInt32,
1312
PSInt32Array,
1413
Property,
1514
PropertyCondition,
@@ -52,8 +51,8 @@ const EXTENSION_COMMANDS = Object.freeze({
5251
minimize: 'patternMinimize',
5352
restore: 'patternRestore',
5453
close: 'patternClose',
55-
closeApp: 'closeApp',
56-
launchApp: 'launchApp',
54+
closeApp: 'windowsCloseApp',
55+
launchApp: 'windowsLaunchApp',
5756
keys: 'executeKeys',
5857
click: 'executeClick',
5958
hover: 'executeHover',
@@ -256,69 +255,12 @@ export async function patternClose(this: NovaWindowsDriver, element: Element): P
256255
await this.sendPowerShellCommand(new FoundAutomationElement(element[W3C_ELEMENT_KEY]).buildCloseCommand());
257256
}
258257

259-
export async function closeApp(this: NovaWindowsDriver, args: {
260-
processId?: number,
261-
processName?: string,
262-
windowHandle?: string | number,
263-
}): Promise<void> {
264-
const { processId, processName, windowHandle } = args ?? {};
265-
const provided = [processId, processName, windowHandle].filter((v) => v != null).length;
266-
267-
if (provided !== 1) {
268-
throw new errors.InvalidArgumentError(
269-
'Exactly one of processId, processName, or windowHandle must be provided.'
270-
);
271-
}
272-
273-
if (processId != null) {
274-
await this.sendPowerShellCommand(`Stop-Process -Id ${processId}`);
275-
return;
276-
}
277-
278-
if (processName != null) {
279-
await this.sendPowerShellCommand(`Stop-Process -Name '${processName}'`);
280-
return;
281-
}
282-
283-
if (windowHandle != null) {
284-
const handle = typeof windowHandle === 'string' ? parseInt(windowHandle, 16) : windowHandle;
285-
const condition = new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(handle));
286-
const elementId = await this.sendPowerShellCommand(
287-
AutomationElement.rootElement
288-
.findFirst(TreeScope.CHILDREN_OR_SELF, condition)
289-
.buildCommand()
290-
);
291-
if (!elementId?.trim()) {
292-
throw new errors.NoSuchWindowError(`No window found with handle ${windowHandle}`);
293-
}
294-
const processId = await this.sendPowerShellCommand(
295-
new FoundAutomationElement(elementId.trim()).buildGetPropertyCommand(Property.PROCESS_ID)
296-
);
297-
if (!processId?.trim()) {
298-
throw new errors.UnknownError(`Could not get process ID for window handle ${windowHandle}`);
299-
}
300-
await this.sendPowerShellCommand(`Stop-Process -Id ${processId.trim()}`);
301-
}
258+
export async function windowsCloseApp(this: NovaWindowsDriver): Promise<void> {
259+
return await this.closeApp();
302260
}
303261

304-
export async function launchApp(this: NovaWindowsDriver, args: {
305-
app: string,
306-
appArguments?: string,
307-
}): Promise<void> {
308-
if (!args || typeof args !== 'object' || !args.app) {
309-
throw new errors.InvalidArgumentError("'app' must be provided.");
310-
}
311-
312-
const { app, appArguments } = args;
313-
if (app.includes('!') && app.includes('_') && !(app.includes('/') || app.includes('\\'))) {
314-
this.log.debug('Detected app path to be in the UWP format.');
315-
await this.sendPowerShellCommand(/* ps1 */ `Start-Process 'explorer.exe' 'shell:AppsFolder\\${app}'${appArguments ? ` -ArgumentList '${appArguments}'` : ''}`);
316-
} else {
317-
this.log.debug('Detected app path to be in the classic format.');
318-
const normalizedPath = normalize(app);
319-
await this.sendPowerShellCommand(/* ps1 */ `Start-Process '${normalizedPath}'${appArguments ? ` -ArgumentList '${appArguments}'` : ''}`);
320-
}
321-
await sleep(1500); // Wait for the app to start
262+
export async function windowsLaunchApp(this: NovaWindowsDriver): Promise<void> {
263+
return await this.launchApp();
322264
}
323265

324266
export async function focusElement(this: NovaWindowsDriver, element: Element): Promise<void> {

test/commands/app/closeApp.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Unit tests for the W3C closeApp command (session-scoped).
3+
*/
4+
import { beforeEach, describe, expect, it, vi } from 'vitest';
5+
import { closeApp } from '../../../lib/commands/app';
6+
import { createMockDriver } from '../../fixtures/driver';
7+
8+
describe('closeApp (W3C)', () => {
9+
beforeEach(() => {
10+
vi.clearAllMocks();
11+
});
12+
13+
it('closes the session app window via UI Automation close', async () => {
14+
const driver = createMockDriver() as any;
15+
driver.sendPowerShellCommand
16+
.mockResolvedValueOnce('element-123') // automationRoot.buildCommand()
17+
.mockResolvedValueOnce(undefined) // buildCloseCommand()
18+
.mockResolvedValueOnce(undefined); // $rootElement = $null
19+
20+
await closeApp.call(driver);
21+
22+
expect(driver.sendPowerShellCommand).toHaveBeenCalledTimes(3);
23+
});
24+
25+
it('throws NoSuchWindowError when no root element exists', async () => {
26+
const driver = createMockDriver() as any;
27+
driver.sendPowerShellCommand.mockResolvedValueOnce(''); // empty = window already gone
28+
29+
await expect(closeApp.call(driver)).rejects.toThrow('No active app window');
30+
});
31+
32+
it('throws NoSuchWindowError when root element returns only whitespace', async () => {
33+
const driver = createMockDriver() as any;
34+
driver.sendPowerShellCommand.mockResolvedValueOnce(' \n \n ');
35+
36+
await expect(closeApp.call(driver)).rejects.toThrow('No active app window');
37+
});
38+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Unit tests for the W3C launchApp command (session-scoped).
3+
*/
4+
import { describe, it, expect, vi, beforeEach } from 'vitest';
5+
import { launchApp } from '../../../lib/commands/app';
6+
import { createMockDriver } from '../../fixtures/driver';
7+
8+
describe('launchApp (W3C)', () => {
9+
beforeEach(() => {
10+
vi.clearAllMocks();
11+
});
12+
13+
it('re-launches the session app via changeRootElement', async () => {
14+
const driver = createMockDriver() as any;
15+
driver.caps = { app: 'C:\\Program Files\\notepad.exe' };
16+
driver.changeRootElement = vi.fn().mockResolvedValue(undefined);
17+
18+
await launchApp.call(driver);
19+
20+
expect(driver.changeRootElement).toHaveBeenCalledWith('C:\\Program Files\\notepad.exe');
21+
expect(driver.changeRootElement).toHaveBeenCalledTimes(1);
22+
});
23+
24+
it('re-launches a UWP app via changeRootElement', async () => {
25+
const driver = createMockDriver() as any;
26+
driver.caps = { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' };
27+
driver.changeRootElement = vi.fn().mockResolvedValue(undefined);
28+
29+
await launchApp.call(driver);
30+
31+
expect(driver.changeRootElement).toHaveBeenCalledWith('Microsoft.WindowsCalculator_8wekyb3d8bbwe!App');
32+
});
33+
34+
it('throws InvalidArgumentError when app capability is not set', async () => {
35+
const driver = createMockDriver() as any;
36+
driver.caps = {};
37+
38+
await expect(launchApp.call(driver)).rejects.toThrow('No app capability is set');
39+
});
40+
41+
it('throws InvalidArgumentError when app is "root"', async () => {
42+
const driver = createMockDriver() as any;
43+
driver.caps = { app: 'root' };
44+
45+
await expect(launchApp.call(driver)).rejects.toThrow('No app capability is set');
46+
});
47+
48+
it('throws InvalidArgumentError when app is "none"', async () => {
49+
const driver = createMockDriver() as any;
50+
driver.caps = { app: 'none' };
51+
52+
await expect(launchApp.call(driver)).rejects.toThrow('No app capability is set');
53+
});
54+
});

test/commands/extension/closeApp.test.ts

Lines changed: 0 additions & 94 deletions
This file was deleted.

test/commands/extension/execute.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@ import * as extension from '../../../lib/commands/extension';
66
import { createMockDriver, MOCK_ELEMENT } from '../../fixtures/driver';
77

88
describe('execute (command router)', () => {
9-
let driver: ReturnType<typeof createMockDriver> & Record<string, any>;
9+
let driver: any;
1010

1111
beforeEach(() => {
1212
vi.clearAllMocks();
1313
driver = createMockDriver() as any;
1414
Object.assign(driver, extension);
1515
});
1616

17-
it('routes windows:launchApp to launchApp with args', async () => {
18-
await extension.execute.call(driver, 'windows: launchApp', [{ app: 'notepad.exe' }]);
19-
expect(driver.sendPowerShellCommand).toHaveBeenCalledWith(
20-
expect.stringContaining("Start-Process 'notepad.exe'")
21-
);
17+
it('routes windows:launchApp to windowsLaunchApp', async () => {
18+
driver.launchApp = vi.fn().mockResolvedValue(undefined);
19+
await extension.execute.call(driver, 'windows: launchApp', []);
20+
expect(driver.launchApp).toHaveBeenCalledOnce();
2221
});
2322

24-
it('routes windows:closeApp to closeApp with args', async () => {
25-
await extension.execute.call(driver, 'windows: closeApp', [{ processId: 12345 }]);
26-
expect(driver.sendPowerShellCommand).toHaveBeenCalledWith('Stop-Process -Id 12345');
23+
it('routes windows:closeApp to windowsCloseApp', async () => {
24+
driver.closeApp = vi.fn().mockResolvedValue(undefined);
25+
await extension.execute.call(driver, 'windows: closeApp', []);
26+
expect(driver.closeApp).toHaveBeenCalledOnce();
2727
});
2828

2929
it('routes windows:deleteFile to deleteFile with args', async () => {

0 commit comments

Comments
 (0)