mirror of
https://github.com/appium/appium.git
synced 2026-02-11 20:39:04 -06:00
Merge pull request #2153 from DylanLacey/class_name_strat
Add class name locator strategy, comply with MJSONWP
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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)
|
||||
|
||||
126
test/unit/common-device-specs.js
Normal file
126
test/unit/common-device-specs.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
77
test/unit/ios-controller-specs.js
Normal file
77
test/unit/ios-controller-specs.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user