Skip to content

Commit 26db919

Browse files
committed
feat(commands): add support for close app and launch app
1 parent c100ffb commit 26db919

File tree

2 files changed

+118
-2
lines changed

2 files changed

+118
-2
lines changed

README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,11 +532,58 @@ To be implemented.
532532

533533
### windows: launchApp
534534

535-
To be implemented.
535+
Launches an application and waits for it to start. Supports both classic Win32 apps (by path) and UWP apps (by App User Model ID).
536+
537+
#### Arguments
538+
539+
Name | Type | Required | Description | Example
540+
--- | --- | --- | --- | ---
541+
app | string | yes | Path to the executable or UWP App User Model ID (AUMID). Classic format: `C:\Path\To\app.exe`. UWP format: `Microsoft.WindowsCalculator_8wekyb3d8bbwe!App` | notepad.exe
542+
appArguments | string | no | Command-line arguments to pass to the application. | --some-flag
543+
544+
#### Example
545+
546+
```csharp
547+
// Launch Notepad
548+
driver.ExecuteScript("windows: launchApp", new Dictionary<string, object> { { "app", "notepad.exe" } });
549+
550+
// Launch Calculator (UWP)
551+
driver.ExecuteScript("windows: launchApp", new Dictionary<string, object> {
552+
{ "app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App" }
553+
});
554+
555+
// Launch with arguments
556+
driver.ExecuteScript("windows: launchApp", new Dictionary<string, object> {
557+
{ "app", "notepad.exe" },
558+
{ "appArguments", "C:\\path\\to\\file.txt" }
559+
});
560+
```
536561

537562
### windows: closeApp
538563

539-
To be implemented.
564+
Terminates a running application by process ID, process name, or window handle. All three methods force-kill the process (same outcome). For graceful window close, use `windows: close` with an element. Exactly one identifier must be provided.
565+
566+
#### Arguments
567+
568+
Name | Type | Required | Description | Example
569+
--- | --- | --- | --- | ---
570+
processId | number | no | Process ID (PID) of the application to terminate. Uses `Stop-Process -Id`. | 12345
571+
processName | string | no | Process name (e.g. executable name without extension). Terminates all processes with that name. Uses `Stop-Process -Name`. | notepad
572+
windowHandle | string or number | no | Native window handle of the application window. Resolves the window to its process ID, then terminates it via `Stop-Process -Id`. Accepts hex string (e.g. `"0x12345678"`) or number. | 0x12345678
573+
574+
575+
#### Example
576+
577+
```csharp
578+
// Close by process ID
579+
driver.ExecuteScript("windows: closeApp", new Dictionary<string, object> { { "processId", 12345 } });
580+
581+
// Close by process name (e.g. all Notepad instances)
582+
driver.ExecuteScript("windows: closeApp", new Dictionary<string, object> { { "processName", "notepad" } });
583+
584+
// Close by window handle (e.g. from getWindowHandle)
585+
driver.ExecuteScript("windows: closeApp", new Dictionary<string, object> { { "windowHandle", "0x12345678" } });
586+
```
540587

541588
### windows: clickAndDrag
542589

lib/commands/extension.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { normalize } from 'node:path';
12
import { W3C_ELEMENT_KEY, errors } from '@appium/base-driver';
23
import { Element, Rect } from '@appium/types';
34
import { NovaWindowsDriver } from '../driver';
@@ -16,6 +17,7 @@ import {
1617
AutomationElement,
1718
AutomationElementMode,
1819
FoundAutomationElement,
20+
PSInt32,
1921
PSInt32Array,
2022
Property,
2123
PropertyCondition,
@@ -47,6 +49,8 @@ const EXTENSION_COMMANDS = Object.freeze({
4749
minimize: 'patternMinimize',
4850
restore: 'patternRestore',
4951
close: 'patternClose',
52+
closeApp: 'closeApp',
53+
launchApp: 'launchApp',
5054
keys: 'executeKeys',
5155
click: 'executeClick',
5256
hover: 'executeHover',
@@ -244,6 +248,71 @@ export async function patternClose(this: NovaWindowsDriver, element: Element): P
244248
await this.sendPowerShellCommand(new FoundAutomationElement(element[W3C_ELEMENT_KEY]).buildCloseCommand());
245249
}
246250

251+
export async function closeApp(this: NovaWindowsDriver, args: {
252+
processId?: number,
253+
processName?: string,
254+
windowHandle?: string | number,
255+
}): Promise<void> {
256+
const { processId, processName, windowHandle } = args ?? {};
257+
const provided = [processId, processName, windowHandle].filter((v) => v != null).length;
258+
259+
if (provided !== 1) {
260+
throw new errors.InvalidArgumentError(
261+
'Exactly one of processId, processName, or windowHandle must be provided.'
262+
);
263+
}
264+
265+
if (processId != null) {
266+
await this.sendPowerShellCommand(`Stop-Process -Id ${processId}`);
267+
return;
268+
}
269+
270+
if (processName != null) {
271+
await this.sendPowerShellCommand(`Stop-Process -Name '${processName}'`);
272+
return;
273+
}
274+
275+
if (windowHandle != null) {
276+
const handle = typeof windowHandle === 'string' ? parseInt(windowHandle, 16) : windowHandle;
277+
const condition = new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(handle));
278+
const elementId = await this.sendPowerShellCommand(
279+
AutomationElement.rootElement
280+
.findFirst(TreeScope.CHILDREN_OR_SELF, condition)
281+
.buildCommand()
282+
);
283+
if (!elementId?.trim()) {
284+
throw new errors.NoSuchWindowError(`No window found with handle ${windowHandle}`);
285+
}
286+
const processId = await this.sendPowerShellCommand(
287+
new FoundAutomationElement(elementId.trim()).buildGetPropertyCommand(Property.PROCESS_ID)
288+
);
289+
if (!processId?.trim()) {
290+
throw new errors.UnknownError(`Could not get process ID for window handle ${windowHandle}`);
291+
}
292+
await this.sendPowerShellCommand(`Stop-Process -Id ${processId.trim()}`);
293+
}
294+
}
295+
296+
export async function launchApp(this: NovaWindowsDriver, args: {
297+
app: string,
298+
appArguments?: string,
299+
}): Promise<void> {
300+
if (!args || typeof args !== 'object' || !args.app) {
301+
throw new errors.InvalidArgumentError("'app' must be provided.");
302+
}
303+
304+
const { app, appArguments } = args;
305+
if (app.includes('!') && app.includes('_') && !(app.includes('/') || app.includes('\\'))) {
306+
this.log.debug('Detected app path to be in the UWP format.');
307+
await this.sendPowerShellCommand(/* ps1 */ `Start-Process 'explorer.exe' 'shell:AppsFolder\\${app}'${appArguments ? ` -ArgumentList '${appArguments}'` : ''}`);
308+
} else {
309+
this.log.debug('Detected app path to be in the classic format.');
310+
const normalizedPath = normalize(app);
311+
await this.sendPowerShellCommand(/* ps1 */ `Start-Process '${normalizedPath}'${appArguments ? ` -ArgumentList '${appArguments}'` : ''}`);
312+
}
313+
await sleep(1500); // Wait for the app to start
314+
}
315+
247316
export async function focusElement(this: NovaWindowsDriver, element: Element): Promise<void> {
248317
await this.sendPowerShellCommand(new FoundAutomationElement(element[W3C_ELEMENT_KEY]).buildSetFocusCommand());
249318
}

0 commit comments

Comments
 (0)