mirror of
https://github.com/appium/appium.git
synced 2026-11-11 04:23:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33372d33a3 | ||
|
|
536d218b28 | ||
|
|
5b3e0ef9c5 | ||
|
|
fd78e3fd9c | ||
|
|
1ca0708b3a | ||
|
|
f50e41d8f9 | ||
|
|
d3fd0a20f0 | ||
|
|
9a1c0d47ac | ||
|
|
cddfc22f9d | ||
|
|
08d555d451 | ||
|
|
5ec20cfcf0 | ||
|
|
f59f1816f5 | ||
|
|
5ca4881797 | ||
|
|
23777f1c2f | ||
|
|
80f182e250 | ||
|
|
93608a3d89 | ||
|
|
b8e3012507 | ||
|
|
becfcc5120 | ||
|
|
10e324d8c9 | ||
|
|
bb4ae11648 | ||
|
|
19f51a843e | ||
|
|
c4a3fa48b8 | ||
|
|
6b5ecaf796 | ||
|
|
2041727ee5 | ||
|
|
e827755f03 | ||
|
|
9d46e32d1e | ||
|
|
ebe99f872d | ||
|
|
130ca48247 | ||
|
|
206e4bc2c8 | ||
|
|
84d59b2471 | ||
|
|
81199c4868 | ||
|
|
e55e6fa955 | ||
|
|
0458c61a9b | ||
|
|
4f29ecfa43 | ||
|
|
1b842562e6 | ||
|
|
9abacc61be | ||
|
|
70b757d71a | ||
|
|
7f39f01a7e | ||
|
|
cda3ee28ac | ||
|
|
c17772247b | ||
|
|
30e1ffaad6 | ||
|
|
5f3c53f2fb | ||
|
|
53b0416add | ||
|
|
b305420e01 | ||
|
|
6b80428202 | ||
|
|
b765981fe5 | ||
|
|
bff678ab06 | ||
|
|
dd8e1c29d5 | ||
|
|
4f4098efa9 | ||
|
|
e60f3f4e89 | ||
|
|
a2c371e3a7 | ||
|
|
12052bec3d | ||
|
|
32c86894c8 | ||
|
|
574638b5b9 | ||
|
|
e8c2ee6e6a | ||
|
|
ab6d456a54 | ||
|
|
d0b8cb4cfe | ||
|
|
46755b23ab | ||
|
|
962c12b287 | ||
|
|
2e57ce2d52 | ||
|
|
7772fe379b | ||
|
|
3f63e8505c | ||
|
|
81bd9d3a56 | ||
|
|
587282409d | ||
|
|
19c72b51c6 |
@@ -1,3 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
*.log
|
||||
.DS_Store
|
||||
coverage
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.12"
|
||||
after_success:
|
||||
- gulp coveralls
|
||||
|
||||
@@ -1,12 +1,48 @@
|
||||
appium-base-driver
|
||||
===================
|
||||
[](https://npmjs.org/package/appium-base-driver)
|
||||
[](https://npmjs.org/package/appium-base-driver)
|
||||
[](https://david-dm.org/appium/appium-base-driver)
|
||||
[](https://david-dm.org/appium/appium-base-driver#info=devDependencies)
|
||||
|
||||
Work in progress, stay tuned!
|
||||
[](https://travis-ci.org/appium/appium-base-driver)
|
||||
[](https://coveralls.io/r/appium/appium-base-driver?branch=master)
|
||||
|
||||
## Status
|
||||
This is the parent class that all [appium](appium.io) drivers inherit from. Appium drivers themselves can either be started from the command line as standalone appium servers, or can be included by another module (appium) which then proxies commands to the appropriate driver based on [Desired Capabilities](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md).
|
||||
|
||||
An appium driver is a module which processes [Mobile Json Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) commands and controls a device accordingly. The commands can either come in over HTTP as json api requests, or they can be passed to the driver object programmatically as already-parsed json object (without the HTTP headers and junk).
|
||||
|
||||
The appium Base driver already includes the [mjsonwp](https://github.com/appium/node-mobile-json-wire-protocol) module, which is the HTTP server that converts incoming requests into json objects that get sent to the driver programmatically.
|
||||
|
||||
The appium Base driver already has all the REST api routes, validation, and error codes supplied by [mjsonwp](https://github.com/appium/node-mobile-json-wire-protocol).
|
||||
|
||||
Appium drivers are designed to have a *single testing session* per instantiation. This means that one Driver object should be attached to a single device and handle commands from a single client. The main appium driver handles multiple sessions and instantiates a new instance of the desired driver for each new session.
|
||||
|
||||
## Writing your own appium driver
|
||||
|
||||
Writing your own appium driver starts with inheriting and extending this Base driver module.
|
||||
|
||||
Appium Base driver has some properties that all drivers share:
|
||||
|
||||
- `driver.opts` - these are the options passed into the driver constructor. Your driver's constructor should take an object of options and pass it on the the Base driver by calling `super(opts)` in your constructor.
|
||||
|
||||
- `driver.desiredCapConstraints` - Base driver sets this property with a customer `setter` function so that when you create a driver, you can add an object which defines the validation contraints of which desired capabilities your new driver can handle. Of course each driver will have it's own specific desired capabilities. Look for examples on our other drivers.
|
||||
|
||||
- `driver.createSession(caps)` - this is the function which gets desired capabilities and creates a session. Make sure to call `super.createSession(caps)` so that things like `this.sessionId` and `this.caps` are populated, and the caps are validated against your `desiredCapConstraints`.
|
||||
|
||||
- `driver.caps` - these are the desired capabilities for the current session.
|
||||
|
||||
- `driver.sessionId` - this is the ID of the current session. It gets populated automaticall by `baseDriver.createSession`.
|
||||
|
||||
- `driver.proxyReqRes()` - used by mjsonwp module for proxying http commands to another process (like chromedriver or selendroid)
|
||||
|
||||
- `driver.jwpProxyAvoid` - used by mjsonwp module. You can specify what REST api routes which you want to SKIP the automatic proxy to another server (which is optional) and instead be handled by your driver.
|
||||
|
||||
|
||||
Base driver exposes a promise called `onUnexpectedShutdown` which is a promise which your driver must reject in cases where an unexpected error occurs and you want to signal to the appium server at large that your driver is now shutting down.
|
||||
|
||||
Your driver should also implement a startUnexpectedShutdown method?
|
||||
|
||||
[](https://travis-ci.org/appium/appium-base-driver)
|
||||
|
||||
## Watch
|
||||
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@ var gulp = require('gulp'),
|
||||
boilerplate({
|
||||
build: 'appium-base-driver',
|
||||
jscs: false,
|
||||
testReporter: process.env.TRAVIS ? 'spec' : 'nyan'
|
||||
watchE2E: true
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// transpile:main
|
||||
|
||||
import { BaseDriver } from './lib/driver';
|
||||
export { BaseDriver };
|
||||
import DeviceSettings from './lib/device-settings';
|
||||
|
||||
export { BaseDriver, DeviceSettings };
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import sessionCmds from './session';
|
||||
import settingsCmds from './settings';
|
||||
|
||||
let commands = {};
|
||||
Object.assign(
|
||||
commands,
|
||||
sessionCmds
|
||||
sessionCmds,
|
||||
settingsCmds
|
||||
// add other command types here
|
||||
);
|
||||
|
||||
|
||||
+10
-8
@@ -3,23 +3,25 @@ import log from '../logger';
|
||||
import { errors } from 'mobile-json-wire-protocol';
|
||||
import UUID from 'uuid-js';
|
||||
|
||||
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
|
||||
|
||||
let commands = {};
|
||||
|
||||
commands.createSession = async function (caps) {
|
||||
if (this.sessionId !== null) {
|
||||
throw new errors.SessionNotCreatedError();
|
||||
throw new errors.SessionNotCreatedError("Cannot create a new session " +
|
||||
"while one is in progress");
|
||||
}
|
||||
this.validateDesiredCaps(caps);
|
||||
this.sessionId = UUID.create().hex;
|
||||
this.caps = caps;
|
||||
|
||||
// TODO move into capabilities object/parser/class thing
|
||||
if (_.isUndefined(this.caps.newCommandTimeout)) {
|
||||
this.newCommandTimeoutMs = NEW_COMMAND_TIMEOUT_MS;
|
||||
} else {
|
||||
this.newCommandTimeoutMs = (this.caps.newCommandTimeout * 1000) || null;
|
||||
// keep track of initial opts since we're going to mutate opts
|
||||
this.initialOpts = _.clone(this.opts);
|
||||
|
||||
// merge caps onto opts so we don't need to worry about what's where
|
||||
Object.assign(this.opts, this.caps);
|
||||
|
||||
if (!_.isUndefined(this.caps.newCommandTimeout)) {
|
||||
this.newCommandTimeoutMs = (this.caps.newCommandTimeout * 1000);
|
||||
}
|
||||
|
||||
log.info(`Session created with session id: ${this.sessionId}`);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import log from '../logger';
|
||||
|
||||
let commands = {};
|
||||
|
||||
commands.updateSettings = async function (newSettings) {
|
||||
if (!this.settings) {
|
||||
log.errorAndThrow("Cannot update settings; settings object not found");
|
||||
}
|
||||
return this.settings.update(newSettings);
|
||||
};
|
||||
|
||||
commands.getSettings = async function () {
|
||||
if (!this.settings) {
|
||||
log.errorAndThrow("Cannot get settings; settings object not found");
|
||||
}
|
||||
return this.settings.getSettings();
|
||||
};
|
||||
|
||||
export default commands;
|
||||
@@ -0,0 +1,103 @@
|
||||
import log from './logger';
|
||||
import validator from 'validate.js';
|
||||
|
||||
let desiredCapabilityConstraints = {
|
||||
platformName: {
|
||||
presence: true,
|
||||
isString: true,
|
||||
inclusion: [
|
||||
'iOS',
|
||||
'Android',
|
||||
'FirefoxOS',
|
||||
'Fake'
|
||||
]
|
||||
},
|
||||
deviceName: {
|
||||
presence: true,
|
||||
isString: true
|
||||
},
|
||||
platformVersion: {},
|
||||
newCommandTimeout: {
|
||||
isNumber: true
|
||||
},
|
||||
automationName: {
|
||||
inclusion: [
|
||||
'Appium',
|
||||
'Selendroid'
|
||||
]
|
||||
},
|
||||
autoLaunch: {
|
||||
isBoolean: true
|
||||
},
|
||||
udid: {
|
||||
isString: true
|
||||
},
|
||||
orientation: {
|
||||
inclusion: [
|
||||
'LANDSCAPE',
|
||||
'PORTRAIT'
|
||||
]
|
||||
},
|
||||
autoWebview: {
|
||||
isBoolean: true
|
||||
},
|
||||
noReset: {
|
||||
isBoolean: true
|
||||
},
|
||||
fullReset: {
|
||||
isBoolean: true
|
||||
}
|
||||
};
|
||||
|
||||
validator.validators.isString = function (value) {
|
||||
if (typeof value === 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `must be of type string`;
|
||||
};
|
||||
validator.validators.isNumber = function (value) {
|
||||
if (typeof value === 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `must be of type number`;
|
||||
};
|
||||
validator.validators.isBoolean = function (value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `must be of type boolean`;
|
||||
};
|
||||
validator.validators.isObject = function (value) {
|
||||
if (typeof value === 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `must be of type object`;
|
||||
};
|
||||
validator.validators.deprecated = function (value, options, key) {
|
||||
if (options) {
|
||||
log.warn(`${key} is a deprecated capability`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export { desiredCapabilityConstraints, validator };
|
||||
@@ -0,0 +1,38 @@
|
||||
import _ from 'lodash';
|
||||
import log from './logger';
|
||||
|
||||
class DeviceSettings {
|
||||
|
||||
constructor (defaultSettings = {}) {
|
||||
this._settings = defaultSettings;
|
||||
}
|
||||
|
||||
// calls updateSettings from implementing driver every time a setting is changed.
|
||||
async update (newSettings) {
|
||||
if (!_.isObject(newSettings)) {
|
||||
throw new Error("Settings update should be called with valid JSON");
|
||||
}
|
||||
for (let prop of _.keys(newSettings)) {
|
||||
if(this._settings[prop] !== newSettings[prop]) {
|
||||
let updatePayload = {
|
||||
key: prop,
|
||||
value: newSettings[prop],
|
||||
oldValue: this._settings[prop],
|
||||
};
|
||||
this._settings[prop] = newSettings[prop];
|
||||
// update setting only when there is updateSettings defined.
|
||||
if (this.onSettingsUpdate) {
|
||||
await this.onSettingsUpdate(updatePayload);
|
||||
} else {
|
||||
log.errorAndThrow("Unable to update settings; onSettingsUpdate method not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSettings () {
|
||||
return this._settings;
|
||||
}
|
||||
}
|
||||
|
||||
export default DeviceSettings;
|
||||
+94
-5
@@ -1,19 +1,53 @@
|
||||
import { MobileJsonWireProtocol,
|
||||
errors } from 'mobile-json-wire-protocol';
|
||||
import os from 'os';
|
||||
import commands from './commands';
|
||||
import helpers from './helpers';
|
||||
import log from './logger';
|
||||
import { desiredCapabilityConstraints, validator } from './desired-caps';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
|
||||
validator.promise = B;
|
||||
validator.prettify = (val) => { return val; };
|
||||
|
||||
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
|
||||
|
||||
class BaseDriver extends MobileJsonWireProtocol {
|
||||
|
||||
constructor () {
|
||||
constructor (opts = {}, shouldValidateCaps = true) {
|
||||
super();
|
||||
this.sessionId = null;
|
||||
this.opts = opts;
|
||||
this.caps = null;
|
||||
this.helpers = helpers;
|
||||
this.newCommandTimeoutMs = NEW_COMMAND_TIMEOUT_MS;
|
||||
this._constraints = _.cloneDeep(desiredCapabilityConstraints);
|
||||
|
||||
// use a custom tmp dir to avoid losing data and app when computer is
|
||||
// restarted
|
||||
this.opts.tmpDir = this.opts.tmpDir ||
|
||||
process.env.APPIUM_TMP_DIR ||
|
||||
os.tmpDir();
|
||||
|
||||
// base-driver internals
|
||||
this.curCommand = new B((r) => { r(); }); // see note in execute
|
||||
this.curCommandCancellable = new B((r) => { r(); }); // see note in execute
|
||||
this.onUnexpectedShutdown = new B(() => {}).cancellable();
|
||||
// handle this ourselves so we don't get a stacktrace printed out
|
||||
this.onUnexpectedShutdown.catch(() => {});
|
||||
this.shutdownUnexpectedly = false;
|
||||
this.noCommandTimer = null;
|
||||
this.newCommandTimeoutMs = null;
|
||||
this.shouldValidateCaps = shouldValidateCaps;
|
||||
}
|
||||
|
||||
// we only want subclasses to ever extend the contraints
|
||||
set desiredCapConstraints (constraints) {
|
||||
this._constraints = Object.assign(this._constraints, constraints);
|
||||
}
|
||||
|
||||
get desiredCapConstraints () {
|
||||
return this._constraints;
|
||||
}
|
||||
|
||||
// method required by MJSONWP in order to determine whether it should
|
||||
@@ -23,8 +57,41 @@ class BaseDriver extends MobileJsonWireProtocol {
|
||||
return sessionId === this.sessionId;
|
||||
}
|
||||
|
||||
logExtraCaps (caps) {
|
||||
let extraCaps = _.difference(_.keys(caps),
|
||||
_.keys(this._constraints));
|
||||
if (extraCaps.length) {
|
||||
log.warn(`The following capabilities were provided, but are not ` +
|
||||
`recognized by appium: ${extraCaps.join(', ')}.`);
|
||||
}
|
||||
}
|
||||
|
||||
validateDesiredCaps (caps) {
|
||||
return !!caps;
|
||||
if (!this.shouldValidateCaps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let validationOptions = {fullMessages: false};
|
||||
let validationErrors = validator.validate(caps,
|
||||
this._constraints,
|
||||
validationOptions);
|
||||
|
||||
if (validationErrors) {
|
||||
let message = `The desiredCapabilities object was not valid for the ` +
|
||||
`following reason(s):`;
|
||||
for (let [attribute, reasons] of _.pairs(validationErrors)) {
|
||||
for (let reason of reasons) {
|
||||
message += ` ${attribute} ${reason},`;
|
||||
}
|
||||
}
|
||||
message = message.slice(0, -1) + '.';
|
||||
log.error(message);
|
||||
throw new errors.SessionNotCreatedError(message);
|
||||
}
|
||||
|
||||
this.logExtraCaps(caps);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is the main command handler for the driver. It wraps command
|
||||
@@ -55,7 +122,18 @@ class BaseDriver extends MobileJsonWireProtocol {
|
||||
// server, it gets tacked on to the end of nextCommand. Thus we create
|
||||
// a chain of promises that acts as a queue with single concurrency.
|
||||
let nextCommand = this.curCommand.then(() => {
|
||||
return this[cmd](...args);
|
||||
// if we unexpectedly shut down, we need to reject every command in
|
||||
// the queue before we actually try to run it
|
||||
if (this.shutdownUnexpectedly) {
|
||||
return B.reject(new Error("The driver shut down unexpectedly"));
|
||||
}
|
||||
// We also need to turn the command into a cancellable promise so if we
|
||||
// have an unexpected shutdown event, for example, we can cancel it from
|
||||
// outside, rejecting the current command immediately
|
||||
this.curCommandCancellable = new B((r) => { r(); }).then(() => {
|
||||
return this[cmd](...args);
|
||||
}).cancellable();
|
||||
return this.curCommandCancellable;
|
||||
});
|
||||
this.curCommand = nextCommand.catch(() => {});
|
||||
let res = await nextCommand;
|
||||
@@ -66,7 +144,7 @@ class BaseDriver extends MobileJsonWireProtocol {
|
||||
// automatic session deletion in this.onCommandTimeout. Of course we don't
|
||||
// want to trigger the timer when the user is shutting down the session
|
||||
// intentionally
|
||||
if (this.newCommandTimeoutMs !== null && cmd !== 'deleteSession') {
|
||||
if (this.newCommandTimeoutMs && cmd !== 'deleteSession') {
|
||||
this.noCommandTimer = setTimeout(this.onCommandTimeout.bind(this),
|
||||
this.newCommandTimeoutMs);
|
||||
}
|
||||
@@ -80,6 +158,17 @@ class BaseDriver extends MobileJsonWireProtocol {
|
||||
await this.deleteSession();
|
||||
}
|
||||
|
||||
async startUnexpectedShutdown (err = null) {
|
||||
if (err === null) {
|
||||
err = new Error("The driver shut down unexpectedly");
|
||||
}
|
||||
this.onUnexpectedShutdown.cancel(err); // allow others to listen for this
|
||||
this.curCommandCancellable.cancel(err);
|
||||
this.shutdownUnexpectedly = true;
|
||||
await this.deleteSession();
|
||||
this.shutdownUnexpectedly = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (let [cmd, fn] of _.pairs(commands)) {
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import logger from './logger';
|
||||
import _fs from 'fs';
|
||||
import B from 'bluebird';
|
||||
import { tempDir, system, fs } from 'appium-support';
|
||||
import { exec } from 'teen_process';
|
||||
import AdmZip from 'adm-zip';
|
||||
import request from 'request-promise';
|
||||
|
||||
const ZIP_EXTS = ['.zip', '.ipa'];
|
||||
|
||||
async function configureApp (app, appExt) {
|
||||
let newApp = null;
|
||||
let shouldUnzipApp = _.contains(ZIP_EXTS, path.extname(app));
|
||||
if (app.substring(0, 4).toLowerCase() === "http") {
|
||||
logger.info(`Using downloadable app ${app}`);
|
||||
newApp = await downloadApp(app, shouldUnzipApp ? ".zip" : appExt);
|
||||
logger.info(`Downloaded app to ${newApp}`);
|
||||
} else {
|
||||
logger.info(`Using local app ${app}`);
|
||||
newApp = app;
|
||||
if (shouldUnzipApp) {
|
||||
newApp = await copyLocalZip(app);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUnzipApp) {
|
||||
newApp = await unzipApp(newApp, appExt);
|
||||
logger.info(`Unzipped local app to ${newApp}`);
|
||||
}
|
||||
|
||||
if (path.extname(newApp) !== appExt) {
|
||||
throw new Error(`New app path ${newApp} did not have extension ${appExt}`);
|
||||
}
|
||||
|
||||
return newApp;
|
||||
}
|
||||
|
||||
async function downloadApp (app, appExt) {
|
||||
let appUrl;
|
||||
try {
|
||||
appUrl = url.parse(app);
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid App URL (${app})`);
|
||||
}
|
||||
|
||||
let appPath;
|
||||
try {
|
||||
appPath = await downloadFile(url.format(appUrl), appExt);
|
||||
} catch (err) {
|
||||
throw new Error(`Problem downloading app from url ${app}: ${err}`);
|
||||
}
|
||||
|
||||
return appPath;
|
||||
}
|
||||
|
||||
async function downloadFile (fileUrl, suffix) {
|
||||
// We will be downloading the files to a directory, so make sure it's there
|
||||
// This step is not required if you have manually created the directory
|
||||
let fileInfo = await tempDir.open({prefix: 'appium-app', suffix: suffix});
|
||||
_fs.close(fileInfo.fd);
|
||||
await fs.writeFile(fileInfo.path, await request(fileUrl));
|
||||
logger.debug(`${fileUrl} downloaded to ${fileInfo.path}`);
|
||||
}
|
||||
|
||||
async function copyLocalZip (localZipPath) {
|
||||
logger.debug("Copying local zip to tmp dir");
|
||||
if (!(await fs.exists(localZipPath))) {
|
||||
throw new Error("Local zip did not exist");
|
||||
}
|
||||
let fileInfo = await tempDir.open({prefix: 'appium-app', suffix: '.zip'});
|
||||
let infile = _fs.createReadStream(localZipPath);
|
||||
let outfile = _fs.createWriteStream(fileInfo.path);
|
||||
return new B((resolve, reject) => {
|
||||
infile.pipe(outfile).on('close', () => {
|
||||
resolve(fileInfo.path);
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function unzipApp (zipPath, appExt) {
|
||||
// first delete any existing apps that might be in our tmp dir
|
||||
let {stdout} = await exec('find', [path.dirname(zipPath), '-type', 'd',
|
||||
'-name', `*${appExt}`]);
|
||||
for (let line of stdout.trim().split("\n").filter(Boolean)) {
|
||||
await fs.rimraf(line);
|
||||
}
|
||||
// now delete any existing zip payload
|
||||
await fs.rimraf(path.resolve(path.dirname(zipPath), '/Payload*'));
|
||||
let output = await unzipFile(zipPath);
|
||||
let relaxedRegStr = `(?:creating|inflating|extracting): (.+${appExt})/?`;
|
||||
// in the strict regex, we check for an entry which ends with the
|
||||
// extension
|
||||
let strictReg = new RegExp(relaxedRegStr + "$", 'm');
|
||||
// otherwise, we allow an entry which contains the extension, but we
|
||||
// need to be careful, because it might be a false positive
|
||||
let relaxedReg = new RegExp(relaxedRegStr, 'm');
|
||||
let strictMatch = strictReg.exec(output);
|
||||
let relaxedMatch = relaxedReg.exec(output);
|
||||
let getAppPath = function (match) {
|
||||
return path.resolve(path.dirname(zipPath), match[1]);
|
||||
};
|
||||
|
||||
if (strictMatch) {
|
||||
return getAppPath(strictMatch);
|
||||
}
|
||||
|
||||
if (relaxedMatch) {
|
||||
logger.debug("Got a relaxed match for app in zip, be careful for app match errors");
|
||||
return getAppPath(relaxedMatch);
|
||||
}
|
||||
|
||||
throw new Error("App zip unzipped OK, but we couldn't find a .app bundle " +
|
||||
"in it. Make sure your archive contains the .app package " +
|
||||
"and nothing else");
|
||||
}
|
||||
|
||||
async function unzipFile (zipPath) {
|
||||
logger.debug(`Unzipping ${zipPath}`);
|
||||
let valid = await testZipArchive(zipPath);
|
||||
if (!valid) {
|
||||
throw new Error(`Zip archive ${zipPath} did not test valid`);
|
||||
}
|
||||
|
||||
if (system.isWindows()) {
|
||||
let zip = new AdmZip(zipPath);
|
||||
zip.extractAllTo(path.dirname(zipPath), true);
|
||||
logger.debug("Unzip successful");
|
||||
return;
|
||||
}
|
||||
|
||||
let execEnv = _.clone(process.env);
|
||||
delete execEnv.UNZIP;
|
||||
let execOpts = {cwd: path.dirname(zipPath), env: execEnv};
|
||||
try {
|
||||
let {stdout} = await exec('unzip', ['-o', zipPath], execOpts);
|
||||
return stdout;
|
||||
} catch (err) {
|
||||
logger.error(`Unzip threw error ${err}`);
|
||||
logger.error(`Stderr: ${err.stderr}`);
|
||||
logger.error(`Stdout: ${err.stdout}`);
|
||||
throw new Error("Archive could not be unzipped, check appium logs.");
|
||||
}
|
||||
}
|
||||
|
||||
async function testZipArchive (zipPath) {
|
||||
logger.debug(`Testing zip archive: ${zipPath}`);
|
||||
if (system.isWindows()) {
|
||||
if (await fs.exists(zipPath)) {
|
||||
logger.debug("Zip archive tested clean");
|
||||
return true;
|
||||
} else {
|
||||
logger.debug("Zip archive not found");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let execEnv = _.clone(process.env);
|
||||
delete execEnv.UNZIP;
|
||||
let execOpts = {cwd: path.dirname(zipPath), env: execEnv};
|
||||
let output;
|
||||
try {
|
||||
output = await exec('unzip', ['-tq', zipPath], execOpts);
|
||||
if (/No errors detected/.exec(output.stdout)) {
|
||||
return true;
|
||||
}
|
||||
logger.error(`Zip file ${zipPath} was not valid`);
|
||||
logger.error(`Stderr: ${output.stderr}`);
|
||||
logger.error(`Stdout: ${output.stdout}`);
|
||||
logger.error("Zip archive did not test successfully, check appium server " +
|
||||
"logs for output");
|
||||
return false;
|
||||
} catch (err) {
|
||||
logger.error(`Test zip archive threw error ${err}`);
|
||||
logger.error(`Stderr: ${err.stderr}`);
|
||||
logger.error(`Stdout: ${err.stdout}`);
|
||||
throw new Error("Error testing zip archive, are you sure this is a zip file?");
|
||||
}
|
||||
}
|
||||
|
||||
export default {configureApp, downloadApp, downloadFile, copyLocalZip,
|
||||
unzipApp, unzipFile, testZipArchive};
|
||||
+12
-8
@@ -4,9 +4,9 @@
|
||||
"keywords": [
|
||||
"appium"
|
||||
],
|
||||
"version": "0.1.3",
|
||||
"version": "0.3.0",
|
||||
"author": "appium",
|
||||
"license": "Apache License 2.0",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium-base-driver.git"
|
||||
@@ -23,14 +23,18 @@
|
||||
"lib": "lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.4.7",
|
||||
"appium-logger": "^1.1.0",
|
||||
"babel-core": "^5.4.3",
|
||||
"babel-runtime": "^5.3.3",
|
||||
"appium-support": "^2.0.0-beta8",
|
||||
"babel-runtime": "5.5.5",
|
||||
"bluebird": "^2.9.25",
|
||||
"lodash": "^3.9.1",
|
||||
"mobile-json-wire-protocol": "^0.7.0",
|
||||
"mobile-json-wire-protocol": "^0.12.4",
|
||||
"request-promise": "^0.4.2",
|
||||
"source-map-support": "^0.2.10",
|
||||
"uuid-js": "^0.7.5"
|
||||
"teen_process": "^1.2.2",
|
||||
"uuid-js": "^0.7.5",
|
||||
"validate.js": "^0.7.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublish": "./node_modules/.bin/gulp prepublish",
|
||||
@@ -38,13 +42,13 @@
|
||||
"watch": "./node_modules/.bin/gulp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"appium-express": "0.0.1",
|
||||
"appium-express": "^0.0.4",
|
||||
"appium-gulp-plugins": "^1.1.1",
|
||||
"chai": "^2.3.0",
|
||||
"chai-as-promised": "^5.0.0",
|
||||
"gulp": "^3.8.11",
|
||||
"mochawait": "^2.0.0",
|
||||
"request-promise": "^0.4.2",
|
||||
"sinon": "^1.14.1",
|
||||
"yargs": "^3.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { BaseDriver } from '..';
|
||||
import { errors } from 'mobile-json-wire-protocol';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import 'mochawait';
|
||||
import logger from '../lib/logger.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const should = chai.should();
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
describe('Desired Capabilities', () => {
|
||||
|
||||
let d;
|
||||
|
||||
beforeEach(() => {
|
||||
d = new BaseDriver();
|
||||
sinon.spy(logger, 'warn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logger.warn.restore();
|
||||
});
|
||||
|
||||
it('should require platformName and deviceName', async () => {
|
||||
try {
|
||||
await d.createSession({});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('platformName');
|
||||
e.message.should.contain('deviceName');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it('should require platformName', async () => {
|
||||
try {
|
||||
await d.createSession({'platformName': 'iOS'});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('deviceName');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it('should require deviceName', async () => {
|
||||
try {
|
||||
await d.createSession({'deviceName': 'Delorean'});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('platformName');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it('should not care about cap order', async () => {
|
||||
|
||||
await d.createSession({
|
||||
deviceName: 'Delorean',
|
||||
platformName: 'iOS'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should check required caps which are added to driver', async () => {
|
||||
d.desiredCapConstraints = {
|
||||
necessary: {
|
||||
presence: true
|
||||
},
|
||||
proper: {
|
||||
presence: true,
|
||||
isString: true,
|
||||
inclusion: ['Delorean', 'Reventon']
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await d.createSession({
|
||||
'platformName': 'iOS',
|
||||
'deviceName': 'Delorean'
|
||||
});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('necessary');
|
||||
e.message.should.contain('proper');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it('should check added required caps in addition to base', async () => {
|
||||
d.desiredCapConstraints = {
|
||||
necessary: {
|
||||
presence: true
|
||||
},
|
||||
proper: {
|
||||
presence: true,
|
||||
isString: true,
|
||||
inclusion: ['Delorean', 'Reventon']
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await d.createSession({
|
||||
necessary: 'yup',
|
||||
proper: 'yup, your highness'
|
||||
});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('platformName');
|
||||
e.message.should.contain('deviceName');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it('should accept extra capabilities', async () => {
|
||||
await d.createSession({
|
||||
'platformName': 'iOS',
|
||||
'deviceName': 'Delorean',
|
||||
'extra': 'cheese',
|
||||
'hold the': 'sauce'
|
||||
});
|
||||
});
|
||||
|
||||
it('should log the use of extra caps', async function () {
|
||||
this.timeout(500);
|
||||
|
||||
await d.createSession({
|
||||
'platformName': 'iOS',
|
||||
'deviceName': 'Delorean',
|
||||
'extra': 'cheese',
|
||||
'hold the': 'sauce'
|
||||
});
|
||||
|
||||
logger.warn.callCount.should.be.above(0);
|
||||
});
|
||||
|
||||
it('should be sensitive to the case of caps', async () => {
|
||||
try {
|
||||
await d.createSession({
|
||||
'platformname': 'iOS',
|
||||
'deviceName': 'Delorean'
|
||||
});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('platformName');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it ('should error if objects in caps', async function () {
|
||||
try {
|
||||
await d.createSession({
|
||||
'platformName': {a: 'iOS'},
|
||||
'deviceName': 'Delorean'
|
||||
});
|
||||
} catch (e) {
|
||||
e.should.be.instanceof(errors.SessionNotCreatedError);
|
||||
e.message.should.contain('platformName');
|
||||
return;
|
||||
}
|
||||
|
||||
should.fail('error should have been thrown');
|
||||
});
|
||||
|
||||
it('should check for deprecated caps', async function () {
|
||||
this.timeout(500);
|
||||
|
||||
d.desiredCapConstraints = {
|
||||
'lynx-version': {
|
||||
deprecated: true
|
||||
}
|
||||
};
|
||||
|
||||
await d.createSession({
|
||||
'platformName': 'iOS',
|
||||
'deviceName': 'Delorean',
|
||||
'lynx-version': 5
|
||||
});
|
||||
|
||||
logger.warn.callCount.should.be.above(0);
|
||||
});
|
||||
|
||||
it('should not warn if deprecated=false', async function () {
|
||||
this.timeout(500);
|
||||
|
||||
d.desiredCapConstraints = {
|
||||
'lynx-version': {
|
||||
deprecated: false
|
||||
}
|
||||
};
|
||||
|
||||
await d.createSession({
|
||||
'platformName': 'iOS',
|
||||
'deviceName': 'Delorean',
|
||||
'lynx-version': 5
|
||||
});
|
||||
|
||||
logger.warn.callCount.should.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
// transpile:mocha
|
||||
|
||||
import { BaseDriver } from '../lib/driver';
|
||||
import baseDriverE2ETests from './driver-e2e-tests';
|
||||
baseDriverE2ETests(BaseDriver, {
|
||||
platformName: 'iOS',
|
||||
deviceName: 'Delorean'
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import _ from 'lodash';
|
||||
import { server } from 'appium-express';
|
||||
import { routeConfiguringFunction } from 'mobile-json-wire-protocol';
|
||||
import request from 'request-promise';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import 'mochawait';
|
||||
import B from 'bluebird';
|
||||
import DeviceSettings from '../lib/device-settings';
|
||||
|
||||
const should = chai.should();
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
function baseDriverE2ETests (DriverClass, defaultCaps = {}) {
|
||||
describe('BaseDriver (e2e)', () => {
|
||||
let baseServer, d = new DriverClass();
|
||||
before(async () => {
|
||||
baseServer = await server(routeConfiguringFunction(d), 8181);
|
||||
});
|
||||
after(() => {
|
||||
baseServer.close();
|
||||
});
|
||||
|
||||
describe('session handling', () => {
|
||||
it('should create session and retrieve a session id, then delete it', async () => {
|
||||
let res = await request({
|
||||
url: 'http://localhost:8181/wd/hub/session',
|
||||
method: 'POST',
|
||||
json: {desiredCapabilities: defaultCaps, requiredCapabilities: {}},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
|
||||
res.statusCode.should.equal(200);
|
||||
res.body.status.should.equal(0);
|
||||
should.exist(res.body.sessionId);
|
||||
res.body.value.should.eql(defaultCaps);
|
||||
|
||||
res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'DELETE',
|
||||
json: true,
|
||||
simple: false,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
|
||||
res.statusCode.should.equal(200);
|
||||
res.body.status.should.equal(0);
|
||||
should.equal(d.sessionId, null);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should throw NYI for commands not implemented', async () => {
|
||||
});
|
||||
|
||||
describe('command timeouts', () => {
|
||||
function startSession (timeout) {
|
||||
let caps = _.clone(defaultCaps);
|
||||
caps.newCommandTimeout = timeout;
|
||||
return request({
|
||||
url: 'http://localhost:8181/wd/hub/session',
|
||||
method: 'POST',
|
||||
json: {desiredCapabilities: caps, requiredCapabilities: {}},
|
||||
});
|
||||
}
|
||||
|
||||
function endSession (id) {
|
||||
return request({
|
||||
url: `http://localhost:8181/wd/hub/session/${id}`,
|
||||
method: 'DELETE',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
}
|
||||
|
||||
d.findElement = function () {
|
||||
return 'foo';
|
||||
}.bind(d);
|
||||
|
||||
d.findElements = async function () {
|
||||
await B.delay(200);
|
||||
return ['foo'];
|
||||
}.bind(d);
|
||||
|
||||
it('should set a default commandTimeout', async () => {
|
||||
let newSession = await startSession();
|
||||
d.newCommandTimeoutMs.should.be.above(0);
|
||||
await endSession(newSession.sessionId);
|
||||
});
|
||||
|
||||
it('should timeout on commands using commandTimeout cap', async () => {
|
||||
let newSession = await startSession(0.25);
|
||||
|
||||
await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/element`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
await B.delay(400);
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'GET',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
res.status.should.equal(6);
|
||||
should.equal(d.sessionId, null);
|
||||
res = await endSession(newSession.sessionId);
|
||||
res.status.should.equal(6);
|
||||
});
|
||||
|
||||
it('should not timeout with commandTimeout of false', async () => {
|
||||
let newSession = await startSession(0.1);
|
||||
let start = Date.now();
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/elements`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
(Date.now() - start).should.be.above(150);
|
||||
res.value.should.eql(['foo']);
|
||||
await endSession(newSession.sessionId);
|
||||
});
|
||||
|
||||
it('should not timeout with commandTimeout of 0', async () => {
|
||||
d.newCommandTimeoutMs = 2;
|
||||
let newSession = await startSession(0);
|
||||
|
||||
await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/element`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
await B.delay(400);
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'GET',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
res.status.should.equal(0);
|
||||
res = await endSession(newSession.sessionId);
|
||||
res.status.should.equal(0);
|
||||
|
||||
d.newCommandTimeoutMs = 60 * 1000;
|
||||
});
|
||||
|
||||
it('should not timeout if its just the command taking awhile', async () => {
|
||||
let newSession = await startSession(0.25);
|
||||
await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/element`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
await B.delay(400);
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'GET',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
res.status.should.equal(6);
|
||||
should.equal(d.sessionId, null);
|
||||
res = await endSession(newSession.sessionId);
|
||||
res.status.should.equal(6);
|
||||
});
|
||||
|
||||
it('should not have a timer running before or after a session', async () => {
|
||||
should.not.exist(d.noCommandTimer);
|
||||
let newSession = await startSession(0.25);
|
||||
newSession.sessionId.should.equal(d.sessionId);
|
||||
should.exist(d.noCommandTimer);
|
||||
await endSession(newSession.sessionId);
|
||||
should.not.exist(d.noCommandTimer);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('settings api', () => {
|
||||
before(() => {
|
||||
d.settings = new DeviceSettings({ignoreUnimportantViews: false});
|
||||
});
|
||||
it('should be able to get settings object',() => {
|
||||
d.settings.getSettings().ignoreUnimportantViews.should.be.false;
|
||||
});
|
||||
it('should throw error when updateSettings method is not defined', async () => {
|
||||
await d.settings.update({ignoreUnimportantViews: true}).should.eventually
|
||||
.be.rejectedWith("onSettingsUpdate");
|
||||
});
|
||||
it('should throw error for invalid update object', async () => {
|
||||
await d.settings.update("invalid json").should.eventually
|
||||
.be.rejectedWith("JSON");
|
||||
});
|
||||
});
|
||||
|
||||
describe('unexpected exits', () => {
|
||||
it('should reject a current command when the driver crashes', async () => {
|
||||
d._oldGetStatus = d.getStatus;
|
||||
d.getStatus = async function () {
|
||||
await B.delay(100);
|
||||
}.bind(d);
|
||||
let p = request({
|
||||
url: 'http://localhost:8181/wd/hub/status',
|
||||
method: 'GET',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
// make sure that the request gets to the server before our shutdown
|
||||
await B.delay(20);
|
||||
d.startUnexpectedShutdown(new Error("Crashytimes"));
|
||||
let res = await p;
|
||||
res.status.should.equal(13);
|
||||
res.value.message.should.contain('Crashytimes');
|
||||
await d.onUnexpectedShutdown.should.be.rejectedWith('Crashytimes');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export default baseDriverE2ETests;
|
||||
+5
-282
@@ -1,285 +1,8 @@
|
||||
// transpile:mocha
|
||||
|
||||
import { BaseDriver } from '../..';
|
||||
import { server } from 'appium-express';
|
||||
import { routeConfiguringFunction } from 'mobile-json-wire-protocol';
|
||||
import request from 'request-promise';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import 'mochawait';
|
||||
import B from 'bluebird';
|
||||
|
||||
const should = chai.should();
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
describe('BaseDriver', () => {
|
||||
|
||||
let d;
|
||||
|
||||
beforeEach(() => {
|
||||
d = new BaseDriver();
|
||||
});
|
||||
|
||||
it('should return a sessionId from createSession', async () => {
|
||||
let [sessId] = await d.createSession({});
|
||||
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 () => {
|
||||
await d.createSession({});
|
||||
await d.createSession({}).should.eventually.be.rejectedWith('session');
|
||||
});
|
||||
|
||||
it('should be able to delete a session', async () => {
|
||||
let sessionId1 = await d.createSession({});
|
||||
await d.deleteSession();
|
||||
should.equal(d.sessionId, null);
|
||||
let sessionId2 = await d.createSession({});
|
||||
sessionId1.should.not.eql(sessionId2);
|
||||
});
|
||||
|
||||
it('should get the current session', async () => {
|
||||
let [,caps] = await d.createSession({});
|
||||
caps.should.equal(await d.getSession());
|
||||
});
|
||||
|
||||
it('should return sessions if no session exists', async () => {
|
||||
let sessions = await d.getSessions();
|
||||
sessions.length.should.equal(0);
|
||||
});
|
||||
|
||||
it('should return sessions', async () => {
|
||||
await d.createSession({a: 'cap'});
|
||||
let sessions = await d.getSessions();
|
||||
|
||||
sessions.length.should.equal(1);
|
||||
sessions[0].should.eql({
|
||||
id: d.sessionId,
|
||||
capabilities: {a: 'cap'}
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should emit an unexpected end session event', async () => {
|
||||
});
|
||||
|
||||
describe('command queue', () => {
|
||||
let d = new BaseDriver();
|
||||
let waitMs = 10;
|
||||
d.getStatus = async () => {
|
||||
await B.delay(waitMs);
|
||||
return Date.now();
|
||||
}.bind(d);
|
||||
|
||||
d.getSessions = async () => {
|
||||
await B.delay(waitMs);
|
||||
throw new Error("multipass");
|
||||
}.bind(d);
|
||||
|
||||
it('should queue commands and execute/respond in the order received', async () => {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.execute('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 () => {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
if (i === 5) {
|
||||
cmds.push(d.execute('getSessions'));
|
||||
} else {
|
||||
cmds.push(d.execute('getStatus'));
|
||||
}
|
||||
}
|
||||
let results = await B.settle(cmds);
|
||||
for (let i = 1; i < 5; i++) {
|
||||
if (results[i].value() <= results[i - 1].value()) {
|
||||
throw new Error("Got result out of order");
|
||||
}
|
||||
}
|
||||
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 () => {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.execute('getStatus'));
|
||||
}
|
||||
let results = await B.all(cmds);
|
||||
cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.execute('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('BaseDriver via HTTP', () => {
|
||||
let baseServer, d = new BaseDriver();
|
||||
before(async () => {
|
||||
baseServer = await server(routeConfiguringFunction(d), 8181);
|
||||
});
|
||||
after(() => {
|
||||
baseServer.close();
|
||||
});
|
||||
|
||||
describe('session handling', () => {
|
||||
it('should create session and retrieve a session id, then delete it', async () => {
|
||||
let res = await request({
|
||||
url: 'http://localhost:8181/wd/hub/session',
|
||||
method: 'POST',
|
||||
json: {desiredCapabilities: {}, requiredCapabilities: {}},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
|
||||
res.statusCode.should.equal(200);
|
||||
res.body.status.should.equal(0);
|
||||
should.exist(res.body.sessionId);
|
||||
res.body.value.should.eql({});
|
||||
|
||||
res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'DELETE',
|
||||
json: true,
|
||||
simple: false,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
|
||||
res.statusCode.should.equal(200);
|
||||
res.body.status.should.equal(0);
|
||||
should.equal(d.sessionId, null);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should throw NYI for commands not implemented', async () => {
|
||||
});
|
||||
|
||||
describe('command timeouts', () => {
|
||||
function startSession (timeout) {
|
||||
let caps = {newCommandTimeout: timeout};
|
||||
return request({
|
||||
url: 'http://localhost:8181/wd/hub/session',
|
||||
method: 'POST',
|
||||
json: {desiredCapabilities: caps, requiredCapabilities: {}},
|
||||
simple: false
|
||||
});
|
||||
}
|
||||
|
||||
function endSession (id) {
|
||||
return request({
|
||||
url: `http://localhost:8181/wd/hub/session/${id}`,
|
||||
method: 'DELETE',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
}
|
||||
|
||||
d.findElement = function () {
|
||||
return 'foo';
|
||||
}.bind(d);
|
||||
|
||||
d.findElements = async function () {
|
||||
await B.delay(200);
|
||||
return ['foo'];
|
||||
}.bind(d);
|
||||
|
||||
it.skip('should timeout on commands using default commandTimeout', async () => {
|
||||
});
|
||||
|
||||
it('should timeout on commands using commandTimeout cap', async () => {
|
||||
let newSession = await startSession(0.25);
|
||||
await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/element`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
await B.delay(400);
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'GET',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
res.status.should.equal(6);
|
||||
should.equal(d.sessionId, null);
|
||||
res = await endSession(newSession.sessionId);
|
||||
res.status.should.equal(6);
|
||||
});
|
||||
|
||||
it('should not timeout with commandTimeout of false', async () => {
|
||||
let newSession = await startSession(0.1);
|
||||
let start = Date.now();
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/elements`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
(Date.now() - start).should.be.above(150);
|
||||
res.value.should.eql(['foo']);
|
||||
await endSession(newSession.sessionId);
|
||||
});
|
||||
|
||||
it.skip('should not timeout with commandTimeout of 0', async () => {
|
||||
});
|
||||
|
||||
it('should not timeout if its just the command taking awhile', async () => {
|
||||
let newSession = await startSession(0.25);
|
||||
await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}/element`,
|
||||
method: 'POST',
|
||||
json: {using: 'name', value: 'foo'},
|
||||
});
|
||||
await B.delay(400);
|
||||
let res = await request({
|
||||
url: `http://localhost:8181/wd/hub/session/${d.sessionId}`,
|
||||
method: 'GET',
|
||||
json: true,
|
||||
simple: false
|
||||
});
|
||||
res.status.should.equal(6);
|
||||
should.equal(d.sessionId, null);
|
||||
res = await endSession(newSession.sessionId);
|
||||
res.status.should.equal(6);
|
||||
});
|
||||
|
||||
it('should not have a timer running before or after a session', async () => {
|
||||
should.not.exist(d.noCommandTimer);
|
||||
let newSession = await startSession(0.25);
|
||||
newSession.sessionId.should.equal(d.sessionId);
|
||||
should.exist(d.noCommandTimer);
|
||||
await endSession(newSession.sessionId);
|
||||
should.not.exist(d.noCommandTimer);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('settings api', () => {
|
||||
// TODO port over settings tests
|
||||
});
|
||||
|
||||
import { BaseDriver } from '../lib/driver';
|
||||
import baseDriverUnitTests from './driver-tests';
|
||||
baseDriverUnitTests(BaseDriver, {
|
||||
platformName: 'iOS',
|
||||
deviceName: 'Delorean'
|
||||
});
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import _ from 'lodash';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import 'mochawait';
|
||||
import B from 'bluebird';
|
||||
|
||||
const should = chai.should();
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
// wrap these tests in a function so we can export the tests and re-use them
|
||||
// for actual driver implementations
|
||||
function baseDriverUnitTests (DriverClass, defaultCaps = {}) {
|
||||
describe('BaseDriver', () => {
|
||||
|
||||
let d;
|
||||
beforeEach(() => {
|
||||
d = new DriverClass();
|
||||
});
|
||||
|
||||
it('should return a sessionId from createSession', async () => {
|
||||
let [sessId] = await d.createSession(defaultCaps);
|
||||
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 () => {
|
||||
await d.createSession(defaultCaps);
|
||||
await d.createSession(defaultCaps).should.eventually.be.rejectedWith('session');
|
||||
});
|
||||
|
||||
it('should be able to delete a session', async () => {
|
||||
let sessionId1 = await d.createSession(defaultCaps);
|
||||
await d.deleteSession();
|
||||
should.equal(d.sessionId, null);
|
||||
let sessionId2 = await d.createSession(defaultCaps);
|
||||
sessionId1.should.not.eql(sessionId2);
|
||||
});
|
||||
|
||||
it('should get the current session', async () => {
|
||||
let [,caps] = await d.createSession(defaultCaps);
|
||||
caps.should.equal(await d.getSession());
|
||||
});
|
||||
|
||||
it('should return sessions if no session exists', async () => {
|
||||
let sessions = await d.getSessions();
|
||||
sessions.length.should.equal(0);
|
||||
});
|
||||
|
||||
it('should return sessions', async () => {
|
||||
let caps = _.clone(defaultCaps);
|
||||
caps.a = 'cap';
|
||||
await d.createSession(caps);
|
||||
let sessions = await d.getSessions();
|
||||
|
||||
sessions.length.should.equal(1);
|
||||
sessions[0].should.eql({
|
||||
id: d.sessionId,
|
||||
capabilities: caps
|
||||
});
|
||||
});
|
||||
|
||||
it('should fulfill an unexpected driver quit promise', async () => {
|
||||
// make a command that will wait a bit so we can crash while it's running
|
||||
d.getStatus = async function () {
|
||||
await B.delay(100);
|
||||
}.bind(d);
|
||||
let cmdPromise = d.execute('getStatus');
|
||||
await B.delay(0);
|
||||
d.startUnexpectedShutdown(new Error("We crashed"));
|
||||
await cmdPromise.should.be.rejectedWith(/We crashed/);
|
||||
await d.onUnexpectedShutdown.should.be.rejectedWith(/We crashed/);
|
||||
});
|
||||
|
||||
it('should not allow commands in middle of unexpected shutdown', async () => {
|
||||
// make a command that will wait a bit so we can crash while it's running
|
||||
d.oldDeleteSession = d.deleteSession;
|
||||
d.deleteSession = async function () {
|
||||
await B.delay(100);
|
||||
await this.oldDeleteSession();
|
||||
}.bind(d);
|
||||
let caps = _.clone(defaultCaps);
|
||||
await d.createSession(caps);
|
||||
d.startUnexpectedShutdown(new Error("We crashed"));
|
||||
await d.onUnexpectedShutdown.should.be.rejectedWith(/We crashed/);
|
||||
await d.execute('getSession').should.be.rejectedWith(/shut down/);
|
||||
});
|
||||
|
||||
it('should allow new commands after done shutting down', async () => {
|
||||
// make a command that will wait a bit so we can crash while it's running
|
||||
d.oldDeleteSession = d.deleteSession;
|
||||
d.deleteSession = async function () {
|
||||
await B.delay(100);
|
||||
await this.oldDeleteSession();
|
||||
}.bind(d);
|
||||
let caps = _.clone(defaultCaps);
|
||||
await d.createSession(caps);
|
||||
d.startUnexpectedShutdown(new Error("We crashed"));
|
||||
await d.onUnexpectedShutdown.should.be.rejectedWith(/We crashed/);
|
||||
await d.execute('getSession').should.be.rejectedWith(/shut down/);
|
||||
await B.delay(100);
|
||||
await d.execute('createSession', caps);
|
||||
await d.deleteSession();
|
||||
});
|
||||
|
||||
describe('command queue', () => {
|
||||
let d = new DriverClass();
|
||||
|
||||
// disabling timeout for those test because it couses the node process
|
||||
// to hang at exit, (because the timeout is still alive).
|
||||
// TODO: there is some bit of logic to be implemented so that commands
|
||||
// like 'getStatus' do not trigger the command timeout.
|
||||
d.newCommandTimeoutMs = null;
|
||||
|
||||
let waitMs = 10;
|
||||
d.getStatus = async () => {
|
||||
await B.delay(waitMs);
|
||||
return Date.now();
|
||||
}.bind(d);
|
||||
|
||||
d.getSessions = async () => {
|
||||
await B.delay(waitMs);
|
||||
throw new Error("multipass");
|
||||
}.bind(d);
|
||||
|
||||
it('should queue commands and execute/respond in the order received', async () => {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.execute('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 () => {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
if (i === 5) {
|
||||
cmds.push(d.execute('getSessions'));
|
||||
} else {
|
||||
cmds.push(d.execute('getStatus'));
|
||||
}
|
||||
}
|
||||
let results = await B.settle(cmds);
|
||||
for (let i = 1; i < 5; i++) {
|
||||
if (results[i].value() <= results[i - 1].value()) {
|
||||
throw new Error("Got result out of order");
|
||||
}
|
||||
}
|
||||
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 () => {
|
||||
let numCmds = 10;
|
||||
let cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.execute('getStatus'));
|
||||
}
|
||||
let results = await B.all(cmds);
|
||||
cmds = [];
|
||||
for (let i = 0; i < numCmds; i++) {
|
||||
cmds.push(d.execute('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");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default baseDriverUnitTests;
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
this is not really an app
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
import chai from 'chai';
|
||||
import path from 'path';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import 'mochawait';
|
||||
import { fs } from 'appium-support';
|
||||
import h from '../lib/helpers';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
function getFixture (file) {
|
||||
return path.resolve(__dirname, '..', '..', 'test', 'fixtures', file);
|
||||
}
|
||||
|
||||
describe('app download and configuration', () => {
|
||||
describe('configureApp', () => {
|
||||
it('should get the path for a local .app', async () => {
|
||||
let newAppPath = await h.configureApp(getFixture('FakeIOSApp.app'), '.app');
|
||||
newAppPath.should.contain('FakeIOSApp.app');
|
||||
let contents = await fs.readFile(newAppPath, 'utf8');
|
||||
contents.should.eql('this is not really an app\n');
|
||||
});
|
||||
it('should get the path for a local .apk', async () => {
|
||||
});
|
||||
it('should unzip and get the path for a local .app.zip', async () => {
|
||||
let newAppPath = await h.configureApp(getFixture('FakeIOSApp.app.zip'), '.app');
|
||||
newAppPath.should.contain('FakeIOSApp.app');
|
||||
let contents = await fs.readFile(newAppPath, 'utf8');
|
||||
contents.should.eql('this is not really an app\n');
|
||||
});
|
||||
it('should unzip and get the path for a local .ipa', async () => {
|
||||
let newAppPath = await h.configureApp(getFixture('FakeIOSApp.ipa'), '.app');
|
||||
newAppPath.should.contain('FakeIOSApp.app');
|
||||
let contents = await fs.readFile(newAppPath, 'utf8');
|
||||
contents.should.eql('this is not really an app\n');
|
||||
});
|
||||
it('should download an app from the web', async () => {
|
||||
// TODO just spin up a local webserver to serve fixtures from, that way
|
||||
// there's no dependency on the internet
|
||||
});
|
||||
// TODO test failure cases like:
|
||||
// - bad zip
|
||||
// - non-matching extension
|
||||
// - zip didn't contain app with extension
|
||||
// - could not download
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
// transpile:main
|
||||
|
||||
import baseDriverUnitTests from './driver-tests';
|
||||
import baseDriverE2ETests from './driver-e2e-tests';
|
||||
|
||||
export { baseDriverUnitTests, baseDriverE2ETests };
|
||||
Reference in New Issue
Block a user