diff --git a/README.md b/README.md index 7507506..be52bf4 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ appium:systemPort | The port number to execute Appium Windows Driver server list appium:prerun | An object containing either `script` or `command` key. The value of each key must be a valid PowerShell script or command to be executed prior to the WinAppDriver session startup. See [Power Shell commands execution](#power-shell-commands-execution) for more details. Example: `{script: 'Get-Process outlook -ErrorAction SilentlyContinue'}` appium: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. appium:newCommandTimeout | How long (in seconds) the driver should wait for a new command from the client before assuming the client has stopped sending requests. After the timeout, the session is going to be deleted. `60` seconds by default. Setting it to zero disables the timer. +appium:wadUrl | Allows to provide a custom URL to the WAD server. The server must be already running when a new session starts. If this URL is provided explicitly then the driver won't try to either autodetect or start WinAppDriver automatically, and it is expected that the server lifecycle is managed externally. ## Driver Scripts diff --git a/lib/commands/general.js b/lib/commands/general.js index 20042ab..b2a0bd1 100644 --- a/lib/commands/general.js +++ b/lib/commands/general.js @@ -77,7 +77,7 @@ export async function getWindowRect () { * @param {number} y * @param {number} width * @param {number} height - * @returns {Promise} + * @returns {Promise} */ export async function setWindowRect (x, y, width, height) { let didProcess = false; @@ -99,6 +99,7 @@ export async function setWindowRect (x, y, width, height) { if (!didProcess) { this.log.info('Either x and y or width and height must be defined. Doing nothing'); } + return {x, y, width, height}; } /** diff --git a/lib/desired-caps.js b/lib/desired-caps.ts similarity index 89% rename from lib/desired-caps.js rename to lib/desired-caps.ts index 401ffae..9bae871 100644 --- a/lib/desired-caps.js +++ b/lib/desired-caps.ts @@ -1,4 +1,4 @@ -const desiredCapConstraints = /** @type {const} */ ({ +export const desiredCapConstraints = { // https://github.com/microsoft/WinAppDriver/blob/master/Docs/AuthoringTestScripts.md#supported-capabilities platformName: { presence: true, @@ -43,8 +43,10 @@ const desiredCapConstraints = /** @type {const} */ ({ }, postrun: { isObject: true - } -}); + }, + wadUrl: { + isString: true + }, +} as const; -export { desiredCapConstraints }; export default desiredCapConstraints; diff --git a/lib/driver.js b/lib/driver.js index ef82bb1..fdfed07 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -43,6 +43,10 @@ const NO_PROXY = [ ]; // Appium instantiates this class +/** + * @implements {ExternalDriver} + * @extends {BaseDriver} + */ export class WindowsDriver extends BaseDriver { /** @type {boolean} */ isProxyActive; @@ -113,6 +117,7 @@ export class WindowsDriver extends BaseDriver { async startWinAppDriverSession () { this.winAppDriver = new WinAppDriver(this.log, { + url: this.opts.wadUrl, port: this.opts.systemPort, reqBasePath: this.basePath, }); @@ -166,11 +171,11 @@ export class WindowsDriver extends BaseDriver { return this.jwpProxyAvoid; } - async proxyCommand (url, method, body = null) { + async proxyCommand (url, method, body) { if (!this.winAppDriver?.proxy) { throw new Error('The proxy must be defined in order to send commands'); } - return await this.winAppDriver.proxy.command(url, method, body); + return /** @type {any} */ (await this.winAppDriver.proxy.command(url, method, body)); } windowsLaunchApp = appManagementCommands.windowsLaunchApp; @@ -221,4 +226,10 @@ export default WindowsDriver; /** * @typedef {typeof desiredCapConstraints} WindowsDriverConstraints * @typedef {import('@appium/types').DriverOpts} WindowsDriverOpts - */ \ No newline at end of file + */ + +/** + * @template {import('@appium/types').Constraints} C + * @template [Ctx=string] + * @typedef {import('@appium/types').ExternalDriver} ExternalDriver + */ diff --git a/lib/winappdriver.js b/lib/winappdriver.js index 9ed2f03..6192f02 100644 --- a/lib/winappdriver.js +++ b/lib/winappdriver.js @@ -43,7 +43,7 @@ class WADProcess { /** * * @param {import('@appium/types').AppiumLogger} log - * @param {{base: string, port: number, executablePath: string, isForceQuitEnabled: boolean}} opts + * @param {WADProcessOptions} opts */ constructor (log, opts) { this.log = log; @@ -138,22 +138,34 @@ export class WinAppDriver { } /** - * - * @param {import('@appium/types').StringRecord} caps + * @param {WindowsDriverCaps} caps */ async start (caps) { - const executablePath = await getWADExecutablePath(); - const isForceQuitEnabled = caps['ms:forcequit'] === true; + if (this.opts.url) { + await this._prepareSessionWithCustomServer(this.opts.url); + } else { + const isForceQuitEnabled = caps['ms:forcequit'] === true; + await this._prepareSessionWithBuiltInServer(isForceQuitEnabled); + } + await this._startSession(caps); + } + + /** + * @param {boolean} isForceQuitEnabled + * @returns {Promise} + */ + async _prepareSessionWithBuiltInServer(isForceQuitEnabled) { + const executablePath = await getWADExecutablePath(); 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.opts.port, executablePath, - isForceQuitEnabled + isForceQuitEnabled, }); await this.process.start(); + /** @type {import('@appium/types').ProxyOptions} */ const proxyOpts = { log: this.log, base: this.process.base, @@ -190,22 +202,65 @@ export class WinAppDriver { }); } catch (e) { if (/Condition unmet/.test(e.message)) { - throw new Error(`WinAppDriver server is not listening within ${STARTUP_TIMEOUT_MS}ms timeout. ` + - `Make sure it could be started manually`); + throw new Error( + `WinAppDriver server is not listening within ${STARTUP_TIMEOUT_MS}ms timeout. ` + + `Make sure it could be started manually` + ); } throw e; } const pid = this.process.proc?.pid; RUNNING_PROCESS_IDS.push(pid); this.process.proc?.on('exit', () => void _.pull(RUNNING_PROCESS_IDS, pid)); + } - await this._startSession(caps); + /** + * + * @param {string} url + * @returns {Promise} + */ + async _prepareSessionWithCustomServer (url) { + this.log.info(`Using custom WinAppDriver server URL: ${url}`); + + /** @type {URL} */ + let parsedUrl; + try { + parsedUrl = new URL(url); + } catch (e) { + throw new Error( + `Cannot parse the provided WinAppDriver URL '${url}'. Original error: ${e.message}` + ); + } + /** @type {import('@appium/types').ProxyOptions} */ + const proxyOpts = { + log: this.log, + base: parsedUrl.pathname, + server: parsedUrl.hostname, + port: parseInt(parsedUrl.port, 10), + scheme: _.trimEnd(parsedUrl.protocol, ':'), + }; + if (this.opts.reqBasePath) { + proxyOpts.reqBasePath = this.opts.reqBasePath; + } + this.proxy = new WADProxy(proxyOpts); + + try { + await this.proxy.command('/status', 'GET'); + } catch { + throw new Error( + `WinAppDriver server is not listening at ${url}. ` + + `Make sure it is running and the provided wadUrl is correct` + ); + } } - async _startSession (desiredCapabilities) { + /** + * @param {WindowsDriverCaps} caps + */ + async _startSession (caps) { const { createSessionTimeout = DEFAULT_CREATE_SESSION_TIMEOUT_MS - } = desiredCapabilities; + } = caps; this.log.debug(`Starting WinAppDriver session. Will timeout in '${createSessionTimeout}' ms.`); let retryIteration = 0; let lastError; @@ -214,7 +269,7 @@ export class WinAppDriver { lastError = null; retryIteration++; try { - await this.proxy?.command('/session', 'POST', {desiredCapabilities}); + await this.proxy?.command('/session', 'POST', {desiredCapabilities: caps}); return true; } catch (error) { lastError = error; @@ -264,6 +319,19 @@ export default WinAppDriver; /** * @typedef {Object} WinAppDriverOptions - * @property {number} port + * @property {number} [port] * @property {string} [reqBasePath] + * @property {string} [url] + */ + +/** + * @typedef {Object} WADProcessOptions + * @property {string} base + * @property {number} [port] + * @property {string} executablePath + * @property {boolean} isForceQuitEnabled + */ + +/** + * @typedef {import('@appium/types').DriverCaps} WindowsDriverCaps */