mirror of
https://github.com/appium/appium.git
synced 2026-05-04 01:11:11 -05:00
bring back @jonahss's settings api changes
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
## Settings
|
||||
|
||||
Settings are a new concept introduced by appium. They are currently not a part of the Mobile JSON Wire Protocol, or the Webdriver spec.
|
||||
|
||||
Settings are a way to specify the behavior of the appium server.
|
||||
|
||||
Settings are:
|
||||
- Mutable, they can be changed during a session
|
||||
- Only relevant during the session they are applied. They are reset for each new session.
|
||||
- Control the way the appium server behaves during test automation. They do not apply to controlling the app or device under test.
|
||||
|
||||
An example of a setting would be `ignoreUnimportantViews` for Android. Android can be set to ignore elements in the View Hierarchy which it deems irrelevant. Setting this can cause tests to run faster. A user who *wants* to access the ignored elements however, would want to disable `ignoreUnimportantViews`, and reenable it afterwards.
|
||||
|
||||
Another example of a use-case for settings would be telling appium to ignore elements which are not visible.
|
||||
|
||||
Settings are implemented via the following API endpoints:
|
||||
|
||||
**POST** /session/:sessionId/appium/settings
|
||||
|
||||
>Expects a JSON hash of settings, where keys correspond to setting names, and values to the value of the setting.
|
||||
```
|
||||
{
|
||||
settings: {
|
||||
ignoreUnimportantViews : true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**GET** /session/:sessionId/appium/settings
|
||||
|
||||
>Returns a JSON hash of all the currently specified settings.
|
||||
```
|
||||
{
|
||||
settings: {
|
||||
ignoreUnimportantViews : true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Settings
|
||||
|
||||
**"ignoreUnimportantViews"** - Boolean which sets whether Android devices should use `setCompressedLayoutHeirarchy()` which ignores all views which are marked IMPORTANT_FOR_ACCESSIBILITY_NO or IMPORTANT_FOR_ACCESSIBILITY_AUTO (and have been deemed not important by the system), in an attempt to make things less confusing or faster.
|
||||
@@ -53,6 +53,7 @@
|
||||
|`unicodeKeyboard`| Enable Unicode input, default `false`| `true` or `false`|
|
||||
|`resetKeyboard`| Reset keyboard to its original state, after running Unicode tests with `unicodeKeyboard` capability. Ignored if used alone. Default `false`| `true` or `false`|
|
||||
|`noSign`| Skip checking and signing of app with debug keys, will work only with UiAutomator and not with selendroid, default `false`| `true` or `false`|
|
||||
|`ignoreUnimportantViews`| Calls the `setCompressedLayoutHierarchy()` uiautomator function. This capability can speed up test execution, since Accessibility commands will run faster ignoring some elements. The ignored elements will not be findable, which is why this capability has also been implemented as a toggle-able *setting* as well as a capability. Defaults to `false` | `true` or `false`
|
||||
|
||||
### iOS Only
|
||||
|
||||
|
||||
@@ -71,6 +71,15 @@ Android.prototype.init = function () {
|
||||
, ['POST', new RegExp('^/wd/hub/session/[^/]+/appium')]
|
||||
, ['GET', new RegExp('^/wd/hub/session/[^/]+/appium')]
|
||||
];
|
||||
|
||||
// listen for changes to ignoreUnimportantViews
|
||||
this.settings.on("update", function (update) {
|
||||
if (update.key === "ignoreUnimportantViews") {
|
||||
this.setCompressedLayoutHierarchy(update.value, update.callback);
|
||||
} else {
|
||||
update.callback();
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Android.prototype._deviceConfigure = Device.prototype.configure;
|
||||
@@ -113,6 +122,7 @@ Android.prototype.start = function (cb, onDie) {
|
||||
this.wakeUp.bind(this),
|
||||
this.unlockScreen.bind(this),
|
||||
this.getDataDir.bind(this),
|
||||
this.setupCompressedLayoutHierarchy.bind(this),
|
||||
this.startApp.bind(this),
|
||||
this.initAutoWebview.bind(this)
|
||||
], function (err) {
|
||||
@@ -159,6 +169,7 @@ Android.prototype.restartUiautomator = function (cb) {
|
||||
async.series([
|
||||
this.forwardPort.bind(this)
|
||||
, this.uiautomator.start.bind(this.uiautomator)
|
||||
, this.setupCompressedLayoutHierarchy.bind(this)
|
||||
], cb);
|
||||
};
|
||||
|
||||
@@ -502,6 +513,26 @@ Android.prototype.getDataDir = function (cb) {
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// Set CompressedLayoutHierarchy on the device based on current settings object
|
||||
Android.prototype.setupCompressedLayoutHierarchy = function (cb) {
|
||||
|
||||
// setup using cap
|
||||
if (_.has(this.args, 'ignoreUnimportantViews')) {
|
||||
// set the setting directly on the internal _settings object, this way we don't trigger an update event
|
||||
this.settings._settings.ignoreUnimportantViews = this.args.ignoreUnimportantViews;
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.getSetting("ignoreUnimportantViews"))) {
|
||||
return cb();
|
||||
}
|
||||
this.setCompressedLayoutHierarchy(this.getSetting("ignoreUnimportantViews"), cb);
|
||||
};
|
||||
|
||||
// Set CompressedLayoutHierarchy on the device
|
||||
Android.prototype.setCompressedLayoutHierarchy = function (compress, cb) {
|
||||
this.proxy(["compressedLayoutHierarchy", {compressLayout: compress}], cb);
|
||||
};
|
||||
|
||||
Android.prototype.waitForActivityToStop = function (cb) {
|
||||
this.adb.waitForNotActivity(this.args.appWaitPackage, this.args.appWaitActivity, cb);
|
||||
};
|
||||
@@ -510,6 +541,8 @@ Android.prototype.waitForActivityToStop = function (cb) {
|
||||
Android.prototype.resetTimeout = deviceCommon.resetTimeout;
|
||||
Android.prototype.waitForCondition = deviceCommon.waitForCondition;
|
||||
Android.prototype.implicitWaitForCondition = deviceCommon.implicitWaitForCondition;
|
||||
Android.prototype.getSettings = deviceCommon.getSettings;
|
||||
Android.prototype.updateSettings = deviceCommon.updateSettings;
|
||||
|
||||
_.extend(Android.prototype, androidController);
|
||||
_.extend(Android.prototype, androidContextController);
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ class AndroidCommandExecutor {
|
||||
map.put("performMultiPointerGesture", new MultiPointerGesture());
|
||||
map.put("openNotification", new OpenNotification());
|
||||
map.put("source", new Source());
|
||||
map.put("compressedLayoutHierarchy", new CompressedLayoutHierarchy());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.appium.android.bootstrap.handler;
|
||||
|
||||
import io.appium.android.bootstrap.AndroidCommand;
|
||||
import io.appium.android.bootstrap.AndroidCommandResult;
|
||||
import io.appium.android.bootstrap.CommandHandler;
|
||||
import io.appium.android.bootstrap.utils.NotImportantViews;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.Hashtable;
|
||||
|
||||
/**
|
||||
* Calls the uiautomator setCompressedLayoutHierarchy() function. If set to true, ignores some views during all Accessibility operations.
|
||||
*/
|
||||
public class CompressedLayoutHierarchy extends CommandHandler {
|
||||
@Override
|
||||
public AndroidCommandResult execute(AndroidCommand command) throws JSONException {
|
||||
|
||||
boolean compressLayout;
|
||||
|
||||
try {
|
||||
final Hashtable<String, Object> params = command.params();
|
||||
compressLayout = (Boolean) params.get("compressLayout");
|
||||
NotImportantViews.discard(compressLayout);
|
||||
} catch (ClassCastException e) {
|
||||
return getErrorResult("must supply a 'compressLayout' boolean parameter");
|
||||
} catch (Exception e) {
|
||||
return getErrorResult("error setting compressLayoutHierarchy " + e.getMessage());
|
||||
}
|
||||
|
||||
return getSuccessResult(compressLayout);
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -8,14 +8,10 @@ public abstract class NotImportantViews {
|
||||
// setCompressedLayoutHeirarchy doesn't exist on API <= 17
|
||||
// http://developer.android.com/reference/android/accessibilityservice/AccessibilityServiceInfo.html#FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
|
||||
private static boolean canDiscard = API_18;
|
||||
private static boolean lastDiscard = false;
|
||||
|
||||
public static void discard(boolean discard) {
|
||||
if (canDiscard) {
|
||||
if (discard != lastDiscard) {
|
||||
UiDevice.getInstance().setCompressedLayoutHeirarchy(discard);
|
||||
lastDiscard = discard;
|
||||
}
|
||||
UiDevice.getInstance().setCompressedLayoutHeirarchy(discard);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,6 @@ public abstract class XMLHierarchy {
|
||||
|
||||
dumpFile.delete();
|
||||
|
||||
//compression off by default TODO add this as a config option
|
||||
NotImportantViews.discard(false);
|
||||
|
||||
try {
|
||||
// dumpWindowHierarchy often has a NullPointerException
|
||||
UiDevice.getInstance().dumpWindowHierarchy(dumpFileName);
|
||||
|
||||
@@ -74,6 +74,9 @@ Selendroid.prototype.init = function () {
|
||||
this.curContext = this.defaultContext();
|
||||
};
|
||||
|
||||
Selendroid.prototype.getSettings = deviceCommon.getSettings;
|
||||
Selendroid.prototype.updateSettings = deviceCommon.updateSettings;
|
||||
|
||||
_.extend(Selendroid.prototype, androidCommon);
|
||||
Selendroid.prototype._deviceConfigure = Device.prototype.configure;
|
||||
Selendroid.prototype._setAndroidArgs = androidCommon.setAndroidArgs;
|
||||
|
||||
@@ -347,3 +347,32 @@ exports.jwpResponse = function (err, val, cb) {
|
||||
}
|
||||
return exports.jwpSuccess(val, cb);
|
||||
};
|
||||
|
||||
// methods for appium session Settings
|
||||
// These should really be on a Session object instead of the Device, but Sessions don't exist yet
|
||||
exports.getSettings = function (cb) {
|
||||
if (!this.settings) {
|
||||
return cb(new UnknownError('No settings object for session'));
|
||||
}
|
||||
|
||||
return cb(null, {
|
||||
status: status.codes.Success.code
|
||||
, value: this.settings._settings
|
||||
});
|
||||
};
|
||||
|
||||
// settings passed in update matching keys in existing settings object, create if not present.
|
||||
exports.updateSettings = function (newSettings, cb) {
|
||||
if (!this.settings) {
|
||||
return cb(new UnknownError('No settings object for session'));
|
||||
}
|
||||
this.settings.update(newSettings, function (err) {
|
||||
if (err) {
|
||||
return cb(new UnknownError('err updating setting: ' + err));
|
||||
}
|
||||
return cb(null, {
|
||||
status: status.codes.Success.code
|
||||
, value: null
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
var EventEmitter = require('events').EventEmitter
|
||||
, CamelBackPromise = require('camel-back-promise') // The straw that breaks the camels back. Instantiate by passing in a deferred promise and a number n. After the CamelBackPromise is called n times, the deferred promise is resolved.
|
||||
, _ = require('underscore')
|
||||
, Q = require('q')
|
||||
;
|
||||
|
||||
// EventEmitter which emits an 'update' event every time a setting is changed. One call to "updateSettings" could trigger multiple update events.
|
||||
// The value of the event is a tuple: {key, value, oldValue, callback}.
|
||||
// Any listeners *MUST* call the function stored in 'callback' to tell the appium server that it it can continue to take commands. This is to prevent race conditions between enabling a setting and calling the next command.
|
||||
// `callback` is a node-style callback, so passing an argument will cause the updateSettings command to throw an error. Note that the setting still gets changed, even if the subscriber doesn't affect the change. It's up to the client to resolve this case.
|
||||
var DeviceSettings = function () {
|
||||
this.init();
|
||||
};
|
||||
|
||||
_.extend(DeviceSettings.prototype, EventEmitter.prototype);
|
||||
|
||||
DeviceSettings.prototype.init = function () {
|
||||
|
||||
this._settings = {};
|
||||
|
||||
// this is where default settings can be declared
|
||||
this._settings.ignoreUnimportantViews = false;
|
||||
};
|
||||
|
||||
DeviceSettings.prototype.update = function (newSettings, cb) {
|
||||
// if this code looks familiar, it's because it's modified from the underscore.js implementation of `extend`
|
||||
if (!_.isObject(newSettings)) {
|
||||
cb();
|
||||
}
|
||||
var numListeners = this.listeners("update").length;
|
||||
|
||||
var prop;
|
||||
var pendingUpdates = [];
|
||||
|
||||
for (prop in newSettings) {
|
||||
if (hasOwnProperty.call(newSettings, prop)) {
|
||||
var deferred = Q.defer();
|
||||
pendingUpdates.push(deferred.promise);
|
||||
|
||||
var updatePayload = {
|
||||
key: prop,
|
||||
value: newSettings[prop],
|
||||
oldValue: this._settings[prop],
|
||||
callback: new CamelBackPromise(deferred, numListeners)
|
||||
};
|
||||
|
||||
this._settings[prop] = newSettings[prop];
|
||||
this.emit("update", updatePayload);
|
||||
}
|
||||
}
|
||||
|
||||
Q.all(pendingUpdates).then(
|
||||
function () {
|
||||
return cb();
|
||||
},
|
||||
function (err) {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = DeviceSettings;
|
||||
@@ -9,6 +9,7 @@ var fs = require('fs')
|
||||
, unzipApp = helpers.unzipApp
|
||||
, downloadFile = helpers.downloadFile
|
||||
, capConversion = require('../server/capabilities.js').capabilityConversions
|
||||
, DeviceSettings = require('./device-settings.js')
|
||||
, url = require('url');
|
||||
|
||||
var Device = function () {
|
||||
@@ -20,6 +21,7 @@ Device.prototype.init = function () {
|
||||
this.tempFiles = [];
|
||||
this.args = {};
|
||||
this.capabilities = {};
|
||||
this.settings = new DeviceSettings();
|
||||
};
|
||||
|
||||
Device.prototype.configure = function (args, caps) {
|
||||
@@ -170,4 +172,9 @@ Device.prototype.downloadAndUnzipApp = function (appUrl, cb) {
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// get a specific setting
|
||||
Device.prototype.getSetting = function (str) {
|
||||
return this.settings._settings[str];
|
||||
};
|
||||
|
||||
module.exports = Device;
|
||||
|
||||
@@ -163,6 +163,8 @@ Firefox.prototype.receive = function (data) {
|
||||
};
|
||||
|
||||
Firefox.prototype.proxy = deviceCommon.proxy;
|
||||
Firefox.prototype.getSettings = deviceCommon.getSettings;
|
||||
Firefox.prototype.updateSettings = deviceCommon.updateSettings;
|
||||
|
||||
Firefox.prototype.push = function (elem) {
|
||||
this.queue.push(elem);
|
||||
|
||||
@@ -1346,6 +1346,8 @@ IOS.prototype.implicitWaitForCondition = deviceCommon.implicitWaitForCondition;
|
||||
IOS.prototype.proxy = deviceCommon.proxy;
|
||||
IOS.prototype.proxyWithMinTime = deviceCommon.proxyWithMinTime;
|
||||
IOS.prototype.respond = deviceCommon.respond;
|
||||
IOS.prototype.getSettings = deviceCommon.getSettings;
|
||||
IOS.prototype.updateSettings = deviceCommon.updateSettings;
|
||||
|
||||
IOS.prototype.initQueue = function () {
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ var androidCaps = [
|
||||
, 'unicodeKeyboard'
|
||||
, 'resetKeyboard'
|
||||
, 'noSign'
|
||||
, 'ignoreUnimportantViews'
|
||||
];
|
||||
|
||||
var iosCaps = [
|
||||
|
||||
@@ -1169,3 +1169,14 @@ exports.setNetworkConnection = function (req, res) {
|
||||
var type = req.body.type || req.body.parameters.type;
|
||||
req.device.setNetworkConnection(type, getResponseHandler(req, res));
|
||||
};
|
||||
|
||||
exports.getSettings = function (req, res) {
|
||||
req.device.getSettings(getResponseHandler(req, res));
|
||||
};
|
||||
|
||||
exports.updateSettings = function (req, res) {
|
||||
var settings = req.body.settings || req.body.parameters.settings;
|
||||
if (checkMissingParams(req, res, {settings: settings})) {
|
||||
req.device.updateSettings(settings, getResponseHandler(req, res));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,6 +133,9 @@ module.exports = function (appium) {
|
||||
rest.post('/wd/hub/session/:sessionId?/appium/element/:elementId?/value', controller.setValueImmediate);
|
||||
rest.post('/wd/hub/session/:sessionId?/appium/element/:elementId?/replace_value', controller.replaceValue);
|
||||
|
||||
rest.post('/wd/hub/session/:sessionId?/appium/settings', controller.updateSettings);
|
||||
rest.get('/wd/hub/session/:sessionId?/appium/settings', controller.getSettings);
|
||||
|
||||
// keep this at the very end!
|
||||
rest.all('/*', controller.unknownCommand);
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"bplist-parser": "~0.0.5",
|
||||
"bufferpack": "0.0.6",
|
||||
"bytes": "~1.0.0",
|
||||
"camel-back-promise": "^1.0.0",
|
||||
"colors": "~0.6.2",
|
||||
"date-utils": "~1.2.15",
|
||||
"difflib": "~0.2.4",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
var setup = require("../../../common/setup-base")
|
||||
, desired = require("../desired")
|
||||
, atv = 'android.widget.TextView'
|
||||
, alv = 'android.widget.ListView';
|
||||
, alv = 'android.widget.ListView'
|
||||
;
|
||||
|
||||
describe("apidemo - find - by xpath", function () {
|
||||
|
||||
@@ -15,7 +16,7 @@ describe("apidemo - find - by xpath", function () {
|
||||
var t = atv;
|
||||
|
||||
before(function (done) {
|
||||
driver.sleep(1000).nodeify(done);
|
||||
driver.sleep(2000).nodeify(done);
|
||||
});
|
||||
|
||||
it('should find element by type', function (done) {
|
||||
@@ -69,4 +70,27 @@ describe("apidemo - find - by xpath", function () {
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
it('should find less elements with compression turned on', function (done) {
|
||||
var getElementsWithoutCompression = function () {
|
||||
return driver.updateSettings({"ignoreUnimportantViews": false}).elementsByXPath("//*");
|
||||
};
|
||||
var getElementsWithCompression = function () {
|
||||
return driver.updateSettings({"ignoreUnimportantViews": true }).elementsByXPath("//*");
|
||||
};
|
||||
|
||||
var elementsWithoutCompression, elementsWithCompression;
|
||||
|
||||
getElementsWithoutCompression()
|
||||
.then(function (els) {
|
||||
elementsWithoutCompression = els;
|
||||
return getElementsWithCompression();
|
||||
})
|
||||
.then(function (els) {
|
||||
elementsWithCompression = els;
|
||||
})
|
||||
.then(function () {
|
||||
return elementsWithoutCompression.length.should.be.greaterThan(elementsWithCompression.length);
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,4 +33,27 @@ describe("apidemos - source", function () {
|
||||
.elementByAccessibilityId('Animation')
|
||||
.nodeify(done);
|
||||
});
|
||||
it('should get less source when compression is enabled', function (done) {
|
||||
var getSourceWithoutCompression = function () {
|
||||
return driver.updateSettings({"ignoreUnimportantViews": false}).source();
|
||||
};
|
||||
var getSourceWithCompression = function () {
|
||||
return driver.updateSettings({"ignoreUnimportantViews": true }).source();
|
||||
};
|
||||
|
||||
var sourceWithoutCompression, sourceWithCompression;
|
||||
|
||||
getSourceWithoutCompression()
|
||||
.then(function (els) {
|
||||
sourceWithoutCompression = els;
|
||||
return getSourceWithCompression();
|
||||
})
|
||||
.then(function (els) {
|
||||
sourceWithCompression = els;
|
||||
})
|
||||
.then(function () {
|
||||
return sourceWithoutCompression.length.should.be.greaterThan(sourceWithCompression.length);
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
var setup = require("./setup-base")
|
||||
, env = require('../../helpers/env')
|
||||
, getAppPath = require('../../helpers/app').getAppPath;
|
||||
|
||||
var app;
|
||||
if (env.IOS) {
|
||||
app = 'testapp';
|
||||
} else if (env.ANDROID || env.SELENDROID) {
|
||||
app = 'ApiDemos';
|
||||
}
|
||||
|
||||
var desired = {
|
||||
app: getAppPath(app)
|
||||
};
|
||||
|
||||
if (env.SELENDROID) {
|
||||
desired.automationName = 'selendroid';
|
||||
}
|
||||
|
||||
describe('settings', function () {
|
||||
var driver;
|
||||
setup(this, desired, {'no-reset': true}).then(function (d) { driver = d; });
|
||||
|
||||
it('should return a settings object even if none specified', function (done) {
|
||||
driver
|
||||
.settings().should.eventually.exist
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it('should be able to store a setting', function (done) {
|
||||
driver
|
||||
.updateSettings({'settlers of': 'Catan'})
|
||||
.settings().should.eventually.have.property('settlers of')
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it('should overwrite new settings', function (done) {
|
||||
driver
|
||||
.updateSettings({'settlers of': 'Catan'})
|
||||
.updateSettings({'settlers of': 'Europa'})
|
||||
.settings().then(function (settings) {
|
||||
return settings['settlers of'].should.equal('Europa');
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it('should leave non-specified settings unchanged', function (done) {
|
||||
driver
|
||||
.updateSettings({'thing one': 1})
|
||||
.updateSettings({'thing two': 2})
|
||||
.settings().should.eventually.have.property('thing one')
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user