diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Clear.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Clear.java index b3af246d7..d69dac6a4 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Clear.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Clear.java @@ -1,18 +1,18 @@ package io.appium.android.bootstrap.handler; -import com.android.uiautomator.core.UiObjectNotFoundException; -import io.appium.android.bootstrap.*; -import org.json.JSONException; -import android.os.SystemClock; -import android.view.KeyEvent; -import java.lang.reflect.Method; import android.graphics.Rect; -import com.android.uiautomator.common.ReflectionUtils; -import com.android.uiautomator.core.UiObject; +import android.os.SystemClock; import android.view.InputDevice; import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import com.android.uiautomator.common.ReflectionUtils; +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiObjectNotFoundException; import com.android.uiautomator.core.UiSelector; - +import io.appium.android.bootstrap.*; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import org.json.JSONException; /** @@ -43,49 +43,39 @@ public class Clear extends CommandHandler { if (command.isElementCommand()) { try { final AndroidElement el = command.getElement(); - final ReflectionUtils utils = new ReflectionUtils(); - Rect rect = el.getVisibleBounds(); - // Trying to select entire text. - TouchLongClick.correctLongClick(rect.left + 20, rect.centerY(),2000); - UiObject selectAll = new UiObject(new UiSelector().descriptionContains("Select all")); - if (selectAll.waitForExists(2000)) { - selectAll.click(); - } - // wait for the selection - SystemClock.sleep(500); - // delete it - final Method sendKey = utils.getControllerMethod("sendKey", int.class,int.class); - sendKey.invoke(utils.getController(), KeyEvent.KEYCODE_DEL, 0); + + // first, try to do native clearing + Logger.debug("Attempting to clear using UiObject.clearText()."); + el.clearText(); if (el.getText().isEmpty()) { return getSuccessResult(true); } - // If above strategy does not work then sending bunch of delete keys after clicking on element. - Logger.debug("Clearing text not successful using selectAllDelete now trying to send delete keys."); - String tempTextHolder = ""; - final Object bridgeObject = utils.getBridge(); - final Method injectInputEvent = utils.getMethodInjectInputEvent(); - // Preventing infinite while loop. - while (!el.getText().isEmpty() && !tempTextHolder.equalsIgnoreCase(el.getText())) { - tempTextHolder = el.getText(); - // Trying send delete keys after clicking in text box. - el.click(); - // Sending 25 delete keys asynchronously - final long eventTime = SystemClock.uptimeMillis(); - KeyEvent deleteEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_DEL, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, - InputDevice.SOURCE_KEYBOARD); - for (int count = 0; count < 25; count++) { - injectInputEvent.invoke(bridgeObject, deleteEvent, false); - } + + final ReflectionUtils utils = new ReflectionUtils(); + + // next try to select everything and delete + Logger.debug("Clearing text not successful. Attempting to clear " + + "by selecting all and deleting."); + if (selectAndDelete(el, utils)) { + return getSuccessResult(true); } - // If still text exist falling back on UIautomator clearText. - if (!el.getText().isEmpty()) { - Logger.debug("Clearing text not successful falling back to UiAutomator method clear"); - el.clearText(); + + // finally try to send delete keys + Logger.debug("Clearing text not successful. Attempting to clear " + + "by sending delete keys."); + if (sendDeleteKeys(el, utils)) { + return getSuccessResult(true); } - // If clear text is still unsuccessful throwing error back + if (!el.getText().isEmpty()) { + // either there was a failure, or there is hint text + if (hasHintText(el, utils)) { + Logger.debug("Text remains after clearing, " + + "but it appears to be hint text."); + return getSuccessResult(true); + } else { return getErrorResult("Clear text not successful."); + } } return getSuccessResult(true); } catch (final UiObjectNotFoundException e) { @@ -94,7 +84,65 @@ public class Clear extends CommandHandler { } catch (final Exception e) { // handle NullPointerException return getErrorResult("Unknown error clearing text"); } - } - return getErrorResult("Unknown error"); } + return getErrorResult("Unknown error"); } + + private boolean selectAndDelete(AndroidElement el, final ReflectionUtils utils) + throws UiObjectNotFoundException, IllegalAccessException, + InvocationTargetException, NoSuchMethodException { + Rect rect = el.getVisibleBounds(); + // Trying to select entire text. + TouchLongClick.correctLongClick(rect.left + 20, rect.centerY(), 2000); + UiObject selectAll = new UiObject(new UiSelector().descriptionContains("Select all")); + if (selectAll.waitForExists(2000)) { + selectAll.click(); + } + // wait for the selection + SystemClock.sleep(500); + // delete it + final Method sendKey = utils.getControllerMethod("sendKey", int.class, int.class); + sendKey.invoke(utils.getController(), KeyEvent.KEYCODE_DEL, 0); + + return el.getText().isEmpty(); + } + + private boolean sendDeleteKeys(AndroidElement el, final ReflectionUtils utils) + throws UiObjectNotFoundException, IllegalAccessException, + InvocationTargetException, NoSuchMethodException { + String tempTextHolder = ""; + final Object bridgeObject = utils.getBridge(); + final Method injectInputEvent = utils.getMethodInjectInputEvent(); + // Preventing infinite while loop. + while (!el.getText().isEmpty() && !tempTextHolder.equalsIgnoreCase(el.getText())) { + tempTextHolder = el.getText(); + // Trying send delete keys after clicking in text box. + el.click(); + // Sending 25 delete keys asynchronously + final long eventTime = SystemClock.uptimeMillis(); + KeyEvent deleteEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_DEL, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + for (int count = 0; count < 25; count++) { + injectInputEvent.invoke(bridgeObject, deleteEvent, false); + } + } + + return el.getText().isEmpty(); + } + + private boolean hasHintText(AndroidElement el, final ReflectionUtils utils) + throws UiObjectNotFoundException, IllegalAccessException, + InvocationTargetException, NoSuchMethodException { + // to test if the remaining text is hint text, try sending a single + // delete key and testing if there is any change. + // ignore the off-chance that the delete silently fails and we get a false + // positive. + String currText = el.getText(); + + final Method sendKey = utils.getControllerMethod("sendKey", int.class, int.class); + sendKey.invoke(utils.getController(), KeyEvent.KEYCODE_DEL, 0); + + return currText.equals(el.getText()); + } +} diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/SetText.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/SetText.java index 2210711ee..adf146c29 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/SetText.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/SetText.java @@ -46,7 +46,10 @@ public class SetText extends CommandHandler { String currText = el.getText(); new Clear().execute(command); if (!el.getText().isEmpty()) { - Logger.debug("clearText not successful, continuing with setText anyway"); + // 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; diff --git a/submodules/ApiDemos b/submodules/ApiDemos index 43889d79c..c7973952c 160000 --- a/submodules/ApiDemos +++ b/submodules/ApiDemos @@ -1 +1 @@ -Subproject commit 43889d79c5dc1ef4a0c90d0be136f506a2b19c3b +Subproject commit c7973952c0a55e61e49de2110e04fa9da17f063b diff --git a/test/functional/common/android-keyboard-base.js b/test/functional/common/android-keyboard-base.js index ff44724b1..7777ffef2 100644 --- a/test/functional/common/android-keyboard-base.js +++ b/test/functional/common/android-keyboard-base.js @@ -2,7 +2,6 @@ var env = require('../../helpers/env') , setup = require("./setup-base") - , safeClear = require('../../helpers/safe-clear') , _ = require('underscore') , getAppPath = require('../../helpers/app').getAppPath; @@ -24,12 +23,43 @@ module.exports = function () { driver .waitForElementByClassName('android.widget.EditText') .then(function (_el) { el = _el; }) - .then(function () { return safeClear(el); }) + .then(function () { + if (env.SELENDROID) { + return el.clear(); + } + }) .then(function () { return el.sendKeys(testText); }) .then(function () { return el.text().should.become(testText); }) .nodeify(done); }; + var runEditAndClearTest = function (testText, done) { + var el; + driver + .waitForElementByClassName('android.widget.EditText') + .then(function (_el) { el = _el; }) + .then(function () { + if (env.SELENDROID) { + return el.clear(); + } + }) + .then(function () { return el.sendKeys(testText).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 = ""; + } + return el.text().should.become(expectedText); + }) + .nodeify(done); + }; + describe('editing ascii text field', function () { setup(this, desired).then(function (d) { driver = d; }); @@ -38,22 +68,12 @@ module.exports = function () { runTextEditTest(testText, done); }); - // TODO: clear is not reliable - it('should be able to edit and clear a text field', function (done) { - var testText = "The answer is 42.", el; - driver - .waitForElementByClassName('android.widget.EditText') - .then(function (_el) { el = _el; }) - .then(function () { return safeClear(el); }) - .then(function () { return el.sendKeys(testText).text().should.become(testText); }) - .then(function () { return safeClear(el); }) - // TODO: there is a bug here we should not need safeClear - // workaround for now. - .then(function () { return el.text().should.become(""); }) - .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('as should be able to send &-', function (done) { + it('should be able to send &-', function (done) { var testText = '&-'; runTextEditTest(testText, done); }); @@ -80,19 +100,9 @@ module.exports = function () { runTextEditTest(testText, done); }); - // TODO: clear is not reliable - it('should be able to edit and clear a text field', function (done) { - var testText = "The answer is 42.", el; - driver - .waitForElementByClassName('android.widget.EditText') - .then(function (_el) { el = _el; }) - .then(function () { return safeClear(el); }) - .then(function () { return el.sendKeys(testText).text().should.become(testText); }) - .then(function () { return safeClear(el); }) - // TODO: there is a bug here we should not need safeClear - // workaround for now. - .then(function () { return el.text().should.become(""); }) - .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 &-', function (done) {