Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: appium/appium-windows-driver
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.0.1
Choose a base ref
...
head repository: appium/appium-windows-driver
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 20 commits
  • 12 files changed
  • 4 contributors

Commits on Feb 9, 2025

  1. feat: Make mouse coordinates DPI-aware (#293)

    mykola-mokhnach authored Feb 9, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    dc32a77 View commit details
  2. chore(release): 4.1.0 [skip ci]

    ## [4.1.0](v4.0.1...v4.1.0) (2025-02-09)
    
    ### Features
    
    * Make mouse coordinates DPI-aware ([#293](#293)) ([dc32a77](dc32a77))
    semantic-release-bot committed Feb 9, 2025
    Copy the full SHA
    0feed93 View commit details
  3. chore: Remove obsolete eslint config (#294)

    mykola-mokhnach authored Feb 9, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    325cd5d View commit details
  4. chore(release): 4.1.1 [skip ci]

    ## [4.1.1](v4.1.0...v4.1.1) (2025-02-09)
    
    ### Miscellaneous Chores
    
    * Remove obsolete eslint config ([#294](#294)) ([325cd5d](325cd5d))
    semantic-release-bot committed Feb 9, 2025
    Copy the full SHA
    2e63e81 View commit details

Commits on Feb 21, 2025

  1. fix: Accept reqBasePath proxy option (#295)

    mykola-mokhnach authored Feb 21, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    a390a37 View commit details
  2. chore(release): 4.1.2 [skip ci]

    ## [4.1.2](v4.1.1...v4.1.2) (2025-02-21)
    
    ### Bug Fixes
    
    * Accept reqBasePath proxy option ([#295](#295)) ([a390a37](a390a37))
    semantic-release-bot committed Feb 21, 2025
    Copy the full SHA
    9bd5fb0 View commit details

Commits on Feb 22, 2025

  1. fix: Pass reqBasePath as part of WinAppDriverOptions (#296)

    mykola-mokhnach authored Feb 22, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    0b18c55 View commit details
  2. chore(release): 4.1.3 [skip ci]

    ## [4.1.3](v4.1.2...v4.1.3) (2025-02-22)
    
    ### Bug Fixes
    
    * Pass reqBasePath as part of WinAppDriverOptions ([#296](#296)) ([0b18c55](0b18c55))
    semantic-release-bot committed Feb 22, 2025
    Copy the full SHA
    d37f2ed View commit details

Commits on Feb 24, 2025

  1. fix: Update windowsSetClipboard method mapping (#298)

    mykola-mokhnach authored Feb 24, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    3b944fd View commit details
  2. chore(release): 4.1.4 [skip ci]

    ## [4.1.4](v4.1.3...v4.1.4) (2025-02-24)
    
    ### Bug Fixes
    
    * Update windowsSetClipboard method mapping ([#298](#298)) ([3b944fd](3b944fd))
    semantic-release-bot committed Feb 24, 2025
    Copy the full SHA
    c110662 View commit details

Commits on Feb 26, 2025

  1. fix: Session deletion proxy

    mykola-mokhnach authored Feb 26, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d78808f View commit details
  2. chore(release): 4.1.5 [skip ci]

    ## [4.1.5](v4.1.4...v4.1.5) (2025-02-26)
    
    ### Bug Fixes
    
    * Session deletion proxy ([d78808f](d78808f))
    semantic-release-bot committed Feb 26, 2025
    Copy the full SHA
    237093c View commit details

Commits on Mar 2, 2025

  1. feat: Support context api (#299)

    kkb912002 authored Mar 2, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    ca08f0e View commit details
  2. chore(release): 4.2.0 [skip ci]

    ## [4.2.0](v4.1.5...v4.2.0) (2025-03-02)
    
    ### Features
    
    * Support context api ([#299](#299)) ([ca08f0e](ca08f0e))
    semantic-release-bot committed Mar 2, 2025
    Copy the full SHA
    9557070 View commit details

Commits on Mar 5, 2025

  1. fix: Clearing the recent recorded video if start new one. (#302)

    * clearing previous recorded video if start new one.
    
    * improve log message
    kkb912002 authored Mar 5, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d8c8b87 View commit details
  2. chore(release): 4.2.1 [skip ci]

    ## [4.2.1](v4.2.0...v4.2.1) (2025-03-05)
    
    ### Bug Fixes
    
    * Clearing the recent recorded video if start new one. ([#302](#302)) ([d8c8b87](d8c8b87))
    semantic-release-bot committed Mar 5, 2025
    Copy the full SHA
    30652af View commit details

Commits on Mar 6, 2025

  1. fix: executeMethodMap of startRecordingScreen (#303)

    kkb912002 authored Mar 6, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    3fc1e96 View commit details
  2. chore(release): 4.2.2 [skip ci]

    ## [4.2.2](v4.2.1...v4.2.2) (2025-03-06)
    
    ### Bug Fixes
    
    * executeMethodMap of startRecordingScreen ([#303](#303)) ([3fc1e96](3fc1e96))
    semantic-release-bot committed Mar 6, 2025
    Copy the full SHA
    fbf1946 View commit details

Commits on Mar 25, 2025

  1. chore(deps-dev): bump sinon from 19.0.5 to 20.0.0 (#304)

    Bumps [sinon](https://github.com/sinonjs/sinon) from 19.0.5 to 20.0.0.
    - [Release notes](https://github.com/sinonjs/sinon/releases)
    - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
    - [Commits](sinonjs/sinon@v19.0.5...v20.0.0)
    
    ---
    updated-dependencies:
    - dependency-name: sinon
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Mar 25, 2025

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b5fb040 View commit details
  2. chore(release): 4.2.3 [skip ci]

    ## [4.2.3](v4.2.2...v4.2.3) (2025-03-25)
    
    ### Miscellaneous Chores
    
    * **deps-dev:** bump sinon from 19.0.5 to 20.0.0 ([#304](#304)) ([b5fb040](b5fb040))
    semantic-release-bot committed Mar 25, 2025
    Copy the full SHA
    6cd17ae View commit details
Showing with 361 additions and 102 deletions.
  1. +0 −22 .eslintrc.json
  2. +60 −0 CHANGELOG.md
  3. +19 −0 lib/commands/context.ts
  4. +35 −12 lib/commands/gestures.js
  5. +96 −2 lib/commands/record-screen.js
  6. +62 −53 lib/commands/winapi/user32.js
  7. +11 −1 lib/driver.js
  8. +3 −2 lib/execute-method-map.ts
  9. +21 −8 lib/winappdriver.js
  10. +2 −2 package.json
  11. +39 −0 test/e2e/commands/context-e2e-specs.js
  12. +13 −0 test/unit/driver-specs.js
22 changes: 0 additions & 22 deletions .eslintrc.json

This file was deleted.

60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,63 @@
## [4.2.3](https://github.com/appium/appium-windows-driver/compare/v4.2.2...v4.2.3) (2025-03-25)

### Miscellaneous Chores

* **deps-dev:** bump sinon from 19.0.5 to 20.0.0 ([#304](https://github.com/appium/appium-windows-driver/issues/304)) ([b5fb040](https://github.com/appium/appium-windows-driver/commit/b5fb0400cc4b8fc86364cbc182cc90a09535d35d))

## [4.2.2](https://github.com/appium/appium-windows-driver/compare/v4.2.1...v4.2.2) (2025-03-06)

### Bug Fixes

* executeMethodMap of startRecordingScreen ([#303](https://github.com/appium/appium-windows-driver/issues/303)) ([3fc1e96](https://github.com/appium/appium-windows-driver/commit/3fc1e960c494ce14187736e1933f22aa7519e710))

## [4.2.1](https://github.com/appium/appium-windows-driver/compare/v4.2.0...v4.2.1) (2025-03-05)

### Bug Fixes

* Clearing the recent recorded video if start new one. ([#302](https://github.com/appium/appium-windows-driver/issues/302)) ([d8c8b87](https://github.com/appium/appium-windows-driver/commit/d8c8b87c4aec62546ba2fb59c9f522b21a78a6eb))

## [4.2.0](https://github.com/appium/appium-windows-driver/compare/v4.1.5...v4.2.0) (2025-03-02)

### Features

* Support context api ([#299](https://github.com/appium/appium-windows-driver/issues/299)) ([ca08f0e](https://github.com/appium/appium-windows-driver/commit/ca08f0e91138cccae85c03036e65ea9627a5c150))

## [4.1.5](https://github.com/appium/appium-windows-driver/compare/v4.1.4...v4.1.5) (2025-02-26)

### Bug Fixes

* Session deletion proxy ([d78808f](https://github.com/appium/appium-windows-driver/commit/d78808f619d7a4a4c09342f03052598504399130))

## [4.1.4](https://github.com/appium/appium-windows-driver/compare/v4.1.3...v4.1.4) (2025-02-24)

### Bug Fixes

* Update windowsSetClipboard method mapping ([#298](https://github.com/appium/appium-windows-driver/issues/298)) ([3b944fd](https://github.com/appium/appium-windows-driver/commit/3b944fd56fe24ae6c7f398db06a4d6bf1c75f144))

## [4.1.3](https://github.com/appium/appium-windows-driver/compare/v4.1.2...v4.1.3) (2025-02-22)

### Bug Fixes

* Pass reqBasePath as part of WinAppDriverOptions ([#296](https://github.com/appium/appium-windows-driver/issues/296)) ([0b18c55](https://github.com/appium/appium-windows-driver/commit/0b18c55f2ed756f3cf3872f47c38289f56674bc2))

## [4.1.2](https://github.com/appium/appium-windows-driver/compare/v4.1.1...v4.1.2) (2025-02-21)

### Bug Fixes

* Accept reqBasePath proxy option ([#295](https://github.com/appium/appium-windows-driver/issues/295)) ([a390a37](https://github.com/appium/appium-windows-driver/commit/a390a3740cde7f7c70141f3b30dd562821e9fa06))

## [4.1.1](https://github.com/appium/appium-windows-driver/compare/v4.1.0...v4.1.1) (2025-02-09)

### Miscellaneous Chores

* Remove obsolete eslint config ([#294](https://github.com/appium/appium-windows-driver/issues/294)) ([325cd5d](https://github.com/appium/appium-windows-driver/commit/325cd5d57a1431a9db987234da4da96e7cccf416))

## [4.1.0](https://github.com/appium/appium-windows-driver/compare/v4.0.1...v4.1.0) (2025-02-09)

### Features

* Make mouse coordinates DPI-aware ([#293](https://github.com/appium/appium-windows-driver/issues/293)) ([dc32a77](https://github.com/appium/appium-windows-driver/commit/dc32a77fa34fc9b115732bb9e30dde5adca48cbb))

## [4.0.1](https://github.com/appium/appium-windows-driver/compare/v4.0.0...v4.0.1) (2025-02-04)

### Miscellaneous Chores
19 changes: 19 additions & 0 deletions lib/commands/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { errors } from 'appium/driver';

const WINDOWS_CONTEXT = 'NATIVE_APP';

export async function getContexts (): Promise<string[]> {
return [WINDOWS_CONTEXT];
}

export async function getCurrentContext (): Promise<string> {
return WINDOWS_CONTEXT;
}

export async function setContext (context: string): Promise<void> {
if (context !== WINDOWS_CONTEXT) {
throw new errors.NoSuchContextError(
`The Windows Driver only supports '${WINDOWS_CONTEXT}' context.`
);
}
}
47 changes: 35 additions & 12 deletions lib/commands/gestures.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import {
toMouseMoveInput,
toMouseWheelInput,
getVirtualScreenSize,
ensureDpiAwareness as _ensureDpiAwareness,
} from './winapi/user32';
import { errors } from 'appium/driver';
import B from 'bluebird';
@@ -288,6 +289,8 @@ export async function windowsClick (
times = 1,
interClickDelayMs = 100,
) {
await ensureDpiAwareness.bind(this)();

const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys);
const [absoluteX, absoluteY] = await toAbsoluteCoordinates.bind(this)(elementId, x, y);
let clickDownInput;
@@ -299,7 +302,7 @@ export async function windowsClick (
toMouseButtonInput({button, action: MOUSE_BUTTON_ACTION.DOWN}),
toMouseButtonInput({button, action: MOUSE_BUTTON_ACTION.UP}),
toMouseButtonInput({button, action: MOUSE_BUTTON_ACTION.CLICK}),
toMouseMoveInput({x: absoluteX, y: absoluteY}),
toMouseMoveInput(absoluteX, absoluteY),
]);
} catch (e) {
throw preprocessError(e);
@@ -362,16 +365,18 @@ export async function windowsScroll (
deltaY,
modifierKeys,
) {
await ensureDpiAwareness.bind(this)();

const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys);
const [absoluteX, absoluteY] = await toAbsoluteCoordinates.bind(this)(elementId, x, y);
let moveInput;
let scrollInput;
try {
moveInput = await toMouseMoveInput({x: absoluteX, y: absoluteY});
scrollInput = toMouseWheelInput({
dx: /** @type {number} */ (deltaX),
dy: /** @type {number} */ (deltaY),
});
moveInput = await toMouseMoveInput(absoluteX, absoluteY);
scrollInput = toMouseWheelInput(
/** @type {number} */ (deltaX),
/** @type {number} */ (deltaY),
);
} catch (e) {
throw preprocessError(e);
}
@@ -431,6 +436,8 @@ export async function windowsClickAndDrag (
modifierKeys,
durationMs = 5000,
) {
await ensureDpiAwareness.bind(this)();

const screenSize = await getVirtualScreenSize();
const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys);
const [[startAbsoluteX, startAbsoluteY], [endAbsoluteX, endAbsoluteY]] = await B.all([
@@ -443,9 +450,9 @@ export async function windowsClickAndDrag (
let moveEndInput;
try {
[moveStartInput, clickDownInput, moveEndInput, clickUpInput] = await B.all([
toMouseMoveInput({x: startAbsoluteX, y: startAbsoluteY}, screenSize),
toMouseMoveInput(startAbsoluteX, startAbsoluteY, screenSize),
toMouseButtonInput({button: MOUSE_BUTTON.LEFT, action: MOUSE_BUTTON_ACTION.DOWN}),
toMouseMoveInput({x: endAbsoluteX, y: endAbsoluteY}, screenSize),
toMouseMoveInput(endAbsoluteX, endAbsoluteY, screenSize),
toMouseButtonInput({button: MOUSE_BUTTON.LEFT, action: MOUSE_BUTTON_ACTION.UP}),
]);
} catch (e) {
@@ -509,6 +516,8 @@ export async function windowsHover (
modifierKeys,
durationMs = 500,
) {
await ensureDpiAwareness.bind(this)();

const screenSize = await getVirtualScreenSize();
const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys);
const [[startAbsoluteX, startAbsoluteY], [endAbsoluteX, endAbsoluteY]] = await B.all([
@@ -520,10 +529,11 @@ export async function windowsHover (
const inputPromisesChunk = [];
const maxChunkSize = 10;
for (let step = 0; step <= stepsCount; ++step) {
const promise = B.resolve(toMouseMoveInput({
x: startAbsoluteX + Math.trunc((endAbsoluteX - startAbsoluteX) * step / stepsCount),
y: startAbsoluteY + Math.trunc((endAbsoluteY - startAbsoluteY) * step / stepsCount),
}, screenSize));
const promise = B.resolve(toMouseMoveInput(
startAbsoluteX + Math.trunc((endAbsoluteX - startAbsoluteX) * step / stepsCount),
startAbsoluteY + Math.trunc((endAbsoluteY - startAbsoluteY) * step / stepsCount),
screenSize
));
inputPromises.push(promise);
// This is needed to avoid 'Error: Too many asynchronous calls are running'
inputPromisesChunk.push(promise);
@@ -590,6 +600,19 @@ export async function windowsKeys (actions) {
}
}

/**
* @this {WindowsDriver}
* @returns {Promise<void>}
*/
async function ensureDpiAwareness() {
if (!await _ensureDpiAwareness()) {
this.log.info(
`The call to SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) API has failed. ` +
`Mouse cursor coordinates calculation for scaled displays might not work as expected.`
);
}
}

/**
* @typedef {import('../driver').WindowsDriver} WindowsDriver
*/
98 changes: 96 additions & 2 deletions lib/commands/record-screen.js
Original file line number Diff line number Diff line change
@@ -234,7 +234,7 @@ export class ScreenRecorder {
* (`false`) or to start a new recording immediately and terminate the existing one if running (`true`).
* @throws {Error} If screen recording has failed to start or is not supported on the device under test.
*/
export async function startRecordingScreen (
export async function windowsStartRecordingScreen (
timeLimit,
videoFilter,
fps,
@@ -252,6 +252,9 @@ export async function startRecordingScreen (
}
this.log.debug('Forcing the active screen recording to stop');
await this._screenRecorder.stop(true);
} else if (this._screenRecorder) {
this.log.debug('Clearing the recent screen recording');
await this._screenRecorder.stop(true);
}
this._screenRecorder = null;

@@ -301,7 +304,7 @@ export async function startRecordingScreen (
* or the file content cannot be uploaded to the remote location
* or screen recording is not supported on the device under test.
*/
export async function stopRecordingScreen (
export async function windowsStopRecordingScreen (
remotePath,
user,
pass,
@@ -335,6 +338,97 @@ export async function stopRecordingScreen (
});
}

/**
* Record the display in background while the automated test is running.
* This method requires FFMPEG (https://www.ffmpeg.org/download.html) to be installed
* and present in PATH.
* The resulting video uses H264 codec and is ready to be played by media players built-in into web browsers.
*
* @param {StartRecordingOptions} [options] - The available options.
* @this {import('../driver').WindowsDriver}
* @throws {Error} If screen recording has failed to start or is not supported on the device under test.
*/
export async function startRecordingScreen(options = {}) {
const {
timeLimit,
videoFilter,
fps,
preset,
captureCursor,
captureClicks,
audioInput,
forceRestart = true,
} = options;

await this.windowsStartRecordingScreen(
timeLimit,
videoFilter,
fps,
preset,
captureCursor,
captureClicks,
audioInput,
forceRestart
);
}

/**
* Stop recording the screen.
* If no screen recording has been started before then the method returns an empty string.
*
* @param {StopRecordingOptions} [options] - The available options.
* @returns {Promise<string>} Base64-encoded content of the recorded media file if 'remotePath'
* parameter is falsy or an empty string.
* @this {import('../driver').WindowsDriver}
* @throws {Error} If there was an error while getting the name of a media file
* or the file content cannot be uploaded to the remote location
* or screen recording is not supported on the device under test.
*/
export async function stopRecordingScreen(options = {}) {
const {remotePath, user, pass, method, headers, fileFieldName, formFields} = options;

return await this.windowsStopRecordingScreen(
remotePath,
user,
pass,
method,
headers,
fileFieldName,
formFields
);
}

/**
* @typedef {import('../driver').WindowsDriver} WindowsDriver
*/

/**
* For detailed explanations of each property,
* please refer to the parameters of the {@linkcode windowsStartRecordingScreen} function.
*
* @typedef {Object} StartRecordingOptions
*
* @property {string} [videoFilter]
* @property {number|string} [fps=15]
* @property {string} [preset='veryfast']
* @property {boolean} [captureCursor=false]
* @property {boolean} [captureClicks=false]
* @property {string} [audioInput]
* @property {string|number} [timeLimit=600]
* @property {boolean} [forceRestart=true]
*/

/**
* For detailed explanations of each property,
* please refer to the parameters of the {@linkcode windowsStopRecordingScreen} function.
*
* @typedef {Object} StopRecordingOptions
*
* @property {string} [remotePath]
* @property {string} [user]
* @property {string} [pass]
* @property {string} [method]
* @property {Object} [headers]
* @property {string} [fileFieldName='file']
* @property {Object[]|[string, string][]} [formFields]
*/
115 changes: 62 additions & 53 deletions lib/commands/winapi/user32.js
Original file line number Diff line number Diff line change
@@ -41,6 +41,9 @@ const getUser32 = _.memoize(function getUser32() {
GetSystemMetrics: nodeUtil.promisify(
user32.func('int __stdcall GetSystemMetrics(int nIndex)').async
),
SetProcessDpiAwarenessContext: nodeUtil.promisify(
user32.func('int __stdcall SetProcessDpiAwarenessContext(int value)').async
)
};
});

@@ -158,11 +161,16 @@ const MOUSEEVENTF_VIRTUALDESK = 0x4000;
const MOUSEEVENTF_ABSOLUTE = 0x8000;
const XBUTTON1 = 0x0001;
const XBUTTON2 = 0x0002;
const SM_XVIRTUALSCREEN = 76;
const SM_YVIRTUALSCREEN = 77;
const SM_CXVIRTUALSCREEN = 78;
const SM_CYVIRTUALSCREEN = 79;
const MOUSE_MOVE_NORM = 0xFFFF;
const WHEEL_DELTA = 120;

// const DPI_AWARENESS_CONTEXT_UNAWARE = 16;
// const DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17;
// const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18;
const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34;

export function createKeyInput(params = {}) {
return {
@@ -205,6 +213,18 @@ export async function handleInputs(inputs) {
return uSent;
}

/** @type {() => Promise<boolean>} */
export const ensureDpiAwareness = _.memoize(async function ensureDpiAwareness() {
return Boolean(
await getUser32().SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
);
});

/**
*
* @param {number} nIndex
* @returns {Promise<number>}
*/
async function getSystemMetrics(nIndex) {
return await getUser32().GetSystemMetrics(nIndex);
}
@@ -306,77 +326,62 @@ export async function toMouseButtonInput({button, action}) {
});
}

/**
*
* @param {number} num
* @param {number} min
* @param {number} max
* @returns {number}
*/
function clamp (num, min, max) {
return Math.min(Math.max(num, min), max);
}

/**
* @typedef {Object} MouseMoveOptions
* @property {number} dx Horizontal delta relative to the current cursor position as an integer.
* Most be provided if dy is present
* @property {number} dy Vertical delta relative to the current cursor position as an integer.
* Most be provided if dx is present
* @property {number} x Horizontal absolute cursor position on the virtual desktop as an integer.
* Most be provided if y is present
* @property {number} y Vertical absolute cursor position on the virtual desktop as an integer.
* Most be provided if x is present
*/

/**
* Transforms given mouse move parameters into an appropriate
* input structure
*
* @param {Partial<MouseMoveOptions>} opts
* @param {Size?} screenSize
* @see https://www.reddit.com/r/cpp_questions/comments/1eslzdv/difficulty_with_win32_mouse_position/
* @param {number} x Horizontal absolute cursor position on the virtual desktop as an integer.
* Most be provided if y is present
* @param {number} y Vertical absolute cursor position on the virtual desktop as an integer.
* Most be provided if x is present
* @param {import('@appium/types').Size | null} [screenSize=null]
* @returns {Promise<INPUT>} The resulting input structure
* @throws {Error} If the input data is invalid
*/
export async function toMouseMoveInput({dx, dy, x, y}, screenSize = null) {
const isAbsolute = _.isInteger(x) && _.isInteger(y);
const isRelative = _.isInteger(dx) && _.isInteger(dy);
if (!isAbsolute && !isRelative) {
throw createInvalidArgumentError('Either relative or absolute move coordinates must be provided');
export async function toMouseMoveInput(x, y, screenSize = null) {
if (!_.isInteger(x) || !_.isInteger(y)) {
throw createInvalidArgumentError('Both move coordinates must be provided');
}

if (isAbsolute) {
const {width, height} = screenSize ?? await getVirtualScreenSize();
if (width <= 1 || height <= 1) {
throw new Error('Cannot retrieve virtual screen dimensions via GetSystemMetrics WinAPI');
}
x = clamp(x, 0, width);
y = clamp(y, 0, height);
return createMouseInput({
dx: (x * MOUSE_MOVE_NORM) / (width - 1),
dy: (y * MOUSE_MOVE_NORM) / (height - 1),
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
});
const {width, height} = screenSize ?? await getVirtualScreenSize();
if (width <= 1 || height <= 1) {
throw new Error('Cannot retrieve virtual screen dimensions via GetSystemMetrics WinAPI');
}
// Relative coordinates

const {x: startX, y: startY} = await getVirtualScreenPosition();
const clampedX = clamp(/** @type {number} */ (x) - startX, 0, width);
const clampedY = clamp(/** @type {number} */ (y) - startY, 0, height);
return createMouseInput({
dx, dy,
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK,
dx: (clampedX * MOUSE_MOVE_NORM) / (width - 1),
dy: (clampedY * MOUSE_MOVE_NORM) / (height - 1),
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
});
}

/**
* @typedef {Object} MouseWheelOptions
* @property {number} dx Horizontal scroll delta as an integer.
* If provided then no vertical scroll delta must be set.
* @property {number} dy Vertical scroll delta as an integer.
* If provided then no horizontal scroll delta must be set.
*/

/**
* Transforms given mouse wheel parameters into an appropriate
* input structure
*
* @param {MouseWheelOptions} opts
* @param {number} dx Horizontal scroll delta as an integer.
* If provided then no vertical scroll delta must be set.
* @param {number} dy Vertical scroll delta as an integer.
* If provided then no horizontal scroll delta must be set.
* @returns {INPUT | null} The resulting input structure or null
* if no input has been generated.
* @throws {Error} If the input data is invalid
*/
export function toMouseWheelInput({dx, dy}) {
export function toMouseWheelInput(dx, dy) {
const hasHorizontalScroll = _.isInteger(dx);
const hasVerticalScroll = _.isInteger(dy);
if (!hasHorizontalScroll && !hasVerticalScroll) {
@@ -432,18 +437,22 @@ export function toUnicodeKeyInputs(text) {
return result;
}

/**
* @typedef {Object} Size
* @property {number} width
* @property {number} height
*/

/**
* Fetches the size of the virtual screen
*
* @returns {Promise<Size>}
* @returns {Promise<import('@appium/types').Size>}
*/
export async function getVirtualScreenSize () {
const [width, height] = await B.all([SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN].map(getSystemMetrics));
return {width, height};
}

/**
* Fetches the location of the virtual screen
*
* @returns {Promise<import('@appium/types').Position>}
*/
export async function getVirtualScreenPosition () {
const [x, y] = await B.all([SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN].map(getSystemMetrics));
return {x, y};
}
12 changes: 11 additions & 1 deletion lib/driver.js
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import * as gestureCommands from './commands/gestures';
import * as powershellCommands from './commands/powershell';
import * as recordScreenCommands from './commands/record-screen';
import * as touchCommands from './commands/touch';
import * as contextCommands from './commands/context';
import { POWER_SHELL_FEATURE } from './constants';
import { newMethodMap } from './method-map';
import { executeMethodMap } from './execute-method-map';
@@ -29,6 +30,8 @@ const NO_PROXY = [
['POST', new RegExp('^/session/[^/]+/appium/device/pull_file')],
['POST', new RegExp('^/session/[^/]+/appium/device/pull_folder')],
['GET', new RegExp('^/session/[^/]+/screenshot')],
['GET', new RegExp('^/session/[^/]+/contexts?')],
['POST', new RegExp('^/session/[^/]+/context')],
// Workarounds for
// - https://github.com/appium/appium/issues/15923
// - https://github.com/appium/appium/issues/16316
@@ -111,6 +114,7 @@ export class WindowsDriver extends BaseDriver {
async startWinAppDriverSession () {
this.winAppDriver = new WinAppDriver(this.log, {
port: this.opts.systemPort,
reqBasePath: this.basePath,
});
await this.winAppDriver.start(this.caps);
this.proxyReqRes = this.winAppDriver.proxy?.proxyReqRes.bind(this.winAppDriver.proxy);
@@ -172,7 +176,7 @@ export class WindowsDriver extends BaseDriver {
windowsLaunchApp = appManagementCommands.windowsLaunchApp;
windowsCloseApp = appManagementCommands.windowsCloseApp;

windowsSetClipboard = clipboardCommands.windowsGetClipboard;
windowsSetClipboard = clipboardCommands.windowsSetClipboard;
windowsGetClipboard = clipboardCommands.windowsGetClipboard;

execute = executeCommands.execute;
@@ -200,10 +204,16 @@ export class WindowsDriver extends BaseDriver {

execPowerShell = powershellCommands.execPowerShell;

windowsStartRecordingScreen = recordScreenCommands.windowsStartRecordingScreen;
windowsStopRecordingScreen = recordScreenCommands.windowsStopRecordingScreen;
startRecordingScreen = recordScreenCommands.startRecordingScreen;
stopRecordingScreen = recordScreenCommands.stopRecordingScreen;

performActions = touchCommands.performActions;

getContexts = contextCommands.getContexts;
getCurrentContext = contextCommands.getCurrentContext;
setContext = contextCommands.setContext;
}

export default WindowsDriver;
5 changes: 3 additions & 2 deletions lib/execute-method-map.ts
Original file line number Diff line number Diff line change
@@ -2,10 +2,11 @@ import { ExecuteMethodMap } from '@appium/types';

export const executeMethodMap = {
'windows: startRecordingScreen': {
command: 'startRecordingScreen',
command: 'windowsStartRecordingScreen',
params: {
optional: [
'timeLimit',
'videoFilter',
'fps',
'preset',
'captureCursor',
@@ -16,7 +17,7 @@ export const executeMethodMap = {
},
},
'windows: stopRecordingScreen': {
command: 'stopRecordingScreen',
command: 'windowsStopRecordingScreen',
params: {
optional: [
'remotePath',
29 changes: 21 additions & 8 deletions lib/winappdriver.js
Original file line number Diff line number Diff line change
@@ -123,39 +123,47 @@ process.once('exit', () => {
} catch {}
});

class WinAppDriver {
export class WinAppDriver {
/**
*
* @param {import('@appium/types').AppiumLogger} log
* @param {{port: number}} opts
* @param {WinAppDriverOptions} opts
*/
constructor (log, opts) {
this.log = log;
this.proxyPort = opts.port;
this.opts = opts;

this.process = null;
this.proxy = null;
}

/**
*
* @param {import('@appium/types').StringRecord} caps
*/
async start (caps) {
const executablePath = await getWADExecutablePath();
const isForceQuitEnabled = caps['ms:forcequit'] === true;

this.process = new WADProcess(this.log, {
// XXXYD TODO: would be better if WinAppDriver didn't require passing in /wd/hub as a param
base: DEFAULT_BASE_PATH,
port: this.proxyPort,
port: this.opts.port,
executablePath,
isForceQuitEnabled
});
await this.process.start();

this.proxy = new WADProxy({
const proxyOpts = {
log: this.log,
base: this.process.base,
server: DEFAULT_HOST,
port: this.process.port,
});
};
if (this.opts.reqBasePath) {
proxyOpts.reqBasePath = this.opts.reqBasePath;
}
this.proxy = new WADProxy(proxyOpts);
this.proxy.didProcessExit = false;
this.process.proc?.on('exit', () => {
if (this.proxy) {
@@ -237,7 +245,7 @@ class WinAppDriver {
if (this.proxy?.sessionId) {
this.log.debug('Deleting WinAppDriver server session');
try {
await this.proxy.command(`/session/${this.proxy?.sessionId}`, 'DELETE');
await this.proxy.command('', 'DELETE');
} catch (err) {
this.log.warn(`Did not get confirmation WinAppDriver deleteSession worked; ` +
`Error was: ${err.message}`);
@@ -252,5 +260,10 @@ class WinAppDriver {
}
}

export { WinAppDriver };
export default WinAppDriver;

/**
* @typedef {Object} WinAppDriverOptions
* @property {number} port
* @property {string} [reqBasePath]
*/
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "appium-windows-driver",
"version": "4.0.1",
"version": "4.2.3",
"description": "Appium bridge to WinAppDriver",
"keywords": [
"appium",
@@ -93,7 +93,7 @@
"mocha": "^11.0.1",
"rimraf": "^5.0.0",
"semantic-release": "^24.0.0",
"sinon": "^19.0.1",
"sinon": "^20.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.4.2",
"webdriverio": "^9.0.1"
39 changes: 39 additions & 0 deletions test/e2e/commands/context-e2e-specs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { buildWdIoOptions } from '../helpers';
import { remote as wdio } from 'webdriverio';

describe('context', function () {
let chai;
/** @type {import('webdriverio').Browser} */
let driver;

before(async function () {
chai = await import('chai');
const chaiAsPromised = await import('chai-as-promised');

chai.should();
chai.use(chaiAsPromised.default);

driver = await wdio(buildWdIoOptions('Root'));
});

after(async function () {
try {
if (driver) {
await driver.deleteSession();
}
} finally {
driver = null;
}
});

it('should support context api', async function () {
(await driver.getAppiumContext()).should.equal('NATIVE_APP');
(await driver.getAppiumContexts()).should.eql(['NATIVE_APP']);
await driver.switchAppiumContext('NATIVE_APP');
});

it('should throw an error if invalid context', async function () {
await driver.switchAppiumContext('INVALID_CONTEXT').should.rejected;
});

});
13 changes: 13 additions & 0 deletions test/unit/driver-specs.js
Original file line number Diff line number Diff line change
@@ -42,6 +42,19 @@ describe('driver.js', function () {
driver.caps.cap.should.equal('foo');
});

describe('context simulation', function () {
it('should support context commands', async function () {
let driver = new WindowsDriver({ app: 'myapp'}, false);
(await driver.getCurrentContext()).should.equal('NATIVE_APP');
(await driver.getContexts()).should.eql(['NATIVE_APP']);
await driver.setContext('NATIVE_APP');
});
it('should throw an error if invalid context', async function () {
let driver = new WindowsDriver({ app: 'myapp'}, false);
await driver.setContext('INVALID_CONTEXT').should.rejected;
});
});

// TODO: Implement or delete
//it('should set the default context', async function () {
// let driver = new SelendroidDriver({}, false);