Merge pull request #2153 from DylanLacey/class_name_strat

Add class name locator strategy, comply with MJSONWP
This commit is contained in:
Jonathan Lipps
2014-03-25 18:56:07 -07:00
9 changed files with 320 additions and 46 deletions

View File

@@ -352,6 +352,12 @@ public class Find extends CommandHandler {
switch (strategy) {
case CLASS_NAME:
sel = sel.className(text);
if (!many) {
sel = sel.instance(0);
}
selectors.add(sel);
break;
case TAG_NAME:
final String androidClass = AndroidElementClassMap.match(text);
sel = sel.className(androidClass);

View File

@@ -5,7 +5,8 @@ var errors = require('../server/errors.js')
, _ = require('underscore')
, exec = require('child_process').exec
, status = require("../server/status.js")
, logger = require('../server/logger.js').get('appium');
, logger = require('../server/logger.js').get('appium')
, logDeprecationWarning = require('../helpers.js').logDeprecationWarning;
var UnknownError = errors.UnknownError
, ProtocolError = errors.ProtocolError;
@@ -208,19 +209,28 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
'dynamic',
'class name'
];
var nativeStrats = [
'-ios-uiautomation'
];
var webStrats = [
'link text',
'css selector',
'partial link text'
];
var nativeDeprecations = {
'tag name': 'class name'
};
var webDeprecations = {
};
var deprecations = {};
if (includeWeb) {
validStrats = validStrats.concat([
'link text',
'css selector',
'partial link text'
]);
validStrats = validStrats.concat(webStrats);
deprecations = webDeprecations;
} else {
validStrats = validStrats.concat(nativeStrats);
deprecations = nativeDeprecations;
}
if (!includeWeb) {
validStrats = validStrats.concat([
'-ios_uiautomation'
]);
}
if (!_.contains(validStrats, strat)) {
logger.info("Invalid locator strategy: " + strat);
cb(null, {
@@ -229,6 +239,9 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
});
return false;
} else {
if (_.has(deprecations, strat)) {
logDeprecationWarning('locator strategy', strat, deprecations[strat]);
}
return true;
}
};

View File

@@ -30,6 +30,38 @@ var logTypesSupported = {
'crashlog': 'Crash logs for iOS applications on real devices and simulators'
};
iOSController.createGetElementCommand = function (strategy, selector, ctx, many) {
var ext = many ? 's' : '';
var command = "";
switch (strategy) {
case "name":
helpers.logDeprecationWarning("Locator Strategy", '"name"', '"accessibility_id"');
command = ["au.getElement", ext, "ByName('", selector, "'", ctx, ")"].join('');
break;
case "accessibility_id":
command = ["au.getElement", ext, "ByName('", selector, "'", ctx, ")"].join('');
break;
case "xpath":
command = ["au.getElement", ext, "ByXpath('", selector, "'", ctx, ")"].join('');
break;
case "id":
selector = selector.replace(/'/g, "\\\\'"); // must escape single quotes
command = ["var exact = au.mainApp().getFirstWithPredicateWeighted(\"name == '", selector,
"' || label == '", selector, "' || value == '", selector, "'\");"].join('');
command += ["exact && exact.status == 0 ? exact : au.mainApp().getFirstWith",
"PredicateWeighted(\"name contains[c] '", selector, "' || label contains[c] '",
selector, "' || value contains[c] '", selector, "'\");"].join('');
break;
case "-ios_uiautomation":
command = ["au.getElement", ext, "ByUIAutomation('", selector, "'", ctx, ")"].join('');
break;
default:
command = ["au.getElement", ext, "ByType('", selector, "'", ctx, ")"].join('');
}
return command;
};
iOSController.findUIElementOrElements = function (strategy, selector, ctx, many, cb) {
selector = escapeSpecialChars(selector, "'");
if (typeof ctx === "undefined" || !ctx) {
@@ -39,41 +71,18 @@ iOSController.findUIElementOrElements = function (strategy, selector, ctx, many,
ctx = ", '" + ctx + "'";
}
if (strategy === "id") {
var strings = this.localizableStrings;
if (strings && strings.length >= 1) selector = strings[0][selector];
try {
selector = this.getSelectorForStrategy(strategy, selector);
} catch (e) {
cb(null, {
status: status.codes.UnknownError.code
, value: e
});
}
if (!selector) return;
var doFind = function (findCb) {
var ext = many ? 's' : '';
var command = "";
switch (strategy) {
case "name":
helpers.logDeprecationWarning("Locator Strategy", '"name"', '"accessibility_id"');
command = ["au.getElement", ext, "ByName('", selector, "'", ctx, ")"].join('');
break;
case "accessibility_id":
command = ["au.getElement", ext, "ByName('", selector, "'", ctx, ")"].join('');
break;
case "xpath":
command = ["au.getElement", ext, "ByXpath('", selector, "'", ctx, ")"].join('');
break;
case "id":
selector = selector.replace(/'/g, "\\\\'"); // must escape single quotes
command = ["var exact = au.mainApp().getFirstWithPredicateWeighted(\"name == '", selector,
"' || label == '", selector, "' || value == '", selector, "'\");"].join('');
command += ["exact && exact.status == 0 ? exact : au.mainApp().getFirstWith",
"PredicateWeighted(\"name contains[c] '", selector, "' || label contains[c] '",
selector, "' || value contains[c] '", selector, "'\");"].join('');
break;
case "-ios_uiautomation":
command = ["au.getElement", ext, "ByUIAutomation('", selector, "'", ctx, ")"].join('');
break;
default:
command = ["au.getElement", ext, "ByType('", selector, "'", ctx, ")"].join('');
}
var command = this.createGetElementCommand(strategy, selector, ctx, many);
this.proxy(command, function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
@@ -91,6 +100,21 @@ iOSController.findUIElementOrElements = function (strategy, selector, ctx, many,
}
};
iOSController.getSelectorForStrategy = function (strategy, selector) {
var newSelector = selector;
if (strategy === "id") {
var strings = this.localizableStrings;
if (strings && strings.length >= 1) newSelector = strings[0][selector];
}
if (strategy === 'class name') {
if (selector.indexOf('UIA') !== 0) {
throw new TypeError("The class name selector must use full UIA class " +
"names. Try 'UIA" + selector + "' instead.");
}
}
return newSelector;
};
iOSController.handleFindCb = function (err, res, many, findCb) {
if (!res) res = {};
if (res.value === null) {

View File

@@ -89,7 +89,8 @@ IOS.prototype.init = function () {
this.curWebCoords = null;
this.onPageChangeCb = null;
this.dontDeleteSimApps = false;
this.supportedStrategies = ["name", "accessibility_id", "tag name", "xpath", "id", "-ios_uiautomation"];
this.supportedStrategies = ["name", "tag name", "xpath", "id", "-ios_uiautomation",
"class name", "accessibility_id"];
this.localizableStrings = {};
this.keepAppToRetainPrefs = false;
};

View File

@@ -103,6 +103,7 @@
"node-static": "~0.7.2",
"q": "~1.0.0",
"unorm": "~1.3.1",
"sinon": "~1.9.0"
"sinon": "~1.9.0",
"sinon-chai": "^2.5.0"
}
}

View File

@@ -52,6 +52,17 @@ describe("apidemo - find elements -", function () {
.should.eventually.have.length.at.least(10)
.nodeify(done);
});
it('should find an element by class name', function (done) {
driver
.elementByClassName("android.widget.TextView").text().should.become("API Demos")
.nodeify(done);
});
it('should find an element by class name', function (done) {
driver
.elementsByClassName("android.widget.TextView")
.should.eventually.have.length.at.least(10)
.nodeify(done);
});
it('should not find an element that doesnt exist', function (done) {
driver
.elementByTagName("blargimarg").should.be.rejectedWith(/status: 7/)

View File

@@ -60,6 +60,13 @@ describe('testapp - find element -', function () {
els[0].value.should.exist;
}).nodeify(done);
});
it('should find all elements by class name in the app', function (done) {
driver
.elementsByClassName('UIAButton').then(function (els) {
[4, 6].should.contain(els.length);
els[0].value.should.exist;
}).nodeify(done);
});
it('should not find any elements on the app but fail gracefully', function (done) {
driver.elementsByTagName('buttonNotThere').should.eventually.have.length(0)
.nodeify(done);
@@ -77,6 +84,14 @@ describe('testapp - find element -', function () {
}).should.be.rejectedWith(/status: 7/)
.nodeify(done);
});
it('should not find element by incomplete class name but return respective error code', function (done) {
driver.elementsByClassName('notAValidReference')
.catch(function (err) {
err['jsonwire-error'].summary.should.eql('UnknownError');
throw err;
}).should.be.rejectedWith(/status: 13/)
.nodeify(done);
});
it('should find multiple elements by valid name', function (done) {
driver.elementsByName('AppElem').should.eventually.have.length(3)

View File

@@ -0,0 +1,126 @@
'use strict';
var common = require('../../lib/devices/common.js')
, checkValidLocStrat = common.checkValidLocStrat
, clearWarnings = require('../../lib/helpers.js').clearWarnings
, loggerjs = require('../../lib/server/logger')
, logger = loggerjs.get('appium');
var _ = require('underscore')
, chai = require('chai')
, should = chai.should()
, sinon = require('sinon');
describe('devices/common.js', function () {
var null_cb = function () {};
var assertLocatorValidity = function (name, loc, includeWeb, expected) {
var cb = function () {};
it(name, function () {
checkValidLocStrat(loc, includeWeb, cb).should.equal(expected);
});
};
var testLocatorIsValid = function (loc, webIncluded) {
var name = 'should treat the ' + loc + ' strategy as valid';
assertLocatorValidity(name, loc, webIncluded, true);
};
var testLocatorIsInvalid = function (loc, webIncluded) {
var name = 'should treat the ' + loc + ' strategy as invalid';
assertLocatorValidity(name, loc, webIncluded, false);
};
var warningTemplate = _.template(
"[DEPRECATED] The <%= deprecated %> locator strategy has been " +
"deprecated and will be removed. Please use the " +
"<%= replacement %> locator strategy instead."
);
var expectedWarning = function (selector, replacement) {
return warningTemplate({deprecated: selector, replacement: replacement});
};
describe('#checkValidLocStrat', function () {
var valid_strategies = [
'xpath',
'id',
'name',
'dynamic',
'tag name',
'class name'
];
var valid_web_strats = [
'link text',
'css selector',
'partial link text'
];
describe('in the native context', function () {
_.each(valid_strategies, function (strategy) {
testLocatorIsValid(strategy, false);
});
_.each(valid_web_strats, function (strategy) {
testLocatorIsInvalid(strategy, false);
});
it('rejects invalid locator strategies', function () {
checkValidLocStrat('derp', false, null_cb).should.equal(false);
});
describe('single context strategy', function () {
beforeEach(function () {
clearWarnings();
sinon.spy(logger, 'warn');
});
afterEach(function () {
logger.warn.restore();
});
describe('tag name', function () {
assertLocatorValidity('is valid', 'tag name', false, true);
it('emits a deprecation warning', function () {
var warning = expectedWarning('tag name', 'class name');
checkValidLocStrat('tag name', false, null_cb);
logger.warn.called.should.equal(true);
logger.warn.args[0][0].should.equal(warning);
});
});
});
});
describe('in the web context', function () {
_.each(valid_strategies, function (strategy) {
testLocatorIsValid(strategy, true);
});
_.each(valid_web_strats, function (strategy) {
testLocatorIsValid(strategy, true);
});
it('rejects invalid locator strategies', function () {
var null_cb = function () {};
checkValidLocStrat('derp', true, null_cb).should.equal(false);
});
describe('single context strategy', function () {
beforeEach(function () {
clearWarnings();
sinon.spy(logger, 'warn');
});
afterEach(function () {
logger.warn.restore();
});
describe('tag name', function () {
it('does not log a deprecation warning', function () {
checkValidLocStrat('tag name', true, null_cb);
logger.warn.called.should.equal(false);
});
});
});
});
});
});

View File

@@ -0,0 +1,77 @@
"use strict";
var chai = require('chai')
, should = chai.should
, controller = require('../../lib/devices/ios/ios-controller.js')
, createGetElementCommand = controller.createGetElementCommand
, getSelectorForStrategy = controller.getSelectorForStrategy;
describe('ios-controller', function () {
describe('#createGetElementCommand', function () {
it('should return \'GetType\' for name selection', function () {
var actual = createGetElementCommand('name', 'UIAKey', null, false);
actual.should.equal("au.getElementByName('UIAKey')");
});
it('should return \'GetType\' for xpath selection', function () {
var actual = createGetElementCommand('xpath', 'UIAKey', null, false);
actual.should.equal("au.getElementByXpath('UIAKey')");
});
it('should return \'GetType\' for id selection', function () {
var actual = createGetElementCommand('id', 'UIAKey', null, false);
var expected = "var exact = au.mainApp().getFirstWithPredicateWeighted" +
"(\"name == 'UIAKey' || label == 'UIAKey' || value == '" +
"UIAKey'\");exact && exact.status == 0 ? exact : " +
"au.mainApp().getFirstWithPredicateWeighted(\"name " +
"contains[c] 'UIAKey' || label contains[c] 'UIAKey' || " +
"value contains[c] 'UIAKey'\");";
actual.should.equal(expected);
});
it('should return \'GetType\' for tag name selection', function () {
var actual = createGetElementCommand('tag name', 'UIAKey', null, false);
actual.should.equal("au.getElementByType('UIAKey')");
});
it('should return \'GetType\' for class name selection', function () {
var actual = createGetElementCommand('class name', 'UIAKey', null, false);
actual.should.equal("au.getElementByType('UIAKey')");
});
});
describe('#getSelectorForStrategy', function () {
describe('given a class name', function () {
it('should allow UIA names', function () {
getSelectorForStrategy('class name', 'UIAKey').should.equal('UIAKey');
});
it('should return an error when given a non-uia name', function () {
var msg = "The class name selector must use full UIA class " +
"names. Try 'UIAkey' instead.";
(function () {
getSelectorForStrategy('class name', 'key');
}).should.Throw(TypeError, msg);
});
});
describe('given an id', function () {
describe('when there are no localizableStrings', function () {
beforeEach(function () {
controller.localizableStrings = {};
});
it('returns the selector if there aren\'t localizableStrings', function () {
var actual = controller.getSelectorForStrategy('id', 'someSelector');
actual.should.equal('someSelector');
});
});
describe('when there are localizableStrings', function () {
beforeEach(function () {
var locString = [{'someSelector': 'localSelector'}];
controller.localizableStrings = locString;
});
afterEach(function () {
controller.localizableStrings = {};
});
it('returns the localized string', function () {
var actual = controller.getSelectorForStrategy('id', 'someSelector');
actual.should.equal('localSelector');
});
});
});
});
});