mirror of
https://github.com/appium/appium.git
synced 2026-02-20 10:20:05 -06:00
feat(driver-test-support): create package
This package contains driver-specific test utilities extracted from `@appium/base-driver` by way of `@appium/test-support`
This commit is contained in:
@@ -83,6 +83,7 @@
|
||||
"@appium/base-plugin": "file:packages/base-plugin",
|
||||
"@appium/doctor": "file:packages/doctor",
|
||||
"@appium/docutils": "file:packages/docutils",
|
||||
"@appium/driver-test-support": "file:packages/driver-test-support",
|
||||
"@appium/eslint-config-appium": "file:packages/eslint-config-appium",
|
||||
"@appium/execute-driver-plugin": "file:packages/execute-driver-plugin",
|
||||
"@appium/fake-driver": "file:packages/fake-driver",
|
||||
|
||||
112
packages/driver-test-support/README.md
Normal file
112
packages/driver-test-support/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# @appium/driver-test-support
|
||||
|
||||
> Testing utilities for [Appium](https://appium.io) drivers
|
||||
|
||||
This package is for driver authors to help test their drivers.
|
||||
|
||||
[Mocha](https://mochajs.org) is the supported test framework.
|
||||
|
||||
## Usage
|
||||
|
||||
### For E2E Tests
|
||||
|
||||
The `driverE2ETestSuite` method creates a Mocha test suite which makes HTTP requests to an in-memory server leveraging your driver.
|
||||
|
||||
Note that this method must be run within a _suite callback_—not a _test callback_.
|
||||
|
||||
```js
|
||||
import {driverE2ETestSuite} from '@appium/driver-test-support';
|
||||
|
||||
const defaultW3CCapabilities = {
|
||||
// some capabilities
|
||||
};
|
||||
|
||||
describe('MyDriverClass', function() {
|
||||
driverE2ETestSuite(MyDriverClass, defaultW3CCapabilities);
|
||||
|
||||
describe('more tests', function() {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### For Unit Tests
|
||||
|
||||
The `driverUnitTestSuite` method creates a Mocha test suite which performs assertions on an isolated instance of your driver.
|
||||
|
||||
Note that this method must be run within a _suite callback_—not a _test callback_.
|
||||
|
||||
```js
|
||||
import {driverUnitTestSuite} from '@appium/driver-test-support';
|
||||
|
||||
const defaultW3CCapabilities = {
|
||||
// some capabilities
|
||||
};
|
||||
|
||||
describe('MyDriverClass', function() {
|
||||
driverUnitTestSuite(MyDriverClass, defaultW3CCapabilities);
|
||||
|
||||
describe('more tests', function() {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Helpers
|
||||
|
||||
These are just some helpers (mainly for E2E tests):
|
||||
|
||||
```js
|
||||
import {TEST_HOST, getTestPort, createAppiumURL} from '@appium/driver-test-support';
|
||||
import assert from 'node:assert';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('TEST_HOST', function() {
|
||||
it('should be localhost', function() {
|
||||
assert.strictEqual(TEST_HOST, '127.0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTestPort()', function() {
|
||||
it('should get a free test port', async function() {
|
||||
const port = await getTestPort();
|
||||
assert.ok(port > 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAppiumURL()', function() {
|
||||
it('should create a "new session" URL', function() {
|
||||
const actual = createAppiumURL(TEST_HOST, 31337, '', 'session');
|
||||
const expected = `http://${TEST_HOST}:31337/session`;
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should create a URL to get an existing session', function() {
|
||||
const sessionId = '12345';
|
||||
const createGetSessionURL = createAppiumURL(TEST_HOST, 31337, _, 'session');
|
||||
const actual = createGetSessionURL(sessionId);
|
||||
const expected = `http://${TEST_HOST}:31337/session/${sessionId}/session`;
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should create a URL for a command using an existing session', function() {
|
||||
const sessionId = '12345';
|
||||
const createURLWithPath = createAppiumURL('127.0.0.1', 31337, sessionId);
|
||||
const actual = createURLWithPath('moocow');
|
||||
const expected = `http://${TEST_HOST}:31337/session/${sessionId}/moocow`;
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
`appium` and `mocha` are peer dependencies.
|
||||
|
||||
```bash
|
||||
npm install appium mocha @appium/driver-test-support --save-dev
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
1
packages/driver-test-support/index.js
Normal file
1
packages/driver-test-support/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./build/lib');
|
||||
465
packages/driver-test-support/lib/e2e-suite.js
Normal file
465
packages/driver-test-support/lib/e2e-suite.js
Normal file
@@ -0,0 +1,465 @@
|
||||
import _ from 'lodash';
|
||||
import {server, routeConfiguringFunction, DeviceSettings} from 'appium/driver';
|
||||
import axios from 'axios';
|
||||
import B from 'bluebird';
|
||||
import {TEST_HOST, getTestPort, createAppiumURL} from './helpers';
|
||||
import chai from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const should = chai.should();
|
||||
|
||||
/**
|
||||
* Creates some helper functions for E2E tests to manage sessions.
|
||||
* @template [CommandData=unknown]
|
||||
* @template [ResponseData=any]
|
||||
* @param {number} port - Port on which the server is running. Typically this will be retrieved via `get-port` beforehand
|
||||
* @param {string} [address] - Address/host on which the server is running. Defaults to {@linkcode TEST_HOST}
|
||||
* @returns {SessionHelpers<CommandData, ResponseData>}
|
||||
*/
|
||||
export function createSessionHelpers(port, address = TEST_HOST) {
|
||||
const createAppiumTestURL =
|
||||
/** @type {import('lodash').CurriedFunction2<string,string,string>} */ (
|
||||
createAppiumURL(address, port)
|
||||
);
|
||||
|
||||
const createSessionURL = createAppiumTestURL(_, '');
|
||||
const newSessionURL = createAppiumTestURL('', 'session');
|
||||
return /** @type {SessionHelpers<CommandData, ResponseData>} */ ({
|
||||
newSessionURL,
|
||||
createAppiumTestURL,
|
||||
/**
|
||||
*
|
||||
* @param {string} sessionId
|
||||
* @param {string} cmdName
|
||||
* @param {any} [data]
|
||||
* @param {AxiosRequestConfig} [config]
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
postCommand: async (sessionId, cmdName, data = {}, config = {}) => {
|
||||
const url = createAppiumTestURL(sessionId, cmdName);
|
||||
const response = await axios.post(url, data, config);
|
||||
return response.data?.value;
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} sessionIdOrCmdName
|
||||
* @param {string|AxiosRequestConfig} cmdNameOrConfig
|
||||
* @param {AxiosRequestConfig} [config]
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getCommand: async (sessionIdOrCmdName, cmdNameOrConfig, config = {}) => {
|
||||
if (!_.isString(cmdNameOrConfig)) {
|
||||
config = cmdNameOrConfig;
|
||||
cmdNameOrConfig = sessionIdOrCmdName;
|
||||
sessionIdOrCmdName = '';
|
||||
}
|
||||
const response = await axios({
|
||||
url: createAppiumTestURL(sessionIdOrCmdName, cmdNameOrConfig),
|
||||
validateStatus: null,
|
||||
...config,
|
||||
});
|
||||
return response.data?.value;
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {NewSessionData} data
|
||||
* @param {AxiosRequestConfig} [config]
|
||||
*/
|
||||
startSession: async (data, config = {}) => {
|
||||
data = _.defaultsDeep(data, {
|
||||
capabilities: {
|
||||
alwaysMatch: {},
|
||||
firstMatch: [{}],
|
||||
},
|
||||
});
|
||||
const response = await axios.post(newSessionURL, data, config);
|
||||
return response.data?.value;
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
endSession: async (sessionId) =>
|
||||
await axios.delete(createSessionURL(sessionId), {
|
||||
validateStatus: null,
|
||||
}),
|
||||
/**
|
||||
* @param {string} sessionId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getSession: async (sessionId) => {
|
||||
const response = await axios({
|
||||
url: createSessionURL(sessionId),
|
||||
validateStatus: null,
|
||||
});
|
||||
return response.data?.value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates E2E test suites for a driver.
|
||||
* @template {Driver} P
|
||||
* @param {DriverClass<P>} DriverClass
|
||||
* @param {AppiumW3CCapabilities} [defaultCaps]
|
||||
*/
|
||||
export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
let address = defaultCaps['appium:address'] ?? TEST_HOST;
|
||||
let port = defaultCaps['appium:port'];
|
||||
const className = DriverClass.name || '(unknown driver)';
|
||||
|
||||
describe(`BaseDriver E2E (as ${className})`, function () {
|
||||
let baseServer;
|
||||
/** @type {P} */
|
||||
let d;
|
||||
/**
|
||||
* This URL creates a new session
|
||||
* @type {string}
|
||||
**/
|
||||
let newSessionURL;
|
||||
|
||||
/** @type {SessionHelpers['startSession']} */
|
||||
let startSession;
|
||||
/** @type {SessionHelpers['getSession']} */
|
||||
let getSession;
|
||||
/** @type {SessionHelpers['endSession']} */
|
||||
let endSession;
|
||||
/** @type {SessionHelpers['getCommand']} */
|
||||
let getCommand;
|
||||
/** @type {SessionHelpers['postCommand']} */
|
||||
let postCommand;
|
||||
before(async function () {
|
||||
port = port ?? (await getTestPort());
|
||||
defaultCaps = {...defaultCaps, 'appium:port': port};
|
||||
d = new DriverClass({port, address});
|
||||
baseServer = await server({
|
||||
routeConfiguringFunction: routeConfiguringFunction(d),
|
||||
port,
|
||||
hostname: address,
|
||||
// @ts-expect-error
|
||||
cliArgs: {},
|
||||
});
|
||||
({startSession, getSession, endSession, newSessionURL, getCommand, postCommand} =
|
||||
createSessionHelpers(port, address));
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await baseServer.close();
|
||||
});
|
||||
|
||||
describe('session handling', function () {
|
||||
it('should handle idempotency while creating sessions', async function () {
|
||||
const sessionIds = [];
|
||||
let times = 0;
|
||||
do {
|
||||
const {sessionId} = await startSession(
|
||||
{
|
||||
capabilities: {alwaysMatch: defaultCaps},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'X-Idempotency-Key': '123456',
|
||||
},
|
||||
// XXX: I'm not sure what these are, as they are not documented axios options,
|
||||
// nor are they mentioned in our source
|
||||
// @ts-expect-error
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
}
|
||||
);
|
||||
|
||||
sessionIds.push(sessionId);
|
||||
times++;
|
||||
} while (times < 2);
|
||||
_.uniq(sessionIds).length.should.equal(1);
|
||||
|
||||
const {status, data} = await endSession(sessionIds[0]);
|
||||
status.should.equal(200);
|
||||
should.equal(data.value, null);
|
||||
});
|
||||
|
||||
it('should handle idempotency while creating parallel sessions', async function () {
|
||||
const reqs = [];
|
||||
let times = 0;
|
||||
do {
|
||||
reqs.push(
|
||||
startSession(
|
||||
{
|
||||
capabilities: {
|
||||
alwaysMatch: defaultCaps,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'X-Idempotency-Key': '12345',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
times++;
|
||||
} while (times < 2);
|
||||
const sessionIds = _.map(await B.all(reqs), 'sessionId');
|
||||
_.uniq(sessionIds).length.should.equal(1);
|
||||
|
||||
const {status, data} = await endSession(sessionIds[0]);
|
||||
status.should.equal(200);
|
||||
should.equal(data.value, null);
|
||||
});
|
||||
|
||||
it('should create session and retrieve a session id, then delete it', async function () {
|
||||
let {status, data} = await axios.post(newSessionURL, {
|
||||
capabilities: {
|
||||
alwaysMatch: defaultCaps,
|
||||
},
|
||||
});
|
||||
|
||||
status.should.equal(200);
|
||||
should.exist(data.value.sessionId);
|
||||
data.value.capabilities.platformName.should.equal(defaultCaps.platformName);
|
||||
data.value.capabilities.deviceName.should.equal(defaultCaps['appium:deviceName']);
|
||||
|
||||
({status, data} = await endSession(/** @type {string} */ (d.sessionId)));
|
||||
|
||||
status.should.equal(200);
|
||||
should.equal(data.value, null);
|
||||
should.equal(d.sessionId, null);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should throw NYI for commands not implemented', async function () {});
|
||||
|
||||
describe('command timeouts', function () {
|
||||
let originalFindElement, originalFindElements;
|
||||
|
||||
/**
|
||||
* @param {number} [timeout]
|
||||
*/
|
||||
async function startTimeoutSession(timeout) {
|
||||
const caps = _.cloneDeep(defaultCaps);
|
||||
caps['appium:newCommandTimeout'] = timeout;
|
||||
return await startSession({capabilities: {alwaysMatch: caps}});
|
||||
}
|
||||
|
||||
before(function () {
|
||||
originalFindElement = d.findElement;
|
||||
d.findElement = function () {
|
||||
return 'foo';
|
||||
}.bind(d);
|
||||
|
||||
originalFindElements = d.findElements;
|
||||
d.findElements = async function () {
|
||||
await B.delay(200);
|
||||
return ['foo'];
|
||||
}.bind(d);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
d.findElement = originalFindElement;
|
||||
d.findElements = originalFindElements;
|
||||
});
|
||||
|
||||
it('should set a default commandTimeout', async function () {
|
||||
let newSession = await startTimeoutSession();
|
||||
d.newCommandTimeoutMs.should.be.above(0);
|
||||
await endSession(newSession.sessionId);
|
||||
});
|
||||
|
||||
it('should timeout on commands using commandTimeout cap', async function () {
|
||||
let newSession = await startTimeoutSession(0.25);
|
||||
const sessionId = /** @type {string} */ (d.sessionId);
|
||||
await postCommand(sessionId, 'element', {
|
||||
using: 'name',
|
||||
value: 'foo',
|
||||
});
|
||||
await B.delay(400);
|
||||
const value = await getSession(sessionId);
|
||||
should.equal(value.error, 'invalid session id');
|
||||
should.equal(d.sessionId, null);
|
||||
const resp = (await endSession(newSession.sessionId)).data.value;
|
||||
should.equal(resp?.error, 'invalid session id');
|
||||
});
|
||||
|
||||
it('should not timeout with commandTimeout of false', async function () {
|
||||
let newSession = await startTimeoutSession(0.1);
|
||||
let start = Date.now();
|
||||
const value = await postCommand(/** @type {string} */ (d.sessionId), 'elements', {
|
||||
using: 'name',
|
||||
value: 'foo',
|
||||
});
|
||||
(Date.now() - start).should.be.above(150);
|
||||
value.should.eql(['foo']);
|
||||
await endSession(newSession.sessionId);
|
||||
});
|
||||
|
||||
it('should not timeout with commandTimeout of 0', async function () {
|
||||
d.newCommandTimeoutMs = 2;
|
||||
let newSession = await startTimeoutSession(0);
|
||||
|
||||
await postCommand(/** @type {string} */ (d.sessionId), 'element', {
|
||||
using: 'name',
|
||||
value: 'foo',
|
||||
});
|
||||
await B.delay(400);
|
||||
const value = await getSession(/** @type {string} */ (d.sessionId));
|
||||
value.platformName?.should.equal(defaultCaps.platformName);
|
||||
const resp = (await endSession(newSession.sessionId)).data.value;
|
||||
should.equal(resp, null);
|
||||
|
||||
d.newCommandTimeoutMs = 60 * 1000;
|
||||
});
|
||||
|
||||
it('should not timeout if its just the command taking awhile', async function () {
|
||||
let newSession = await startTimeoutSession(0.25);
|
||||
// XXX: race condition: we must build this URL before ...something happens...
|
||||
// which causes `d.sessionId` to be missing
|
||||
const {sessionId} = d;
|
||||
|
||||
await postCommand(/** @type {string} */ (d.sessionId), 'element', {
|
||||
using: 'name',
|
||||
value: 'foo',
|
||||
});
|
||||
await B.delay(400);
|
||||
const value = await getSession(/** @type {string} */ (sessionId));
|
||||
value.error.should.equal('invalid session id');
|
||||
should.equal(d.sessionId, null);
|
||||
const resp = (await endSession(newSession.sessionId)).data.value;
|
||||
/** @type {string} */ (/** @type { {error: string} } */ (resp).error).should.equal(
|
||||
'invalid session id'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not have a timer running before or after a session', async function () {
|
||||
// @ts-expect-error
|
||||
should.not.exist(d.noCommandTimer);
|
||||
let newSession = await startTimeoutSession(0.25);
|
||||
newSession.sessionId.should.equal(d.sessionId);
|
||||
// @ts-expect-error
|
||||
should.exist(d.noCommandTimer);
|
||||
await endSession(newSession.sessionId);
|
||||
// @ts-expect-error
|
||||
should.not.exist(d.noCommandTimer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings api', function () {
|
||||
before(function () {
|
||||
d.settings = new DeviceSettings({ignoreUnimportantViews: false});
|
||||
});
|
||||
it('should be able to get settings object', function () {
|
||||
d.settings.getSettings().ignoreUnimportantViews.should.be.false;
|
||||
});
|
||||
it('should not reject when `updateSettings` method is not provided', async function () {
|
||||
await d.settings.update({ignoreUnimportantViews: true}).should.not.be.rejected;
|
||||
});
|
||||
it('should reject for invalid update object', async function () {
|
||||
await d.settings
|
||||
// @ts-expect-error
|
||||
.update('invalid json')
|
||||
.should.be.rejectedWith('JSON');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unexpected exits', function () {
|
||||
/** @type {import('sinon').SinonSandbox} */
|
||||
let sandbox;
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should reject a current command when the driver crashes', async function () {
|
||||
sandbox.stub(d, 'getStatus').callsFake(async function () {
|
||||
await B.delay(5000);
|
||||
});
|
||||
const reqPromise = getCommand('status', {validateStatus: null});
|
||||
// make sure that the request gets to the server before our shutdown
|
||||
await B.delay(100);
|
||||
const shutdownEventPromise = new B((resolve, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
'onUnexpectedShutdown event is expected to be fired within 5 seconds timeout'
|
||||
)
|
||||
),
|
||||
5000
|
||||
);
|
||||
d.onUnexpectedShutdown(resolve);
|
||||
});
|
||||
d.startUnexpectedShutdown(new Error('Crashytimes'));
|
||||
const value = await reqPromise;
|
||||
value.message.should.contain('Crashytimes');
|
||||
await shutdownEventPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe('event timings', function () {
|
||||
it('should not add timings if not using opt-in cap', async function () {
|
||||
const session = await startSession({capabilities: {alwaysMatch: defaultCaps}});
|
||||
const res = await getSession(session.sessionId);
|
||||
should.not.exist(res.events);
|
||||
await endSession(session.sessionId);
|
||||
});
|
||||
it('should add start session timings', async function () {
|
||||
const caps = {...defaultCaps, 'appium:eventTimings': true};
|
||||
const session = await startSession({capabilities: {alwaysMatch: caps}});
|
||||
const res = await getSession(session.sessionId);
|
||||
should.exist(res.events);
|
||||
should.exist(res.events?.newSessionRequested);
|
||||
should.exist(res.events?.newSessionStarted);
|
||||
res.events?.newSessionRequested[0].should.be.a('number');
|
||||
res.events?.newSessionStarted[0].should.be.a('number');
|
||||
await endSession(session.sessionId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@linkcode DriverClass}, except using the base {@linkcode Driver} type instead of `ExternalDriver`.
|
||||
* This allows the suite to work for `BaseDriver`.
|
||||
* @template {Driver} P
|
||||
* @typedef {import('@appium/types').DriverClass<P>} DriverClass
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').Capabilities} Capabilities
|
||||
* @typedef {import('@appium/types').Driver} Driver
|
||||
* @typedef {import('@appium/types').DriverStatic} DriverStatic
|
||||
* @typedef {import('@appium/types').AppiumW3CCapabilities} AppiumW3CCapabilities
|
||||
* @typedef {import('axios').AxiosRequestConfig} AxiosRequestConfig
|
||||
* @typedef {import('@appium/types').SingularSessionData} SingularSessionData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T,D
|
||||
* @typedef {import('axios').AxiosResponse<T, D>} AxiosResponse
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef NewSessionData
|
||||
* @property {import('type-fest').RequireAtLeastOne<import('@appium/types').W3CCapabilities, 'firstMatch'|'alwaysMatch'>} capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef NewSessionResponse
|
||||
* @property {string} sessionId,
|
||||
* @property {import('@appium/types').Capabilities} capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some E2E helpers for making requests and managing sessions
|
||||
* See {@linkcode createSessionHelpers}
|
||||
* @template [CommandData=unknown]
|
||||
* @template [ResponseData=any]
|
||||
* @typedef SessionHelpers
|
||||
* @property {string} newSessionURL - URL to create a new session. Can be used with raw `axios` requests to fully inspect raw response. Mostly, this will not be used.
|
||||
* @property {(data: NewSessionData, config?: AxiosRequestConfig) => Promise<NewSessionResponse>} startSession - Begin a session
|
||||
* @property {(sessionId: string) => Promise<AxiosResponse<{value: {error?: string}?}, {validateStatus: null}>>} endSession - End a session. _Note: resolves with raw response object_
|
||||
* @property {(sessionId: string) => Promise<SingularSessionData>} getSession - Get info about a session
|
||||
* @property {(sessionId: string, cmdName: string, data?: CommandData, config?: AxiosRequestConfig) => Promise<ResponseData>} postCommand - Send an arbitrary command via `POST`.
|
||||
* @property {(sessionIdOrCmdName: string, cmdNameOrConfig: string|AxiosRequestConfig, config?: AxiosRequestConfig) => Promise<ResponseData>} getCommand - Send an arbitrary command via `GET`. Optional `sessionId`.
|
||||
*/
|
||||
68
packages/driver-test-support/lib/helpers.js
Normal file
68
packages/driver-test-support/lib/helpers.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import getPort from 'get-port';
|
||||
import {curry} from 'lodash';
|
||||
|
||||
/**
|
||||
* Default test host
|
||||
*/
|
||||
const TEST_HOST = '127.0.0.1';
|
||||
|
||||
let testPort;
|
||||
|
||||
/**
|
||||
* Returns a free port; one per process
|
||||
* @param {boolean} [force] - If true, do not reuse the port (if it already exists)
|
||||
* @returns {Promise<number>} a free port
|
||||
*/
|
||||
async function getTestPort(force = false) {
|
||||
if (force || !testPort) {
|
||||
let port = await getPort();
|
||||
if (!testPort) {
|
||||
testPort = port;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
return testPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an Appium URL from components.
|
||||
*
|
||||
* **All** parameters are required. Provide an empty string (`''`) if you don't need one.
|
||||
* To rearrange arguments (if needed), use the placeholder from Lodash (`_`).
|
||||
*
|
||||
*/
|
||||
const createAppiumURL = curry(
|
||||
/**
|
||||
* @param {string} address - Base address (w/ optional protocol)
|
||||
* @param {string|number} port - Port number
|
||||
* @param {string?} session - Session ID
|
||||
* @param {string} pathname - Extra path
|
||||
* @returns {string} New URL
|
||||
* @example
|
||||
*
|
||||
* import _ from 'lodash';
|
||||
*
|
||||
* // http://127.0.0.1:31337/session
|
||||
* createAppiumURL('127.0.0.1', 31337, '', 'session')
|
||||
*
|
||||
* // http://127.0.0.1:31337/session/asdfgjkl
|
||||
* const createSessionURL = createAppiumURL('127.0.0.1', 31337, _, 'session')
|
||||
* createSessionURL('asdfgjkl')
|
||||
*
|
||||
* // http://127.0.0.1:31337/session/asdfgjkl/appium/execute
|
||||
* const createURLWithPath = createAppiumURL('127.0.0.1', 31337, 'asdfgjkl');
|
||||
* createURLWithPath('appium/execute')
|
||||
*/
|
||||
(address, port, session, pathname) => {
|
||||
if (!/^https?:\/\//.test(address)) {
|
||||
address = `http://${address}`;
|
||||
}
|
||||
let path = session ? `session/${session}` : '';
|
||||
if (pathname) {
|
||||
path = `${path}/${pathname}`;
|
||||
}
|
||||
return new URL(path, `${address}:${port}`).href;
|
||||
}
|
||||
);
|
||||
|
||||
export {TEST_HOST, getTestPort, createAppiumURL};
|
||||
9
packages/driver-test-support/lib/index.js
Normal file
9
packages/driver-test-support/lib/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './e2e-suite';
|
||||
export * from './unit-suite';
|
||||
export * from './helpers';
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').DriverClass} DriverClass
|
||||
* @typedef {import('@appium/types').W3CCapabilities} W3CCapabilities
|
||||
* @typedef {import('@appium/types').AppiumW3CCapabilities} AppiumW3CCapabilities
|
||||
*/
|
||||
631
packages/driver-test-support/lib/unit-suite.js
Normal file
631
packages/driver-test-support/lib/unit-suite.js
Normal file
@@ -0,0 +1,631 @@
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
import chai from 'chai';
|
||||
|
||||
const should = chai.should();
|
||||
const {expect} = chai;
|
||||
|
||||
// wrap these tests in a function so we can export the tests and re-use them
|
||||
// for actual driver implementations
|
||||
|
||||
/**
|
||||
* Creates unit test suites for a driver.
|
||||
* @param {DriverClass} DriverClass
|
||||
* @param {AppiumW3CCapabilities} [defaultCaps]
|
||||
*/
|
||||
|
||||
export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
|
||||
// to display the driver under test in report
|
||||
const className = DriverClass.name ?? '(unknown driver)';
|
||||
|
||||
describe(`BaseDriver unit suite (as ${className})`, function () {
|
||||
/** @type {InstanceType<typeof DriverClass>} */
|
||||
let d;
|
||||
/** @type {W3CCapabilities} */
|
||||
let w3cCaps;
|
||||
/** @type {import('sinon').SinonSandbox} */
|
||||
let sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = createSandbox();
|
||||
d = new DriverClass();
|
||||
w3cCaps = {
|
||||
alwaysMatch: {
|
||||
...defaultCaps,
|
||||
platformName: 'Fake',
|
||||
'appium:deviceName': 'Commodore 64',
|
||||
},
|
||||
firstMatch: [{}],
|
||||
};
|
||||
});
|
||||
afterEach(async function () {
|
||||
sandbox.restore();
|
||||
await d.deleteSession();
|
||||
});
|
||||
|
||||
describe('static property', function () {
|
||||
describe('baseVersion', function () {
|
||||
it('should exist', function () {
|
||||
DriverClass.baseVersion.should.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Log prefix', function () {
|
||||
it('should setup log prefix', async function () {
|
||||
const d = new DriverClass();
|
||||
const previousPrefix = d.log.prefix;
|
||||
await d.createSession({
|
||||
alwaysMatch: {...defaultCaps, platformName: 'Fake', 'appium:deviceName': 'Commodore 64'},
|
||||
firstMatch: [{}],
|
||||
});
|
||||
try {
|
||||
expect(previousPrefix).not.to.eql(d.log.prefix);
|
||||
} finally {
|
||||
await d.deleteSession();
|
||||
expect(previousPrefix).to.eql(d.log.prefix);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an empty status object', async function () {
|
||||
let status = await d.getStatus();
|
||||
status.should.eql({});
|
||||
});
|
||||
|
||||
it('should return a sessionId from createSession', async function () {
|
||||
let [sessId] = await d.createSession(w3cCaps);
|
||||
should.exist(sessId);
|
||||
sessId.should.be.a('string');
|
||||
sessId.length.should.be.above(5);
|
||||
});
|
||||
|
||||
it('should not be able to start two sessions without closing the first', async function () {
|
||||
await d.createSession(_.cloneDeep(w3cCaps));
|
||||
await d.createSession(_.cloneDeep(w3cCaps)).should.be.rejectedWith('session');
|
||||
});
|
||||
|
||||
it('should be able to delete a session', async function () {
|
||||
let sessionId1 = await d.createSession(_.cloneDeep(w3cCaps));
|
||||
await d.deleteSession();
|
||||
should.equal(d.sessionId, null);
|
||||
let sessionId2 = await d.createSession(_.cloneDeep(w3cCaps));
|
||||
sessionId1.should.not.eql(sessionId2);
|
||||
});
|
||||
|
||||
it('should get the current session', async function () {
|
||||
let [, caps] = await d.createSession(w3cCaps);
|
||||
caps.should.equal(await d.getSession());
|
||||
});
|
||||
|
||||
it('should return sessions if no session exists', async function () {
|
||||
let sessions = await d.getSessions();
|
||||
sessions.length.should.equal(0);
|
||||
});
|
||||
|
||||
it('should return sessions', async function () {
|
||||
const caps = _.cloneDeep(w3cCaps);
|
||||
await d.createSession(caps);
|
||||
let sessions = await d.getSessions();
|
||||
|
||||
sessions.length.should.equal(1);
|
||||
sessions[0].should.include({
|
||||
id: d.sessionId,
|
||||
});
|
||||
sessions[0].capabilities.should.include({
|
||||
deviceName: 'Commodore 64',
|
||||
platformName: 'Fake',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fulfill an unexpected driver quit promise', async function () {
|
||||
// make a command that will wait a bit so we can crash while it's running
|
||||
sandbox.stub(d, 'getStatus').callsFake(async () => {
|
||||
await B.delay(1000);
|
||||
return 'good status';
|
||||
});
|
||||
let cmdPromise = d.executeCommand('getStatus');
|
||||
await B.delay(10);
|
||||
const p = new B((resolve, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
'onUnexpectedShutdown event is expected to be fired within 5 seconds timeout'
|
||||
)
|
||||
),
|
||||
5000
|
||||
);
|
||||
d.onUnexpectedShutdown(resolve);
|
||||
});
|
||||
d.startUnexpectedShutdown(new Error('We crashed'));
|
||||
await cmdPromise.should.be.rejectedWith(/We crashed/);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('should not allow commands in middle of unexpected shutdown', async function () {
|
||||
// make a command that will wait a bit so we can crash while it's running
|
||||
sandbox.stub(d, 'deleteSession').callsFake(async function () {
|
||||
await B.delay(100);
|
||||
DriverClass.prototype.deleteSession.call(this);
|
||||
});
|
||||
await d.createSession(w3cCaps);
|
||||
const p = new B((resolve, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
'onUnexpectedShutdown event is expected to be fired within 5 seconds timeout'
|
||||
)
|
||||
),
|
||||
5000
|
||||
);
|
||||
d.onUnexpectedShutdown(resolve);
|
||||
});
|
||||
d.startUnexpectedShutdown(new Error('We crashed'));
|
||||
await p;
|
||||
await d.executeCommand('getSession').should.be.rejectedWith(/shut down/);
|
||||
});
|
||||
|
||||
it('should allow new commands after done shutting down', async function () {
|
||||
// make a command that will wait a bit so we can crash while it's running
|
||||
sandbox.stub(d, 'deleteSession').callsFake(async function () {
|
||||
await B.delay(100);
|
||||
DriverClass.prototype.deleteSession.call(this);
|
||||
});
|
||||
|
||||
await d.createSession(_.cloneDeep(w3cCaps));
|
||||
const p = new B((resolve, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
'onUnexpectedShutdown event is expected to be fired within 5 seconds timeout'
|
||||
)
|
||||
),
|
||||
5000
|
||||
);
|
||||
d.onUnexpectedShutdown(resolve);
|
||||
});
|
||||
d.startUnexpectedShutdown(new Error('We crashed'));
|
||||
await p;
|
||||
|
||||
await d.executeCommand('getSession').should.be.rejectedWith(/shut down/);
|
||||
await B.delay(500);
|
||||
|
||||
await d.executeCommand('createSession', null, null, _.cloneDeep(w3cCaps));
|
||||
await d.deleteSession();
|
||||
});
|
||||
|
||||
it('should distinguish between W3C and JSONWP session', async function () {
|
||||
// Test W3C (leave first 2 args null because those are the JSONWP args)
|
||||
await d.executeCommand('createSession', null, null, {
|
||||
alwaysMatch: {
|
||||
...defaultCaps,
|
||||
platformName: 'Fake',
|
||||
'appium:deviceName': 'Commodore 64',
|
||||
},
|
||||
firstMatch: [{}],
|
||||
});
|
||||
|
||||
expect(d.protocol).to.equal('W3C');
|
||||
});
|
||||
|
||||
describe('protocol detection', function () {
|
||||
it('should use W3C if only W3C caps are provided', async function () {
|
||||
await d.createSession({
|
||||
alwaysMatch: _.clone(defaultCaps),
|
||||
firstMatch: [{}],
|
||||
});
|
||||
expect(d.protocol).to.equal('W3C');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a method to get driver for a session', async function () {
|
||||
let [sessId] = await d.createSession(w3cCaps);
|
||||
expect(d.driverForSession(sessId)).to.eql(d);
|
||||
});
|
||||
|
||||
describe('command queue', function () {
|
||||
/** @type {InstanceType<DriverClass>} */
|
||||
let d;
|
||||
let waitMs = 10;
|
||||
|
||||
beforeEach(function () {
|
||||
d = new DriverClass();
|
||||
sandbox.stub(d, 'getStatus').callsFake(async () => {
|
||||
await B.delay(waitMs);
|
||||
return Date.now();
|
||||
});
|
||||
sandbox.stub(d, 'getSessions').callsFake(async () => {
|
||||
await B.delay(waitMs);
|
||||
throw new Error('multipass');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await d.clearNewCommandTimeout();
|
||||
});
|
||||
|
||||
it('should queue commands and.executeCommand/respond in the order received', async function () {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.executeCommand('getStatus'));
|
||||
}
|
||||
let results = await B.all(cmds);
|
||||
for (let i = 1; i < numCmds; i++) {
|
||||
if (results[i] <= results[i - 1]) {
|
||||
throw new Error('Got result out of order');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle errors correctly when queuing', async function () {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
if (i === 5) {
|
||||
cmds.push(d.executeCommand('getSessions'));
|
||||
} else {
|
||||
cmds.push(d.executeCommand('getStatus'));
|
||||
}
|
||||
}
|
||||
let results = /** @type {PromiseFulfilledResult<any>[]} */ (
|
||||
// eslint-disable-next-line promise/no-native
|
||||
await Promise.allSettled(cmds)
|
||||
);
|
||||
for (let i = 1; i < 5; i++) {
|
||||
if (results[i].value <= results[i - 1].value) {
|
||||
throw new Error('Got result out of order');
|
||||
}
|
||||
}
|
||||
/** @type {PromiseRejectedResult} */ (
|
||||
/** @type {unknown} */ (results[5])
|
||||
).reason.message.should.contain('multipass');
|
||||
for (let i = 7; i < numCmds; i++) {
|
||||
if (results[i].value <= results[i - 1].value) {
|
||||
throw new Error('Got result out of order');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should not care if queue empties for a bit', async function () {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.executeCommand('getStatus'));
|
||||
}
|
||||
let results = await B.all(cmds);
|
||||
cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.executeCommand('getStatus'));
|
||||
}
|
||||
results = await B.all(cmds);
|
||||
for (let i = 1; i < numCmds; i++) {
|
||||
if (results[i] <= results[i - 1]) {
|
||||
throw new Error('Got result out of order');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeouts', function () {
|
||||
before(async function () {
|
||||
await d.createSession(w3cCaps);
|
||||
});
|
||||
describe('command', function () {
|
||||
it('should exist by default', function () {
|
||||
d.newCommandTimeoutMs.should.equal(60000);
|
||||
});
|
||||
it('should be settable through `timeouts`', async function () {
|
||||
await d.timeouts('command', 20);
|
||||
d.newCommandTimeoutMs.should.equal(20);
|
||||
});
|
||||
});
|
||||
describe('implicit', function () {
|
||||
it('should not exist by default', function () {
|
||||
d.implicitWaitMs.should.equal(0);
|
||||
});
|
||||
it('should be settable through `timeouts`', async function () {
|
||||
await d.timeouts('implicit', 20);
|
||||
d.implicitWaitMs.should.equal(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeouts (W3C)', function () {
|
||||
beforeEach(async function () {
|
||||
await d.createSession(w3cCaps);
|
||||
});
|
||||
afterEach(async function () {
|
||||
await d.deleteSession();
|
||||
});
|
||||
it('should get timeouts that we set', async function () {
|
||||
// @ts-expect-error
|
||||
await d.timeouts(undefined, undefined, undefined, undefined, 1000);
|
||||
await d.getTimeouts().should.eventually.have.property('implicit', 1000);
|
||||
await d.timeouts('command', 2000);
|
||||
await d.getTimeouts().should.eventually.deep.equal({
|
||||
implicit: 1000,
|
||||
command: 2000,
|
||||
});
|
||||
// @ts-expect-error
|
||||
await d.timeouts(undefined, undefined, undefined, undefined, 3000);
|
||||
await d.getTimeouts().should.eventually.deep.equal({
|
||||
implicit: 3000,
|
||||
command: 2000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset compatibility', function () {
|
||||
it('should not allow both fullReset and noReset to be true', async function () {
|
||||
const newCaps = {
|
||||
alwaysMatch: {
|
||||
...defaultCaps,
|
||||
platformName: 'Fake',
|
||||
'appium:deviceName': 'Commodore 64',
|
||||
'appium:fullReset': true,
|
||||
'appium:noReset': true,
|
||||
},
|
||||
firstMatch: [{}],
|
||||
};
|
||||
await d.createSession(newCaps).should.be.rejectedWith(/noReset.+fullReset/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxying', function () {
|
||||
let sessId;
|
||||
beforeEach(async function () {
|
||||
[sessId] = await d.createSession(w3cCaps);
|
||||
});
|
||||
describe('#proxyActive', function () {
|
||||
it('should exist', function () {
|
||||
d.proxyActive.should.be.an.instanceof(Function);
|
||||
});
|
||||
it('should return false', function () {
|
||||
d.proxyActive(sessId).should.be.false;
|
||||
});
|
||||
it('should throw an error when sessionId is wrong', function () {
|
||||
(() => {
|
||||
d.proxyActive('aaa');
|
||||
}).should.throw;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getProxyAvoidList', function () {
|
||||
it('should exist', function () {
|
||||
d.getProxyAvoidList.should.be.an.instanceof(Function);
|
||||
});
|
||||
it('should return an array', function () {
|
||||
d.getProxyAvoidList(sessId).should.be.an.instanceof(Array);
|
||||
});
|
||||
it('should throw an error when sessionId is wrong', function () {
|
||||
(() => {
|
||||
d.getProxyAvoidList('aaa');
|
||||
}).should.throw;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#canProxy', function () {
|
||||
it('should have a #canProxy method', function () {
|
||||
d.canProxy.should.be.an.instanceof(Function);
|
||||
});
|
||||
it('should return a boolean from #canProxy', function () {
|
||||
d.canProxy(sessId).should.be.a('boolean');
|
||||
});
|
||||
it('should throw an error when sessionId is wrong', function () {
|
||||
(() => {
|
||||
d.canProxy();
|
||||
}).should.throw;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#proxyRouteIsAvoided', function () {
|
||||
it('should validate form of avoidance list', function () {
|
||||
const avoidStub = sandbox.stub(d, 'getProxyAvoidList');
|
||||
// @ts-expect-error
|
||||
avoidStub.returns([['POST', /\/foo/], ['GET']]);
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
d.proxyRouteIsAvoided();
|
||||
}).should.throw;
|
||||
avoidStub.returns([
|
||||
['POST', /\/foo/],
|
||||
// @ts-expect-error
|
||||
['GET', /^foo/, 'bar'],
|
||||
]);
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
d.proxyRouteIsAvoided();
|
||||
}).should.throw;
|
||||
});
|
||||
it('should reject bad http methods', function () {
|
||||
const avoidStub = sandbox.stub(d, 'getProxyAvoidList');
|
||||
avoidStub.returns([
|
||||
['POST', /^foo/],
|
||||
['BAZETE', /^bar/],
|
||||
]);
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
d.proxyRouteIsAvoided();
|
||||
}).should.throw;
|
||||
});
|
||||
it('should reject non-regex routes', function () {
|
||||
const avoidStub = sandbox.stub(d, 'getProxyAvoidList');
|
||||
avoidStub.returns([
|
||||
['POST', /^foo/],
|
||||
// @ts-expect-error
|
||||
['GET', '/bar'],
|
||||
]);
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
d.proxyRouteIsAvoided();
|
||||
}).should.throw;
|
||||
});
|
||||
it('should return true for routes in the avoid list', function () {
|
||||
const avoidStub = sandbox.stub(d, 'getProxyAvoidList');
|
||||
avoidStub.returns([['POST', /^\/foo/]]);
|
||||
d.proxyRouteIsAvoided('foo', 'POST', '/foo/bar').should.be.true;
|
||||
});
|
||||
it('should strip away any wd/hub prefix', function () {
|
||||
const avoidStub = sandbox.stub(d, 'getProxyAvoidList');
|
||||
avoidStub.returns([['POST', /^\/foo/]]);
|
||||
d.proxyRouteIsAvoided('foo', 'POST', '/foo/bar').should.be.true;
|
||||
});
|
||||
it('should return false for routes not in the avoid list', function () {
|
||||
const avoidStub = sandbox.stub(d, 'getProxyAvoidList');
|
||||
avoidStub.returns([['POST', /^\/foo/]]);
|
||||
d.proxyRouteIsAvoided('foo', 'GET', '/foo/bar').should.be.false;
|
||||
d.proxyRouteIsAvoided('foo', 'POST', '/boo').should.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('event timing framework', function () {
|
||||
let beforeStartTime;
|
||||
beforeEach(async function () {
|
||||
beforeStartTime = Date.now();
|
||||
d.shouldValidateCaps = false;
|
||||
await d.executeCommand('createSession', null, null, {
|
||||
alwaysMatch: {...defaultCaps},
|
||||
firstMatch: [{}],
|
||||
});
|
||||
});
|
||||
describe('#eventHistory', function () {
|
||||
it('should have an eventHistory property', function () {
|
||||
should.exist(d.eventHistory);
|
||||
should.exist(d.eventHistory.commands);
|
||||
});
|
||||
|
||||
it('should have a session start timing after session start', function () {
|
||||
let {newSessionRequested, newSessionStarted} = d.eventHistory;
|
||||
newSessionRequested.should.have.length(1);
|
||||
newSessionStarted.should.have.length(1);
|
||||
newSessionRequested[0].should.be.a('number');
|
||||
newSessionStarted[0].should.be.a('number');
|
||||
(newSessionRequested[0] >= beforeStartTime).should.be.true;
|
||||
(newSessionStarted[0] >= newSessionRequested[0]).should.be.true;
|
||||
});
|
||||
|
||||
it('should include a commands list', async function () {
|
||||
await d.executeCommand('getStatus', []);
|
||||
d.eventHistory.commands.length.should.equal(2);
|
||||
d.eventHistory.commands[1].cmd.should.equal('getStatus');
|
||||
d.eventHistory.commands[1].startTime.should.be.a('number');
|
||||
d.eventHistory.commands[1].endTime.should.be.a('number');
|
||||
});
|
||||
});
|
||||
describe('#logEvent', function () {
|
||||
it('should allow logging arbitrary events', function () {
|
||||
d.logEvent('foo');
|
||||
d.eventHistory.foo[0].should.be.a('number');
|
||||
(d.eventHistory.foo[0] >= beforeStartTime).should.be.true;
|
||||
});
|
||||
it('should not allow reserved or oddly formed event names', function () {
|
||||
(() => {
|
||||
d.logEvent('commands');
|
||||
}).should.throw();
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
d.logEvent(1);
|
||||
}).should.throw();
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
d.logEvent({});
|
||||
}).should.throw();
|
||||
});
|
||||
});
|
||||
it('should allow logging the same event multiple times', function () {
|
||||
d.logEvent('bar');
|
||||
d.logEvent('bar');
|
||||
d.eventHistory.bar.should.have.length(2);
|
||||
d.eventHistory.bar[1].should.be.a('number');
|
||||
(d.eventHistory.bar[1] >= d.eventHistory.bar[0]).should.be.true;
|
||||
});
|
||||
describe('getSession decoration', function () {
|
||||
it('should decorate getSession response if opt-in cap is provided', async function () {
|
||||
let res = await d.getSession();
|
||||
should.not.exist(res.events);
|
||||
|
||||
_.set(d, 'caps.eventTimings', true);
|
||||
res = await d.getSession();
|
||||
should.exist(res.events);
|
||||
should.exist(res.events?.newSessionRequested);
|
||||
expect(res.events?.newSessionRequested[0]).to.be.a('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('.reset', function () {
|
||||
it('should reset as W3C if the original session was W3C', async function () {
|
||||
const caps = {
|
||||
alwaysMatch: {
|
||||
'appium:app': 'Fake',
|
||||
'appium:deviceName': 'Fake',
|
||||
'appium:automationName': 'Fake',
|
||||
platformName: 'Fake',
|
||||
...defaultCaps,
|
||||
},
|
||||
|
||||
firstMatch: [{}],
|
||||
};
|
||||
await d.createSession(caps);
|
||||
expect(d.protocol).to.equal('W3C');
|
||||
await d.reset();
|
||||
expect(d.protocol).to.equal('W3C');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isFeatureEnabled', function () {
|
||||
let d;
|
||||
|
||||
beforeEach(function () {
|
||||
d = new DriverClass();
|
||||
});
|
||||
|
||||
it('should say a feature is enabled when it is explicitly allowed', function () {
|
||||
d.allowInsecure = ['foo', 'bar'];
|
||||
d.isFeatureEnabled('foo').should.be.true;
|
||||
d.isFeatureEnabled('bar').should.be.true;
|
||||
d.isFeatureEnabled('baz').should.be.false;
|
||||
});
|
||||
|
||||
it('should say a feature is not enabled if it is not enabled', function () {
|
||||
d.allowInsecure = [];
|
||||
d.isFeatureEnabled('foo').should.be.false;
|
||||
});
|
||||
|
||||
it('should prefer denyInsecure to allowInsecure', function () {
|
||||
d.allowInsecure = ['foo', 'bar'];
|
||||
d.denyInsecure = ['foo'];
|
||||
d.isFeatureEnabled('foo').should.be.false;
|
||||
d.isFeatureEnabled('bar').should.be.true;
|
||||
d.isFeatureEnabled('baz').should.be.false;
|
||||
});
|
||||
|
||||
it('should allow global setting for insecurity', function () {
|
||||
d.relaxedSecurityEnabled = true;
|
||||
d.isFeatureEnabled('foo').should.be.true;
|
||||
d.isFeatureEnabled('bar').should.be.true;
|
||||
d.isFeatureEnabled('baz').should.be.true;
|
||||
});
|
||||
|
||||
it('global setting should be overrideable', function () {
|
||||
d.relaxedSecurityEnabled = true;
|
||||
d.denyInsecure = ['foo', 'bar'];
|
||||
d.isFeatureEnabled('foo').should.be.false;
|
||||
d.isFeatureEnabled('bar').should.be.false;
|
||||
d.isFeatureEnabled('baz').should.be.true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').DriverClass} DriverClass
|
||||
* @typedef {import('@appium/types').W3CCapabilities} W3CCapabilities
|
||||
* @typedef {import('@appium/types').AppiumW3CCapabilities} AppiumW3CCapabilities
|
||||
*/
|
||||
69
packages/driver-test-support/package.json
Normal file
69
packages/driver-test-support/package.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.1.0",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "packages/driver-test-support"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib",
|
||||
"build"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "babel lib --root-mode=upward --out-dir=build/lib",
|
||||
"dev": "npm run build -- --watch",
|
||||
"fix": "npm run lint -- --fix",
|
||||
"lint": "eslint -c ../../.eslintrc --ignore-path ../../.eslintignore .",
|
||||
"prepare": "npm run build",
|
||||
"test": "npm run test:unit",
|
||||
"test:smoke": "node ./index.js",
|
||||
"test:unit": "mocha \"test/unit/**/*.spec.js\""
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "0.4.0",
|
||||
"@babel/runtime": "7.18.9",
|
||||
"@types/lodash": "4.14.184",
|
||||
"axios": "0.27.2",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "4.3.6",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "14.0.0",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
"mocha": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
41
packages/driver-test-support/test/unit/index.spec.js
Normal file
41
packages/driver-test-support/test/unit/index.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import {TEST_HOST, getTestPort, createAppiumURL} from '../../lib';
|
||||
import _ from 'lodash';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('TEST_HOST', function () {
|
||||
it('should be localhost', function () {
|
||||
expect(TEST_HOST).to.equal('127.0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTestPort()', function () {
|
||||
it('should get a free test port', async function () {
|
||||
const port = await getTestPort();
|
||||
expect(port).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAppiumURL()', function () {
|
||||
it('should create a "new session" URL', function () {
|
||||
const actual = createAppiumURL(TEST_HOST, 31337, '', 'session');
|
||||
const expected = `http://${TEST_HOST}:31337/session`;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('should create a URL to get an existing session', function () {
|
||||
const sessionId = '12345';
|
||||
const createGetSessionURL = createAppiumURL(TEST_HOST, 31337, _, 'session');
|
||||
const actual = createGetSessionURL(sessionId);
|
||||
const expected = `http://${TEST_HOST}:31337/session/${sessionId}/session`;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('should create a URL for a command using an existing session', function () {
|
||||
const sessionId = '12345';
|
||||
const createURLWithPath = createAppiumURL('127.0.0.1', 31337, sessionId);
|
||||
const actual = createURLWithPath('moocow');
|
||||
const expected = `http://${TEST_HOST}:31337/session/${sessionId}/moocow`;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
20
packages/driver-test-support/tsconfig.json
Normal file
20
packages/driver-test-support/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../config/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"@appium/types": ["../types"],
|
||||
"appium/driver": ["../base-driver"],
|
||||
},
|
||||
"types": [
|
||||
"mocha", "chai", "chai-as-promised"
|
||||
]
|
||||
},
|
||||
"include": ["./lib"],
|
||||
"references": [
|
||||
{"path": "../types"},
|
||||
{"path": "../base-driver"}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "packages/driver-test-support"
|
||||
},
|
||||
{
|
||||
"path": "packages/schema"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user