Fix Android keys by finding focused element

This commit is contained in:
Isaac Murchie 2015-04-23 08:59:31 -07:00
Родитель 42761c1b33
Коммит f8c8034237
4 изменённых файлов: 230 добавлений и 226 удалений

Просмотреть файл

@ -202,15 +202,15 @@ androidController.getPageIndex = function (elementId, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.keys = function (elementId, keys, cb) {
androidController.keys = function (keys, cb) {
var params = {
elementId: elementId,
text: keys
text: keys,
replace: false
};
if (this.args.unicodeKeyboard) {
params.unicodeKeyboard = true;
}
this.proxy(["element:setText", params], cb);
this.proxy(['setText', params], cb);
};
androidController.frame = function (frame, cb) {

Просмотреть файл

@ -2,7 +2,10 @@ package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiSelector;
import io.appium.android.bootstrap.*;
import io.appium.android.bootstrap.exceptions.ElementNotFoundException;
import io.appium.android.bootstrap.handler.Find;
import org.json.JSONException;
import java.util.Hashtable;
@ -26,51 +29,60 @@ public class SetText extends CommandHandler {
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
AndroidElement el = null;
if (command.isElementCommand()) {
// Only makes sense on an element
try {
final Hashtable<String, Object> params = command.params();
final AndroidElement el = command.getElement();
boolean replace = Boolean.parseBoolean(params.get("replace").toString());
String text = params.get("text").toString();
boolean pressEnter = false;
if (text.endsWith("\\n")) {
pressEnter = true;
text = text.replace("\\n", "");
Logger.debug("Will press enter after setting text");
}
boolean unicodeKeyboard = false;
if (params.get("unicodeKeyboard") != null) {
unicodeKeyboard = Boolean.parseBoolean(params.get("unicodeKeyboard").toString());
}
String currText = el.getText();
new Clear().execute(command);
if (!el.getText().isEmpty()) {
// clear could have failed, or we could have a hint in the field
// we'll assume it is the latter
Logger.debug("Text not cleared. Assuming remainder is hint text.");
currText = "";
}
if (!replace) {
text = currText + text;
}
final boolean result = el.setText(text, unicodeKeyboard);
if (!result) {
return getErrorResult("el.setText() failed!");
}
if (pressEnter) {
final UiDevice d = UiDevice.getInstance();
d.pressEnter();
}
return getSuccessResult(result);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
Logger.debug("Using element passed in.");
el = command.getElement();
} else {
return getErrorResult("Unable to set text without an element.");
try {
Logger.debug("Using currently-focused element.");
AndroidElementsHash elements = AndroidElementsHash.getInstance();
el = elements.getElement(new UiSelector().focused(true), "");
} catch (ElementNotFoundException e) {
Logger.debug("Error retrieving focused element: " + e);
return getErrorResult("Unable to set text without a focused element.");
}
}
try {
final Hashtable<String, Object> params = command.params();
boolean replace = Boolean.parseBoolean(params.get("replace").toString());
String text = params.get("text").toString();
boolean pressEnter = false;
if (text.endsWith("\\n")) {
pressEnter = true;
text = text.replace("\\n", "");
Logger.debug("Will press enter after setting text");
}
boolean unicodeKeyboard = false;
if (params.get("unicodeKeyboard") != null) {
unicodeKeyboard = Boolean.parseBoolean(params.get("unicodeKeyboard").toString());
}
String currText = el.getText();
new Clear().execute(command);
if (!el.getText().isEmpty()) {
// clear could have failed, or we could have a hint in the field
// we'll assume it is the latter
Logger.debug("Text not cleared. Assuming remainder is hint text.");
currText = "";
}
if (!replace) {
text = currText + text;
}
final boolean result = el.setText(text, unicodeKeyboard);
if (!result) {
return getErrorResult("el.setText() failed!");
}
if (pressEnter) {
final UiDevice d = UiDevice.getInstance();
d.pressEnter();
}
return getSuccessResult(result);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
}
}

Просмотреть файл

@ -70,6 +70,7 @@ Selendroid.prototype.init = function () {
, ['POST', new RegExp('^/wd/hub/session/[^/]+/network_connection')]
, ['POST', new RegExp('^/wd/hub/session/[^/]+/ime')]
, ['GET', new RegExp('^/wd/hub/session/[^/]+/ime')]
, ['POST', new RegExp('^/wd/hub/session/[^/]+/keys')]
];
this.curContext = this.defaultContext();
};
@ -525,12 +526,11 @@ Selendroid.prototype.defaultWebviewName = function () {
return this.WEBVIEW_WIN + "_0";
};
Selendroid.prototype.setValue = function (elementId, value, cb) {
logger.debug('Setting text on element \'' + elementId + '\': \'' + value + '\'');
var encodeString = function (value, unicode) {
for (var i = 0; i < value.length; i++) {
var c = value.charCodeAt(i);
// if we're using the unicode keyboard, and this is unicode, maybe encode
if (this.args.unicodeKeyboard && (c > 127 || c === 38)) {
if (unicode && (c > 127 || c === 38)) {
// this is not simple ascii, or it is an ampersand (`&`)
if (c >= parseInt("E000", 16) && c <= parseInt("E040", 16)) {
// Selenium uses a Unicode PUA to cover certain special characters
@ -542,6 +542,12 @@ Selendroid.prototype.setValue = function (elementId, value, cb) {
}
}
}
return value;
};
Selendroid.prototype.setValue = function (elementId, value, cb) {
logger.debug('Setting text on element \'' + elementId + '\': \'' + value + '\'');
value = encodeString(value, this.args.unicodeKeyboard);
var reqUrl = this.proxyHost + ':' + this.proxyPort +
'/wd/hub/session/' + this.proxySessionId +
'/element/' + elementId + '/value';
@ -554,6 +560,21 @@ Selendroid.prototype.setValue = function (elementId, value, cb) {
});
};
Selendroid.prototype.keys = function (value, cb) {
logger.debug('Setting text: \'' + value + '\'');
value = encodeString(value, this.args.unicodeKeyboard);
var reqUrl = this.proxyHost + ':' + this.proxyPort +
'/wd/hub/session/' + this.proxySessionId +
'/keys';
doRequest(reqUrl, 'POST', { value: [value] }, null, function (err) {
if (err) return cb(err);
cb(null, {
status: status.codes.Success.code,
value: ''
});
});
};
Selendroid.prototype.back = function (cb) {
this.keyevent(4, null, cb);
};

Просмотреть файл

@ -3,13 +3,11 @@
var env = require('../../helpers/env')
, setup = require("./setup-base")
, _ = require('underscore')
, getAppPath = require('../../helpers/app').getAppPath
, status = require('../../../lib/server/status');
, getAppPath = require('../../helpers/app').getAppPath;
var desired = {
app: getAppPath('ApiDemos'),
appActivity: '.view.TextFields',
newCommandTimeout: 90
};
if (env.SELENDROID) {
@ -19,219 +17,192 @@ if (env.SELENDROID) {
module.exports = function () {
var driver;
var runTextEditTest = function (testText, done) {
var runTextEditTest = function (testText, keys, done) {
var el;
driver
.waitForElementByClassName('android.widget.EditText')
.then(function (_el) { el = _el; })
.waitForElementsByClassName('android.widget.EditText')
// use a text field with no hint text, so clear is faster
.then(function (els) {
el = _.last(els);
return el;
})
.clear()
.then(function () {
if (env.SELENDROID) {
return el.clear();
if (keys) {
return driver.keys(testText);
} else {
return el.sendKeys(testText);
}
})
.then(function () {
if (env.SELENDROID) {
// in Selendroid mode we sometimes get the text before
// it is fully sent to the element
return driver.sleep(300);
}
})
.then(function () { return el.sendKeys(testText); })
.then(function () { return el.text().should.become(testText); })
.nodeify(done);
};
var runEditAndClearTest = function (testText, done) {
var runEditAndClearTest = function (testText, keys, done) {
var el;
driver
.waitForElementByClassName('android.widget.EditText')
.then(function (_el) { el = _el; })
.waitForElementsByClassName('android.widget.EditText')
.then(function (els) {
el = _.last(els);
return el;
})
.clear()
.then(function () {
if (env.SELENDROID) {
return el.clear();
if (keys) {
return driver.keys(testText);
} else {
return el.sendKeys(testText);
}
})
.then(function () { return el.sendKeys(testText).text().should.become(testText); })
.then(function () {
el.text().should.become(testText);
})
.then(function () {
return el.clear().should.not.be.rejected;
})
.then(function () {
// Selendroid and uiautomator have different ways of dealing with
// hint text. In particular, Selendroid does not return it
// and uiautomator does.
var expectedText = "hint text";
if (env.SELENDROID) {
expectedText = "";
}
var expectedText = "";
return el.text().should.become(expectedText);
})
.nodeify(done);
};
describe('editing ascii text field', function () {
setup(this, desired).then(function (d) { driver = d; });
var runKeyboardTests = function (testText) {
return function () {
it('should work with sendKeys', function (done) {
runTextEditTest(testText, false, done);
});
it('should work with keys', function (done) {
runTextEditTest(testText, true, done);
});
};
};
it('should be able to edit a text field', function (done) {
var testText = "Life, the Universe and Everything.";
runTextEditTest(testText, done);
var runKeyEventTests = function () {
var editTextField = 'android.widget.TextView';
if (env.SELENDROID) {
// with Selendroid we can't find classes by their parent class
// and with uiautomator we can't find the subclass.
editTextField = 'io.appium.android.apis.text.LogTextBox';
}
// skip selendroid because selendroid implements keyevent with an adb
// call, and we are unable to send metastate that way
it('should be able to send combination keyevents @skip-selendroid-all', function (done) {
driver
.elementById('clear').click()
.pressDeviceKey(29, 193)
.elementsByClassName(editTextField)
.then(function (els) {
return _.last(els).text();
})
.then(function (txt) {
txt.should.include('keyCode=KEYCODE_A');
txt.should.include('metaState=META_SHIFT_ON');
})
.nodeify(done);
});
it('should be able to edit and manually clear a text field', function (done) {
var testText = "The answer is 42.";
runEditAndClearTest(testText, done);
it('should be able to send keyevents', function (done) {
driver
.elementById('clear').click()
.pressDeviceKey(82)
.elementsByClassName(editTextField)
.then(function (els) {
return _.last(els).text();
})
.then(function (txt) {
txt.should.include('[keycode=82]');
txt.should.include('keyCode=KEYCODE_MENU');
})
.nodeify(done);
});
};
it('should be able to send &-', function (done) {
var testText = '&-';
runTextEditTest(testText, done);
var runManualClearTests = function () {
var testText = "The answer is 42.";
it('should work with sendKeys', function (done) {
runEditAndClearTest(testText, false, done);
});
it('should be able to send & and - in other text', function (done) {
var testText = 'In the mid-1990s he ate fish & chips as mayor-elect.';
runTextEditTest(testText, done);
it('should work with keys', function (done) {
runEditAndClearTest(testText, true, done);
});
};
it('should be able to send - in text', function (done) {
var testText = 'Super-test.';
runTextEditTest(testText, done);
});
describe('pressing device key', function () {
// skip selendroid because selendroid implements keyevent with an adb
// call, and we are unable to send metastate that way
it('should be able to send combination keyevents @skip-selendroid-all', function (done) {
driver
.waitForElementByClassName('android.widget.EditText')
.clear()
.pressDeviceKey(29, 193)
.elementByClassName('android.widget.EditText')
.text().should.become('A')
.nodeify(done);
describe('editing a text field', function () {
var appActivity = '.view.TextFields';
var tests = [
{ label: 'editing a text field', text: 'Life, the Universe and Everything.' },
{ label: 'sending &-', text: '&-' },
{ label: 'sending & and - in other text', text: 'In the mid-1990s he ate fish & chips as mayor-elect.' },
{ label: 'sending - in text', text: 'Super-test.' },
];
describe('ascii', function () {
setup(this, _.defaults({
appActivity: appActivity
}, desired)).then(function (d) { driver = d; });
_.each(tests, function (test) {
describe(test.label, runKeyboardTests(test.text));
});
it('should be able to send keyevents', function (done) {
// This test need to be last before session ending, it exeits the app.
driver
.waitForElementByClassName('android.widget.EditText')
.elementById('com.android.launcher:id/search_button')
.should.be.rejectedWith(status.codes.NoSuchElement.code)
.pressDeviceKey(3)
.waitForElementById('com.android.launcher:id/search_button')
.elementByClassName('android.widget.EditText')
.should.be.rejectedWith(status.codes.NoSuchElement.code)
.nodeify(done);
describe('editing and manually clearing a text field', runManualClearTests);
});
describe('unicode', function () {
setup(this, _.defaults({
appActivity: appActivity,
unicodeKeyboard: true,
resetKeyboard: true
}, desired)).then(function (d) { driver = d; });
var unicodeTests = _.union(tests, [
{ label: 'should be able to send - in unicode text', text: 'परीक्षा-परीक्षण' },
{ label: 'should be able to send & in text', text: 'Fish & chips' },
{ label: 'should be able to send & in unicode text', text: 'Mīna & chips' },
{ label: 'should be able to send roman characters with diacritics', text: 'Áé Œ ù ḍ' },
{ label: 'should be able to send a u with an umlaut', text: 'ü' },
{ label: 'should be able to send Tamil', text: 'சோதனை' },
{ label: 'should be able to send Gujarati', text: 'પરીક્ષણ' },
{ label: 'should be able to send Chinese', text: '测试' },
{ label: 'should be able to send Russian', text: 'тестирование' },
// skip rtl languages, which don't clear correctly atm
// { label: 'should be able to send Arabic', 'تجريب'],
// { label: 'should be able to send Hebrew', 'בדיקות'],
]);
_.each(unicodeTests, function (test) {
describe(test.label, runKeyboardTests(test.text));
});
describe('editing and manually clearing a text field', runManualClearTests);
});
});
describe('editing unicode text field', function () {
setup(this, _.defaults({
unicodeKeyboard: true,
resetKeyboard: true
}, desired)).then(function (d) { driver = d; });
it('should be able to edit a text field', function (done) {
var testText = "Life, the Universe and Everything.";
runTextEditTest(testText, done);
describe('key events', function () {
var appActivity = '.text.KeyEventText';
describe('ascii', function () {
setup(this, _.defaults({
appActivity: appActivity
}, desired)).then(function (d) { driver = d; });
describe('pressing device key', runKeyEventTests);
});
it('should be able to edit and manually clear a text field', function (done) {
var testText = "The answer is 42.";
runEditAndClearTest(testText, done);
});
it('should be able to send &-', function (done) {
var testText = '&-';
runTextEditTest(testText, done);
});
it('should be able to send & and - in other text', function (done) {
var testText = 'In the mid-1990s he ate fish & chips as mayor-elect.';
runTextEditTest(testText, done);
});
it('should be able to send - in text', function (done) {
var testText = 'Super-test.';
runTextEditTest(testText, done);
});
it('should be able to send - in unicode text', function (done) {
var testText = 'परीक्षा-परीक्षण';
runTextEditTest(testText, done);
});
it('should be able to send & in text', function (done) {
var testText = 'Fish & chips';
runTextEditTest(testText, done);
});
it('should be able to send & in unicode text', function (done) {
var testText = 'Mīna & chips';
runTextEditTest(testText, done);
});
it('should be able to send roman characters with diacritics', function (done) {
var testText = 'Áé Œ ù ḍ';
runTextEditTest(testText, done);
});
it('should be able to send a u with an umlaut', function (done) {
var testText = 'ü';
runTextEditTest(testText, done);
});
// skipping because clear doesn't work reliably on RTL scripts
it.skip('should be able to send Arabic', function (done) {
var testText = 'تجريب';
runTextEditTest(testText, done);
});
// skipping because clear doesn't work reliably on RTL scripts
it.skip('should be able to send Hebrew', function (done) {
var testText = 'בדיקות';
runTextEditTest(testText, done);
});
it('should be able to send Tamil', function (done) {
var testText = 'சோதனை';
runTextEditTest(testText, done);
});
it('should be able to send Gujarati', function (done) {
var testText = 'પરીક્ષણ';
runTextEditTest(testText, done);
});
it('should be able to send Chinese', function (done) {
var testText = '测试';
runTextEditTest(testText, done);
});
it('should be able to send Russian', function (done) {
var testText = 'тестирование';
runTextEditTest(testText, done);
});
describe('pressing device key with unicode keyboard', function () {
// skip selendroid because selendroid implements keyevent with an adb
// call, and we are unable to send metastate that way
it('should be able to send combination keyevents @skip-selendroid-all', function (done) {
driver
.waitForElementByClassName('android.widget.EditText')
.clear()
.pressDeviceKey(29, 193)
.elementByClassName('android.widget.EditText')
.text().should.become('A')
.nodeify(done);
});
it('should be able to send keyevents', function (done) {
// This test need to be last before session ending, it exeits the app.
driver
.waitForElementByClassName('android.widget.EditText')
.elementById('com.android.launcher:id/search_button')
.should.be.rejectedWith(status.codes.NoSuchElement.code)
.pressDeviceKey(3)
.waitForElementById('com.android.launcher:id/search_button')
.elementByClassName('android.widget.EditText')
.should.be.rejectedWith(status.codes.NoSuchElement.code)
.nodeify(done);
});
describe('unicode', function () {
setup(this, _.defaults({
appActivity: appActivity,
unicodeKeyboard: true,
resetKeyboard: true
}, desired)).then(function (d) { driver = d; });
describe('pressing device key', runKeyEventTests);
});
});
};