Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,31 +516,78 @@ Position | Type | Description | Example

### windows: startRecordingScreen

To be implemented.
Starts screen recording using the **bundled ffmpeg** included with the driver. There is no system PATH fallback: if the bundle is not present (e.g. driver was not installed via npm with dependencies), screen recording is not available and the driver reports a clear error.

### windows: stopRecordingScreen

To be implemented.
Stops the current screen recording and returns the video (base64 or uploads to a remote path if specified).

### windows: deleteFile

To be implemented.
Deletes a file on the Windows machine. Uses PowerShell `Remove-Item -Path ... -Force`. Paths containing `[`, `]`, or `?` use `-LiteralPath` for correct interpretation.

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
path | string | yes | Absolute or relative path to the file to delete. | `C:\Temp\file.txt`

### windows: deleteFolder

To be implemented.
Deletes a folder on the Windows machine. Uses PowerShell `Remove-Item -Path ... -Force` with optional `-Recurse`. Paths containing `[`, `]`, or `?` use `-LiteralPath`.

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
path | string | yes | Absolute or relative path to the folder to delete. | `C:\Temp\MyFolder`
recursive | boolean | no | If true (default), delete contents recursively. If false, only remove the folder when empty. | `true`

### windows: launchApp

To be implemented.
Re-launches the application configured in the `app` session capability. The app path or App User Model ID (AUMID) must have been set when the session was created. Typically used to reopen an app after it has been closed with `windows: closeApp`.

This command takes no arguments.

#### Example

```javascript
// Re-launch the app set in the session capability
await driver.executeScript('windows: launchApp', []);
```

### windows: closeApp

To be implemented.
Closes the current root application window by sending a close command via the Windows UI Automation WindowPattern. Clears the root element reference in the session afterward. Throws a `NoSuchWindowError` if no active window is found.

This command takes no arguments.

#### Example

```javascript
// Close the current app window
await driver.executeScript('windows: closeApp', []);
```

### windows: clickAndDrag

To be implemented.
Performs a click-and-drag: move to the start position, press the mouse button, move to the end position over the given duration, then release. Start and end can be specified by element (center or offset) or by screen coordinates. Uses the same Windows input APIs as other pointer actions.

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
startElementId | string | no* | Element ID for drag start. Use *or* startX/startY. | `1.2.3.4.5`
startX | number | no* | X coordinate for drag start (with startY). | `100`
startY | number | no* | Y coordinate for drag start (with startX). | `200`
endElementId | string | no* | Element ID for drag end. Use *or* endX/endY. | `1.2.3.4.6`
endX | number | no* | X coordinate for drag end (with endY). | `300`
endY | number | no* | Y coordinate for drag end (with endX). | `400`
modifierKeys | string or string[] | no | Keys to hold during drag: `shift`, `ctrl`, `alt`, `win`. | `["ctrl"]`
durationMs | number | no | Duration of the move from start to end (default: 500). | `300`
button | string | no | Mouse button: `left` (default), `middle`, `right`, `back`, `forward`. | `left`

\* Provide either startElementId or both startX and startY; and either endElementId or both endX and endY.

## Development

Expand Down
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ import appiumConfig from '@appium/eslint-config-appium-ts';
export default defineConfig(
eslint.configs.recommended,
...appiumConfig,
{
files: ['test/e2e/**/*.ts'],
},
);
30 changes: 30 additions & 0 deletions lib/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,33 @@ export async function handleKeyAction(this: NovaWindowsDriver, action: KeyAction
}
}
}

export async function releaseActions(this: NovaWindowsDriver): Promise<void> {
if (this.keyboardState.shift) {
keyUp(Key.SHIFT);
keyUp(Key.R_SHIFT);
this.keyboardState.shift = false;
}
if (this.keyboardState.ctrl) {
keyUp(Key.CONTROL);
keyUp(Key.R_CONTROL);
this.keyboardState.ctrl = false;
}
if (this.keyboardState.meta) {
keyUp(Key.META);
keyUp(Key.R_META);
this.keyboardState.meta = false;
}
if (this.keyboardState.alt) {
keyUp(Key.ALT);
keyUp(Key.R_ALT);
this.keyboardState.alt = false;
}
for (const key of this.keyboardState.pressed) {
keyUp(key);
}
this.keyboardState.pressed.clear();
mouseUp(0);
mouseUp(1);
mouseUp(2);
}
21 changes: 19 additions & 2 deletions lib/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ export async function setWindow(this: NovaWindowsDriver, nameOrHandle: string):
throw new errors.NoSuchWindowError(`No window was found with name or handle '${nameOrHandle}'.`);
}

export async function closeApp(this: NovaWindowsDriver): Promise<void> {
const result = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand());
const elementId = result.split('\n').map((id) => id.trim()).filter(Boolean)[0];
if (!elementId) {
throw new errors.NoSuchWindowError('No active app window is found for this session.');
}
await this.sendPowerShellCommand(new FoundAutomationElement(elementId).buildCloseCommand());
await this.sendPowerShellCommand(/* ps1 */ `$rootElement = $null`);
}

export async function launchApp(this: NovaWindowsDriver): Promise<void> {
if (!this.caps.app || ['root', 'none'].includes(this.caps.app.toLowerCase())) {
throw new errors.InvalidArgumentError('No app capability is set for this session.');
}
await this.changeRootElement(this.caps.app);
}

export async function changeRootElement(this: NovaWindowsDriver, path: string): Promise<void>
export async function changeRootElement(this: NovaWindowsDriver, nativeWindowHandle: number): Promise<void>
export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWindowHandle: string | number): Promise<void> {
Expand All @@ -152,7 +169,7 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
if (path.includes('!') && path.includes('_') && !(path.includes('/') || path.includes('\\'))) {
this.log.debug('Detected app path to be in the UWP format.');
await this.sendPowerShellCommand(/* ps1 */ `Start-Process 'explorer.exe' 'shell:AppsFolder\\${path}'${this.caps.appArguments ? ` -ArgumentList '${this.caps.appArguments}'` : ''}`);
await sleep(500); // TODO: make a setting for the initial wait time
await sleep((this.caps['ms:waitForAppLaunch'] ?? 0) * 1000 || 500);
for (let i = 1; i <= 20; i++) {
const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name 'ApplicationFrameHost').Id`);
const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number);
Expand All @@ -172,7 +189,7 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
this.log.debug('Detected app path to be in the classic format.');
const normalizedPath = normalize(path);
await this.sendPowerShellCommand(/* ps1 */ `Start-Process '${normalizedPath}'${this.caps.appArguments ? ` -ArgumentList '${this.caps.appArguments}'` : ''}`);
await sleep(500); // TODO: make a setting for the initial wait time
await sleep((this.caps['ms:waitForAppLaunch'] ?? 0) * 1000 || 500);
for (let i = 1; i <= 20; i++) {
try {
const breadcrumbs = normalizedPath.toLowerCase().split('\\').flatMap((x) => x.split('/'));
Expand Down
6 changes: 3 additions & 3 deletions lib/commands/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { PSString, pwsh$ } from '../powershell';
const GET_SYSTEM_TIME_COMMAND = pwsh$ /* ps1 */ `(Get-Date).ToString(${0})`;
const ISO_8061_FORMAT = 'yyyy-MM-ddTHH:mm:sszzz';

export async function getDeviceTime(this: NovaWindowsDriver, format?: string): Promise<string> {
format = format ? new PSString(format).toString() : `'${ISO_8061_FORMAT}'`;
return await this.sendPowerShellCommand(GET_SYSTEM_TIME_COMMAND.format(format));
export async function getDeviceTime(this: NovaWindowsDriver, _sessionId?: string, format?: string): Promise<string> {
const fmt = format ? new PSString(format).toString() : `'${ISO_8061_FORMAT}'`;
return await this.sendPowerShellCommand(GET_SYSTEM_TIME_COMMAND.format(fmt));
}

// command: 'hideKeyboard'
Expand Down
Loading