From 893bd888cf4e7b43e07c83e8ce71d71a30a8428e Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Thu, 16 Feb 2023 14:48:17 -0800 Subject: [PATCH] feat(driver-test-support): add startStoppableAppium() This adds a function `startStoppableAppium()` which starts an Appium server (via its `main()` entry point), but resolves with an object having a `stop() => Promise` method. This method force-quits the server, even if there are active connections. --- package-lock.json | 19 +++++++ packages/driver-test-support/lib/index.js | 3 + packages/driver-test-support/lib/stoppable.ts | 57 +++++++++++++++++++ packages/driver-test-support/package.json | 2 + .../test/e2e/stoppable.e2e.spec.ts | 37 ++++++++++++ packages/driver-test-support/tsconfig.json | 2 +- 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 packages/driver-test-support/lib/stoppable.ts create mode 100644 packages/driver-test-support/test/e2e/stoppable.e2e.spec.ts diff --git a/package-lock.json b/package-lock.json index 7b8fdedda..bae12d0eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2405,6 +2405,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stoppable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", + "integrity": "sha512-b8N+fCADRIYYrGZOcmOR8ZNBOqhktWTB/bMUl5LvGtT201QKJZOOH5UsFyI3qtteM6ZAJbJqZoBcLqqxKIwjhw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/supports-color": { "version": "8.1.1", "license": "MIT" @@ -14379,6 +14387,15 @@ "node": ">= 0.8" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/stream-browserify": { "version": "2.0.2", "dev": true, @@ -16582,6 +16599,7 @@ "dependencies": { "@appium/types": "^0.9.1", "@types/lodash": "4.14.191", + "@types/stoppable": "1.1.1", "axios": "1.3.3", "bluebird": "3.7.2", "chai": "4.3.7", @@ -16589,6 +16607,7 @@ "lodash": "4.17.21", "sinon": "15.0.1", "source-map-support": "0.5.21", + "stoppable": "1.1.0", "type-fest": "3.5.7" }, "engines": { diff --git a/packages/driver-test-support/lib/index.js b/packages/driver-test-support/lib/index.js index 50f942bac..6c2120e98 100644 --- a/packages/driver-test-support/lib/index.js +++ b/packages/driver-test-support/lib/index.js @@ -2,6 +2,9 @@ export {createSessionHelpers, driverE2ETestSuite} from './e2e-suite'; export * from './unit-suite'; export * from './helpers'; +// eslint-disable-next-line import/no-unresolved +export * from './stoppable'; + /** * @typedef {import('@appium/types').DriverClass} DriverClass * @typedef {import('@appium/types').BaseNSCapabilities} BaseNSCapabilities diff --git a/packages/driver-test-support/lib/stoppable.ts b/packages/driver-test-support/lib/stoppable.ts new file mode 100644 index 000000000..9af37503f --- /dev/null +++ b/packages/driver-test-support/lib/stoppable.ts @@ -0,0 +1,57 @@ +import type {AppiumServer} from '@appium/types'; +import {main as startAppium} from 'appium'; +import type {Args, CliCommandServer} from 'appium/types'; +import B from 'bluebird'; +import type {Server} from 'node:http'; +import stoppable from 'stoppable'; +import type {Asyncify} from 'type-fest'; + +/** + * Options for {@linkcode startStoppableAppium} + */ +export type AppiumServerOpts = Args; + +/** + * An {@linkcode AppiumServer} with a method `stop() => Promise`, which closes all sockets and fully stops the server. + * + * Returned by {@linkcode startStoppableAppium} + */ +export type TestAppiumServer = Omit & { + stop: Asyncify; + close: (callback: (err?: Error) => void) => Promise; +}; + +/** + * The {@linkcode AppiumServer} type, but with the `close` method normalized to a callback-style function. + */ +export type NormativeAppiumServer = Omit & { + close: Server['close']; +}; + +/** + * Coerces {@linkcode AppiumServer} into a {@linkcode TestAppiumServer}. + * @param opts Options for {@linkcode startAppium} + * @todo This should be moved into `@appium/driver-test-support` or something + * @returns A stoppable Appium server + */ +export async function startStoppableAppium(opts: AppiumServerOpts): Promise { + const appiumServer = (await startAppium(opts)) as AppiumServer; + const stoppableServer = stoppable(appiumServer as unknown as NormativeAppiumServer, 0); + const originalAsyncClose = appiumServer.close; + (stoppableServer as unknown as TestAppiumServer).close = async function ( + callback?: (err?: Error) => void + ) { + if (callback) { + try { + await originalAsyncClose.call(this); + callback(); + } catch (err) { + callback(err); + } + } else { + await originalAsyncClose.call(this); + } + }; + stoppableServer.stop = B.promisify(stoppableServer.stop, {context: stoppableServer}); + return stoppableServer as unknown as TestAppiumServer; +} diff --git a/packages/driver-test-support/package.json b/packages/driver-test-support/package.json index 2390a9254..e5fd165ba 100644 --- a/packages/driver-test-support/package.json +++ b/packages/driver-test-support/package.json @@ -43,6 +43,7 @@ "dependencies": { "@appium/types": "^0.9.1", "@types/lodash": "4.14.191", + "@types/stoppable": "1.1.1", "axios": "1.3.3", "bluebird": "3.7.2", "chai": "4.3.7", @@ -50,6 +51,7 @@ "lodash": "4.17.21", "sinon": "15.0.1", "source-map-support": "0.5.21", + "stoppable": "1.1.0", "type-fest": "3.5.7" }, "peerDependencies": { diff --git a/packages/driver-test-support/test/e2e/stoppable.e2e.spec.ts b/packages/driver-test-support/test/e2e/stoppable.e2e.spec.ts new file mode 100644 index 000000000..a91ee7aef --- /dev/null +++ b/packages/driver-test-support/test/e2e/stoppable.e2e.spec.ts @@ -0,0 +1,37 @@ +import axios from 'axios'; +import B from 'bluebird'; +import {Agent} from 'node:http'; +import {startStoppableAppium, TestAppiumServer} from '../../lib'; +import getPort from 'get-port'; + +const {expect} = chai; + +describe('startStoppableAppium()', function () { + it('should start an Appium server', async function () { + let server: TestAppiumServer | undefined; + try { + server = await startStoppableAppium({port: await getPort()}); + expect(server, 'to be an object'); + } finally { + if (server) { + await expect(server.stop()).to.be.fulfilled; + } + } + }); + + describe('when the server has connections', function () { + it('should stop the server and resolve with a boolean', async function () { + const port = await getPort(); + const server = await startStoppableAppium({port}); + const getConnections = B.promisify(server.getConnections, {context: server}); + await axios.get(`http://127.0.0.1:${port}/status`, { + httpAgent: new Agent({keepAlive: true}), + }); + try { + await expect(getConnections()).to.eventually.be.greaterThan(0); + } finally { + await expect(server.stop()).to.eventually.be.a('boolean'); + } + }); + }); +}); diff --git a/packages/driver-test-support/tsconfig.json b/packages/driver-test-support/tsconfig.json index 52ab4f201..2c75d42bd 100644 --- a/packages/driver-test-support/tsconfig.json +++ b/packages/driver-test-support/tsconfig.json @@ -10,6 +10,6 @@ }, "types": ["mocha", "chai", "chai-as-promised"] }, - "include": ["./lib"], + "include": ["lib", "test"], "references": [{"path": "../types"}, {"path": "../base-driver"}] }