Compare commits

...
65 Commits
Author SHA1 Message Date
Jonathan Lipps 33372d33a3 0.3.0 2015-08-17 18:37:06 -07:00
Jonathan Lipps 536d218b28 Merge pull request #6 from moizjv/master
Adding new device-settings
2015-08-17 18:34:52 -07:00
moizjv 5b3e0ef9c5 Adding new device-settings 2015-08-17 18:32:40 -07:00
Jonathan Lipps fd78e3fd9c make sure that validation errors are helpfully logged 2015-08-12 11:15:49 -07:00
Jonah 1ca0708b3a Update README.md 2015-07-29 14:00:33 -07:00
Jonah f50e41d8f9 Update README.md 2015-07-29 14:00:19 -07:00
Jonah d3fd0a20f0 Update README.md 2015-07-29 13:59:50 -07:00
Jonah 9a1c0d47ac Update README.md 2015-07-29 13:47:06 -07:00
Jonah cddfc22f9d Update README.md
more explanation. doc for createSession
2015-07-29 11:53:02 -07:00
Jonah 08d555d451 Update README.md
desiredCapConstraints docs
2015-07-29 11:47:37 -07:00
Jonathan Lipps 5ec20cfcf0 should probably clone instead of assign 2015-07-29 11:35:39 -07:00
Jonah f59f1816f5 Update README.md
formatting
2015-07-29 11:32:40 -07:00
Jonah 5ca4881797 Update README.md
typo
2015-07-29 11:32:03 -07:00
Jonah Stiennon 23777f1c2f being readme 2015-07-29 11:30:19 -07:00
Jonathan Lipps 80f182e250 0.2.1 2015-07-28 16:21:36 -07:00
Jonathan Lipps 93608a3d89 add initial functional tests and scope out a bit of further test work for configureApp and friends 2015-07-28 16:21:15 -07:00
Jonathan Lipps b8e3012507 add a bunch of file download and unzip helpers that all the drivers will need 2015-07-24 17:56:36 -07:00
Jonathan Lipps becfcc5120 0.2.0 2015-07-13 20:21:25 +08:00
Jonathan Lipps 10e324d8c9 add functionality for handling and triggering unexpected driver shutdowns 2015-07-13 20:21:06 +08:00
Jonathan Lipps bb4ae11648 0.1.19 2015-06-19 16:25:36 -07:00
Jonathan Lipps 19f51a843e use correct paths 2015-06-19 16:25:00 -07:00
Jonathan Lipps c4a3fa48b8 0.1.18 2015-06-19 10:20:18 -07:00
Jonathan Lipps 6b5ecaf796 use better appium-express 2015-06-19 10:20:10 -07:00
Jonathan Lipps 2041727ee5 export tests from tests/index.js, not /index.js 2015-06-19 10:17:53 -07:00
Jonathan Lipps e827755f03 0.1.17 2015-06-11 15:15:56 -07:00
Jonathan Lipps 9d46e32d1e allow fake platform 2015-06-11 15:15:46 -07:00
Jonathan Lipps ebe99f872d 0.1.16 2015-06-10 17:43:25 -07:00
Jonathan Lipps 130ca48247 update appium-express because devdep doesn't auto patch upgrade 2015-06-10 17:41:23 -07:00
Jonathan Lipps 206e4bc2c8 0.1.15 2015-06-10 16:33:03 -07:00
Jonathan Lipps 84d59b2471 don't fix appium-express ver 2015-06-10 16:32:55 -07:00
Jonathan Lipps 81199c4868 0.1.14 2015-06-10 16:25:15 -07:00
Jonathan Lipps e55e6fa955 fix babel version 2015-06-10 16:23:56 -07:00
seb vincent 0458c61a9b Update README.md 2015-06-05 18:08:29 +08:00
sebv 4f29ecfa43 adds coveralls badge 2015-06-05 17:17:19 +08:00
sebv 1b842562e6 fixes licence 2015-06-05 17:16:27 +08:00
sebv 9abacc61be using defaults in gulpfile 2015-06-05 17:11:57 +08:00
sebv 70b757d71a test fix 2015-06-05 17:07:15 +08:00
sebv 7f39f01a7e configuring coveralls 2015-06-05 16:14:37 +08:00
sebv cda3ee28ac travis configuration 2015-06-05 15:21:48 +08:00
sebv c17772247b coverage fixes 2015-06-05 10:43:46 +08:00
sebv 30e1ffaad6 coverage fixes 2015-06-05 03:13:28 +08:00
sebv 5f3c53f2fb configuring coveralls 2015-06-05 02:31:40 +08:00
Jonah 53b0416add Merge pull request #3 from Jonahss/squashed
add desired cap validation
2015-06-03 12:24:50 -07:00
Jonah Stiennon b305420e01 add desired cap validation 2015-06-03 12:17:39 -07:00
Jonathan Lipps 6b80428202 0.1.13 2015-06-03 12:34:55 +01:00
Jonathan Lipps b765981fe5 add details to session not created error 2015-06-03 12:34:37 +01:00
Jonathan Lipps bff678ab06 0.1.12 2015-05-29 17:59:52 -07:00
Jonathan Lipps dd8e1c29d5 since we export tests, request-promise is actually a regular dep 2015-05-29 17:59:34 -07:00
Jonathan Lipps 4f4098efa9 0.1.11 2015-05-29 17:28:04 -07:00
Jonathan Lipps e60f3f4e89 remove extraneous redefinition of capabilites in tests 2015-05-29 17:28:00 -07:00
Jonathan Lipps a2c371e3a7 0.1.10 2015-05-29 17:23:38 -07:00
Jonathan Lipps 12052bec3d make sure to fail if startSession doesn't work in tests 2015-05-29 17:23:32 -07:00
Jonathan Lipps 32c86894c8 0.1.9 2015-05-29 17:15:47 -07:00
Jonathan Lipps 574638b5b9 also parameterize caps 2015-05-29 17:15:34 -07:00
Jonathan Lipps e8c2ee6e6a 0.1.8 2015-05-29 15:04:28 -07:00
Jonathan Lipps ab6d456a54 parameterize driver tests so they can run against any driver class 2015-05-29 15:04:19 -07:00
Jonathan Lipps d0b8cb4cfe 0.1.7 2015-05-29 14:56:31 -07:00
Jonathan Lipps 46755b23ab export from correct files 2015-05-29 14:56:23 -07:00
Jonathan Lipps 962c12b287 0.1.6 2015-05-29 14:54:51 -07:00
Jonathan Lipps 2e57ce2d52 use better test exporting architecture 2015-05-29 14:54:42 -07:00
Jonathan Lipps 7772fe379b 0.1.5 2015-05-29 14:41:08 -07:00
Jonathan Lipps 3f63e8505c export tests for use in other packages 2015-05-29 14:40:57 -07:00
Jonathan Lipps 81bd9d3a56 0.1.4 2015-05-28 14:22:09 -07:00
Jonathan Lipps 587282409d update mjsonwp with error checking fix 2015-05-28 14:21:59 -07:00
Jonathan Lipps 19c72b51c6 upgrade mjsonwp 2015-05-28 13:54:56 -07:00
23 changed files with 1197 additions and 310 deletions
+2
View File
@@ -1,3 +1,5 @@
node_modules
build
*.log
.DS_Store
coverage
+2
View File
@@ -1,3 +1,5 @@
language: node_js
node_js:
- "0.12"
after_success:
- gulp coveralls
+40 -4
View File
@@ -1,12 +1,48 @@
appium-base-driver
===================
[![NPM version](http://img.shields.io/npm/v/appium-base-driver.svg)](https://npmjs.org/package/appium-base-driver)
[![Downloads](http://img.shields.io/npm/dm/appium-base-driver.svg)](https://npmjs.org/package/appium-base-driver)
[![Dependency Status](https://david-dm.org/appium/appium-base-driver.svg)](https://david-dm.org/appium/appium-base-driver)
[![devDependency Status](https://david-dm.org/appium/appium-base-driver/dev-status.svg)](https://david-dm.org/appium/appium-base-driver#info=devDependencies)
Work in progress, stay tuned!
[![Build Status](https://travis-ci.org/appium/appium-base-driver.svg?branch=master)](https://travis-ci.org/appium/appium-base-driver)
[![Coverage Status](https://coveralls.io/repos/appium/appium-base-driver/badge.svg?branch=master)](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?
[![Build
Status](https://travis-ci.org/appium/appium-base-driver.svg?branch=master)](https://travis-ci.org/appium/appium-base-driver)
## Watch
+1 -1
View File
@@ -6,5 +6,5 @@ var gulp = require('gulp'),
boilerplate({
build: 'appium-base-driver',
jscs: false,
testReporter: process.env.TRAVIS ? 'spec' : 'nyan'
watchE2E: true
});
+2 -1
View File
@@ -1,5 +1,6 @@
// transpile:main
import { BaseDriver } from './lib/driver';
export { BaseDriver };
import DeviceSettings from './lib/device-settings';
export { BaseDriver, DeviceSettings };
+3 -1
View File
@@ -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
View File
@@ -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}`);
+19
View File
@@ -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;
+103
View File
@@ -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 };
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
+212
View File
@@ -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);
});
});
+8
View File
@@ -0,0 +1,8 @@
// transpile:mocha
import { BaseDriver } from '../lib/driver';
import baseDriverE2ETests from './driver-e2e-tests';
baseDriverE2ETests(BaseDriver, {
platformName: 'iOS',
deviceName: 'Delorean'
});
+221
View File
@@ -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
View File
@@ -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'
});
+186
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
this is not really an app
Binary file not shown.
BIN
View File
Binary file not shown.
+46
View File
@@ -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
});
});
+6
View File
@@ -0,0 +1,6 @@
// transpile:main
import baseDriverUnitTests from './driver-tests';
import baseDriverE2ETests from './driver-e2e-tests';
export { baseDriverUnitTests, baseDriverE2ETests };