aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/helpers
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2015-02-20 09:35:39 -0800
committerKevin Jin <kjin@google.com>2015-02-20 14:37:53 -0800
commit4b31201b5a2dbf8036da5a8d089a68a39cc1dc44 (patch)
tree0a4a6d976ca45f3b87433927d57d50cb3cd51b41 /src/io/appium/droiddriver/helpers
parent85a1731f32032690e528a6ca1084aa148200569b (diff)
downloaddroiddriver-4b31201b5a2dbf8036da5a8d089a68a39cc1dc44.tar.gz
rename package 'com.google.android' to 'io.appium'
Change-Id: I2c7c96cd6a6971806e2ea7b06cd6c2c6666e4340
Diffstat (limited to 'src/io/appium/droiddriver/helpers')
-rw-r--r--src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java288
-rw-r--r--src/io/appium/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java68
-rw-r--r--src/io/appium/droiddriver/helpers/DroidDrivers.java151
-rw-r--r--src/io/appium/droiddriver/helpers/PollingListeners.java56
-rw-r--r--src/io/appium/droiddriver/helpers/ScrollerHelper.java81
-rw-r--r--src/io/appium/droiddriver/helpers/SingleRun.java46
-rw-r--r--src/io/appium/droiddriver/helpers/package-info.java111
7 files changed, 801 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
new file mode 100644
index 0000000..7b29bc0
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.appium.droiddriver.helpers;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Debug;
+import android.test.FlakyTest;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import io.appium.droiddriver.DroidDriver;
+import io.appium.droiddriver.exceptions.UnrecoverableException;
+import io.appium.droiddriver.util.FileUtils;
+import io.appium.droiddriver.util.Logs;
+
+/**
+ * Base class for tests using DroidDriver that reports uncaught exceptions, for * example OOME,
+ * instead of crash. Also supports other features, including taking screenshot on failure. It is NOT
+ * required, but provides handy features.
+ */
+public abstract class BaseDroidDriverTest<T extends Activity> extends
+ D2ActivityInstrumentationTestCase2<T> {
+ /**
+ * Calls {@link DroidDrivers#init} once and only once.
+ */
+ public static class DroidDriversInitializer extends SingleRun {
+ private static DroidDriversInitializer instance;
+ protected final Instrumentation instrumentation;
+
+ protected DroidDriversInitializer(Instrumentation instrumentation) {
+ this.instrumentation = instrumentation;
+ }
+
+ @Override
+ protected void run() {
+ DroidDrivers.init(DroidDrivers.newDriver(instrumentation));
+ }
+
+ public static synchronized DroidDriversInitializer get(Instrumentation instrumentation) {
+ if (instance == null) {
+ instance = new DroidDriversInitializer(instrumentation);
+ }
+ return instance;
+ }
+ }
+
+ private static boolean classSetUpDone = false;
+ // In case of device-wide fatal errors, e.g. OOME, the remaining tests will
+ // fail and the messages will not help, so skip them.
+ private static boolean skipRemainingTests = false;
+ // Store uncaught exception from AUT.
+ private static volatile Throwable uncaughtException;
+ static {
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ uncaughtException = ex;
+ // In most cases uncaughtException will be reported by onFailure().
+ // But if it occurs in InstrumentationTestRunner, it's swallowed.
+ // Always log it for all cases.
+ Logs.log(Log.ERROR, uncaughtException, "uncaughtException");
+ }
+ });
+ }
+
+ protected DroidDriver driver;
+
+ protected BaseDroidDriverTest(Class<T> activityClass) {
+ super(activityClass);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ if (!classSetUpDone) {
+ classSetUp();
+ classSetUpDone = true;
+ }
+ driver = DroidDrivers.get();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ driver = null;
+ }
+
+ protected Context getTargetContext() {
+ return getInstrumentation().getTargetContext();
+ }
+
+ /**
+ * Initializes test fixture once for all tests extending this class. This may have unexpected
+ * behavior - if multiple subclasses override this method, only the first override is executed.
+ * Other overrides are silently ignored. You can either use {@link SingleRun} in {@link #setUp},
+ * or override this method, which is a simpler alternative with the aforementioned catch.
+ * <p>
+ * If an InstrumentationDriver is used, this is a good place to call {@link
+ * io.appium.droiddriver.instrumentation.ViewElement#overrideClassName}
+ */
+ protected void classSetUp() {
+ DroidDriversInitializer.get(getInstrumentation()).singleRun();
+ }
+
+ protected boolean reportSkippedAsFailed() {
+ return false;
+ }
+
+ protected void skip() {
+ if (reportSkippedAsFailed()) {
+ fail("Skipped due to prior failure");
+ }
+ }
+
+ /**
+ * Hook for handling failure, for example, taking a screenshot.
+ */
+ protected void onFailure(Throwable failure) throws Throwable {
+ // If skipRemainingTests is true, the failure has already been reported.
+ if (skipRemainingTests) {
+ return;
+ }
+ if (shouldSkipRemainingTests(failure)) {
+ skipRemainingTests = true;
+ }
+
+ // Give uncaughtException (thrown by AUT instead of tests) high priority
+ if (uncaughtException != null) {
+ failure = uncaughtException;
+ }
+
+ try {
+ if (failure instanceof OutOfMemoryError) {
+ dumpHprof();
+ } else if (uncaughtException == null) {
+ String baseFileName = getBaseFileName();
+ driver.dumpUiElementTree(baseFileName + ".xml");
+ driver.getUiDevice().takeScreenshot(baseFileName + ".png");
+ }
+ } catch (Throwable e) {
+ // This method is for troubleshooting. Do not throw new error; we'll
+ // throw the original failure.
+ Logs.log(Log.WARN, e);
+ if (e instanceof OutOfMemoryError && !(failure instanceof OutOfMemoryError)) {
+ skipRemainingTests = true;
+ try {
+ dumpHprof();
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+
+ throw failure;
+ }
+
+ protected boolean shouldSkipRemainingTests(Throwable e) {
+ return e instanceof UnrecoverableException || e instanceof OutOfMemoryError
+ || skipRemainingTests || uncaughtException != null;
+ }
+
+ /**
+ * Gets the base filename for troubleshooting files. For example, a screenshot
+ * is saved in the file "basename".png.
+ */
+ protected String getBaseFileName() {
+ return "dd/" + getClass().getSimpleName() + "." + getName();
+ }
+
+ protected void dumpHprof() throws IOException {
+ String path = FileUtils.getAbsoluteFile(getBaseFileName() + ".hprof").getPath();
+ // create an empty readable file
+ FileUtils.open(path).close();
+ Debug.dumpHprofData(path);
+ }
+
+ /**
+ * Fixes JUnit3: always call tearDown even when setUp throws. Also adds the
+ * {@link #onFailure} hook.
+ */
+ @Override
+ public void runBare() throws Throwable {
+ if (skipRemainingTests) {
+ skip();
+ return;
+ }
+ if (uncaughtException != null) {
+ onFailure(uncaughtException);
+ }
+
+ Throwable exception = null;
+ try {
+ setUp();
+ runTest();
+ } catch (Throwable runException) {
+ exception = runException;
+ // ActivityInstrumentationTestCase2.tearDown() finishes activity
+ // created by getActivity(), so call this before tearDown().
+ onFailure(exception);
+ } finally {
+ try {
+ tearDown();
+ } catch (Throwable tearDownException) {
+ if (exception == null) {
+ exception = tearDownException;
+ }
+ }
+ }
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ /**
+ * Overrides to fail fast when the test is annotated as FlakyTest and we should skip remaining
+ * tests (the failure is fatal). Most lines are copied from super classes.
+ * <p>
+ * When a flaky test is re-run, tearDown() and setUp() are called first in order to reset state.
+ */
+ @Override
+ protected void runTest() throws Throwable {
+ String fName = getName();
+ assertNotNull(fName);
+ Method method = null;
+ try {
+ // use getMethod to get all public inherited
+ // methods. getDeclaredMethods returns all
+ // methods of this class but excludes the
+ // inherited ones.
+ method = getClass().getMethod(fName, (Class[]) null);
+ } catch (NoSuchMethodException e) {
+ fail("Method \"" + fName + "\" not found");
+ }
+
+ if (!Modifier.isPublic(method.getModifiers())) {
+ fail("Method \"" + fName + "\" should be public");
+ }
+
+ int tolerance = 1;
+ if (method.isAnnotationPresent(FlakyTest.class)) {
+ tolerance = method.getAnnotation(FlakyTest.class).tolerance();
+ }
+
+ for (int runCount = 0; runCount < tolerance; runCount++) {
+ if (runCount > 0) {
+ Logs.logfmt(Log.INFO, "Running %s round %d of %d attempts", fName, runCount + 1, tolerance);
+ // We are re-attempting a test, so reset all state.
+ tearDown();
+ setUp();
+ }
+
+ try {
+ method.invoke(this);
+ return;
+ } catch (InvocationTargetException e) {
+ e.fillInStackTrace();
+ Throwable exception = e.getTargetException();
+ if (shouldSkipRemainingTests(exception) || runCount >= tolerance - 1) {
+ throw exception;
+ }
+ Logs.log(Log.WARN, exception);
+ } catch (IllegalAccessException e) {
+ e.fillInStackTrace();
+ throw e;
+ }
+ }
+ }
+}
diff --git a/src/io/appium/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java b/src/io/appium/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java
new file mode 100644
index 0000000..ab0585e
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.appium.droiddriver.helpers;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.ActivityTestCase;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+/**
+ * Fixes bugs in {@link ActivityInstrumentationTestCase2}.
+ */
+public abstract class D2ActivityInstrumentationTestCase2<T extends Activity> extends
+ ActivityInstrumentationTestCase2<T> {
+ protected D2ActivityInstrumentationTestCase2(Class<T> activityClass) {
+ super(activityClass);
+ }
+
+ /**
+ * Fixes a bug in {@link ActivityTestCase#scrubClass} that causes
+ * NullPointerException if your leaf-level test class declares static fields.
+ * This is <a href="https://code.google.com/p/android/issues/detail?id=4244">a
+ * known bug</a> that has been fixed in ICS Android release. But it still
+ * exists on devices older than ICS. If your test class extends this class, it
+ * can work on older devices.
+ * <p>
+ * In addition to the official fix in ICS and beyond, which skips
+ * {@code final} fields, the fix below also skips {@code static} fields, which
+ * should be the expectation of Java programmers.
+ * </p>
+ */
+ @Override
+ protected void scrubClass(final Class<?> testCaseClass) throws IllegalAccessException {
+ final Field[] fields = getClass().getDeclaredFields();
+ for (Field field : fields) {
+ final Class<?> fieldClass = field.getDeclaringClass();
+ if (testCaseClass.isAssignableFrom(fieldClass) && !field.getType().isPrimitive()
+ && !Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) {
+ try {
+ field.setAccessible(true);
+ field.set(this, null);
+ } catch (Exception e) {
+ android.util.Log.d("TestCase", "Error: Could not nullify field!");
+ }
+
+ if (field.get(this) != null) {
+ android.util.Log.d("TestCase", "Error: Could not nullify field!");
+ }
+ }
+ }
+ }
+}
diff --git a/src/io/appium/droiddriver/helpers/DroidDrivers.java b/src/io/appium/droiddriver/helpers/DroidDrivers.java
new file mode 100644
index 0000000..60c3740
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/DroidDrivers.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.appium.droiddriver.helpers;
+
+import android.annotation.TargetApi;
+import android.app.Instrumentation;
+import android.os.Build;
+import android.os.Bundle;
+
+import java.lang.reflect.InvocationTargetException;
+
+import io.appium.droiddriver.DroidDriver;
+import io.appium.droiddriver.exceptions.DroidDriverException;
+import io.appium.droiddriver.instrumentation.InstrumentationDriver;
+import io.appium.droiddriver.uiautomation.UiAutomationDriver;
+
+/**
+ * Static utility methods pertaining to {@link DroidDriver} instances.
+ */
+public class DroidDrivers {
+ private static DroidDriver driver;
+ private static Instrumentation instrumentation;
+ private static Bundle options;
+
+ /**
+ * Gets the singleton driver. Throws if {@link #init} has not been called.
+ */
+ public static DroidDriver get() {
+ if (DroidDrivers.driver == null) {
+ throw new DroidDriverException("init() has not been called");
+ }
+ return DroidDrivers.driver;
+ }
+
+ /**
+ * Initializes the singleton driver. The singleton driver is NOT required, but
+ * it is handy and using a singleton driver can avoid memory leak if you have
+ * many instances around (for example, one in every test -- JUnit framework
+ * keeps the test instances in memory after running them).
+ */
+ public static void init(DroidDriver driver) {
+ if (DroidDrivers.driver != null) {
+ throw new DroidDriverException("init() can only be called once");
+ }
+ DroidDrivers.driver = driver;
+ }
+
+ /**
+ * Initializes for the convenience methods {@link #getInstrumentation()} and
+ * {@link #getOptions()}. Called by
+ * {@link io.appium.droiddriver.runner.TestRunner}. If a custom
+ * runner is used, this method must be called appropriately, otherwise the two
+ * convenience methods won't work.
+ */
+ public static void initInstrumentation(Instrumentation instrumentation, Bundle arguments) {
+ if (DroidDrivers.instrumentation != null) {
+ throw new DroidDriverException("DroidDrivers.initInstrumentation() can only be called once");
+ }
+ DroidDrivers.instrumentation = instrumentation;
+ DroidDrivers.options = arguments;
+ }
+
+ public static Instrumentation getInstrumentation() {
+ return instrumentation;
+ }
+
+ /**
+ * Gets the <a href=
+ * "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax"
+ * >am instrument options</a>.
+ */
+ public static Bundle getOptions() {
+ return options;
+ }
+
+ /**
+ * Returns whether the running target (device or emulator) has
+ * {@link android.app.UiAutomation} API, which is introduced in SDK API 18
+ * (JELLY_BEAN_MR2).
+ */
+ public static boolean hasUiAutomation() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ }
+
+ /**
+ * Returns a new DroidDriver instance. If am instrument options have "driver",
+ * treat it as the fully-qualified-class-name and create a new instance of it
+ * with {@code instrumentation} as the argument; otherwise a new
+ * platform-dependent default DroidDriver instance.
+ */
+ public static DroidDriver newDriver(Instrumentation instrumentation) {
+ String driverClass = options == null ? null : options.getString("driver");
+ if (driverClass != null) {
+ try {
+ return (DroidDriver) Class.forName(driverClass).getConstructor(Instrumentation.class)
+ .newInstance(instrumentation);
+ } catch (ClassNotFoundException e) {
+ throw new DroidDriverException(e);
+ } catch (NoSuchMethodException e) {
+ throw new DroidDriverException(e);
+ } catch (InstantiationException e) {
+ throw new DroidDriverException(e);
+ } catch (IllegalAccessException e) {
+ throw new DroidDriverException(e);
+ } catch (IllegalArgumentException e) {
+ throw new DroidDriverException(e);
+ } catch (InvocationTargetException e) {
+ throw new DroidDriverException(e);
+ }
+ }
+
+ // If "driver" is not specified, return default.
+ if (hasUiAutomation()) {
+ return newUiAutomationDriver(instrumentation);
+ }
+ return newInstrumentationDriver(instrumentation);
+ }
+
+ /** Returns a new InstrumentationDriver */
+ public static InstrumentationDriver newInstrumentationDriver(Instrumentation instrumentation) {
+ return new InstrumentationDriver(instrumentation);
+ }
+
+ /** Returns a new UiAutomationDriver */
+ @TargetApi(18)
+ public static UiAutomationDriver newUiAutomationDriver(Instrumentation instrumentation) {
+ if (!hasUiAutomation()) {
+ throw new DroidDriverException("UiAutomation is not available below API 18. "
+ + "See http://developer.android.com/reference/android/app/UiAutomation.html");
+ }
+ if (instrumentation.getUiAutomation() == null) {
+ throw new DroidDriverException(
+ "uiAutomation==null: did you forget to set '-w' flag for 'am instrument'?");
+ }
+ return new UiAutomationDriver(instrumentation);
+ }
+}
diff --git a/src/io/appium/droiddriver/helpers/PollingListeners.java b/src/io/appium/droiddriver/helpers/PollingListeners.java
new file mode 100644
index 0000000..2508fdf
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/PollingListeners.java
@@ -0,0 +1,56 @@
+package io.appium.droiddriver.helpers;
+
+import io.appium.droiddriver.DroidDriver;
+import io.appium.droiddriver.Poller.PollingListener;
+import io.appium.droiddriver.exceptions.ElementNotFoundException;
+import io.appium.droiddriver.finders.Finder;
+
+/**
+ * Static utility methods to create commonly used PollingListeners.
+ */
+public class PollingListeners {
+ /**
+ * Tries to find {@code watchFinder}, and clicks it if found.
+ *
+ * @param driver a DroidDriver instance
+ * @param watchFinder Identifies the UI component to watch
+ * @return whether {@code watchFinder} is found
+ */
+ public static boolean tryFindAndClick(DroidDriver driver, Finder watchFinder) {
+ try {
+ driver.find(watchFinder).click();
+ return true;
+ } catch (ElementNotFoundException enfe) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a new {@code PollingListener} that will look for
+ * {@code watchFinder}, then click {@code dismissFinder} to dismiss it.
+ * <p>
+ * Typically a {@code PollingListener} is used to dismiss "random" dialogs. If
+ * you know the certain situation when a dialog is displayed, you should deal
+ * with the dialog in the specific situation instead of using a
+ * {@code PollingListener} because it is checked in all polling events, which
+ * occur frequently.
+ * </p>
+ *
+ * @param watchFinder Identifies the UI component, for example an AlertDialog
+ * @param dismissFinder Identifies the UiElement to click on that will dismiss
+ * the UI component
+ */
+ public static PollingListener newDismissListener(final Finder watchFinder,
+ final Finder dismissFinder) {
+ return new PollingListener() {
+ @Override
+ public void onPolling(DroidDriver driver, Finder finder) {
+ if (driver.has(watchFinder)) {
+ driver.find(dismissFinder).click();
+ }
+ }
+ };
+ }
+
+ private PollingListeners() {}
+}
diff --git a/src/io/appium/droiddriver/helpers/ScrollerHelper.java b/src/io/appium/droiddriver/helpers/ScrollerHelper.java
new file mode 100644
index 0000000..857ccda
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/ScrollerHelper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.appium.droiddriver.helpers;
+
+import io.appium.droiddriver.DroidDriver;
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.exceptions.ElementNotFoundException;
+import io.appium.droiddriver.finders.Finder;
+import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
+import io.appium.droiddriver.scroll.Scroller;
+
+/**
+ * Helper for Scroller.
+ */
+public class ScrollerHelper {
+ private final DroidDriver driver;
+ private final Finder containerFinder;
+ private final Scroller scroller;
+
+ public ScrollerHelper(Scroller scroller, DroidDriver driver, Finder containerFinder) {
+ this.scroller = scroller;
+ this.driver = driver;
+ this.containerFinder = containerFinder;
+ }
+
+ /**
+ * Scrolls {@code containerFinder} in both directions if necessary to find
+ * {@code itemFinder}, which is a descendant of {@code containerFinder}.
+ *
+ * @param itemFinder Finder for the desired item; relative to
+ * {@code containerFinder}
+ * @return the UiElement matching {@code itemFinder}
+ * @throws ElementNotFoundException If no match is found
+ */
+ public UiElement scrollTo(Finder itemFinder) {
+ return scroller.scrollTo(driver, containerFinder, itemFinder);
+ }
+
+ /**
+ * Scrolls {@code containerFinder} in {@code direction} if necessary to find
+ * {@code itemFinder}, which is a descendant of {@code containerFinder}.
+ *
+ * @param itemFinder Finder for the desired item; relative to
+ * {@code containerFinder}
+ * @param direction specifies where the view port will move instead of the finger
+ * @return the UiElement matching {@code itemFinder}
+ * @throws ElementNotFoundException If no match is found
+ */
+ public UiElement scrollTo(Finder itemFinder, PhysicalDirection direction) {
+ return scroller.scrollTo(driver, containerFinder, itemFinder, direction);
+ }
+
+ /**
+ * Scrolls to {@code itemFinder} and returns true, otherwise returns false.
+ *
+ * @param itemFinder Finder for the desired item
+ * @return true if successful, otherwise false
+ */
+ public boolean canScrollTo(Finder itemFinder) {
+ try {
+ scrollTo(itemFinder);
+ return true;
+ } catch (ElementNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/io/appium/droiddriver/helpers/SingleRun.java b/src/io/appium/droiddriver/helpers/SingleRun.java
new file mode 100644
index 0000000..5ffd21e
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/SingleRun.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 DroidDriver committers
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.appium.droiddriver.helpers;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Base class for an action that should run only once no matter how many times the method {@link
+ * #singleRun()} is called upon an instance. Typically it is used on a singleton to achieve once for
+ * a class effect.
+ */
+public abstract class SingleRun {
+ private AtomicBoolean hasRun = new AtomicBoolean();
+
+ /**
+ * Calls {@link #run()} if it is the first time this method is called upon this instance.
+ *
+ * @return true if this is the first time it is called, otherwise false
+ */
+ public final boolean singleRun() {
+ if (hasRun.compareAndSet(false, true)) {
+ run();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Takes the action that should run only once.
+ */
+ protected abstract void run();
+}
diff --git a/src/io/appium/droiddriver/helpers/package-info.java b/src/io/appium/droiddriver/helpers/package-info.java
new file mode 100644
index 0000000..62d1f25
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/package-info.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 DroidDriver committers
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Helper classes for writing an Android UI test framework using DroidDriver.
+ *
+ * <h2>UI test framework design principles</h2>
+ *
+ * A UI test framework should model the UI of the AUT in a hierarchical way to maximize code reuse.
+ * Common interactions should be abstracted as methods of page objects. Uncommon interactions may
+ * not be abstracted, but carried out using "driver" directly.
+ * <p>
+ * The organization of the entities (pages, components) does not need to strictly follow the AUT
+ * structure. The UI model can be greatly simplified to make it easy to use.
+ * <p>
+ * In general the framework should follow these principles:
+ * <ul>
+ * <li>Layered abstraction: at the highest level, methods completely abstract the implementation
+ * detail. This kind of methods carry out a complex action, usually involving multiple steps.
+ * At a lower level, methods can expose some details, e.g. clickInstallButton(), which does a
+ * single action and returns a dialog instance it opens, and let the caller decide how to
+ * further interact with it. Lastly at the lowest level, you can always use "driver" to access
+ * any elements if no higher-level methods are available.</li>
+ * <li>Instance methods of a page object assume the page is currently shown.</li>
+ * <li>If a method opens another page, it should return that page on a best-effort basis. There
+ * could be exceptions where we let callers determine the type of the new page, but that
+ * should be considered hacks and be clearly documented.</li>
+ * <li>The page object constructors are public so that it's easy to hack as mentioned above, but
+ * don't abuse it -- typically callers should acquire page objects by calling methods of other
+ * page objects. The root is the home page of the AUT.</li>
+ * <li>Simple dialogs may not merit their own top-level classes, and can be nested as static
+ * subclasses.</li>
+ * <li>Define constants that use values generated from Android resources instead of using string
+ * literals. For example, call {@link android.content.Context#getResources} to get the
+ * Resources instance, then call {@link android.content.res.Resources#getResourceName} to get
+ * the string representation of a resource id, or call {@link
+ * android.content.res.Resources#getString} to get the localized string of a string resource.
+ * This gives you compile-time check over incompatible changes.</li>
+ * <li>Avoid public constants. Typically clients of a page object are interested in what can be
+ * done on the page (the content or actions), not how to achieve that (which is an
+ * implementation detail). The constants used by the page object hence should be encapsulated
+ * (declared private). Another reason for this item is that the constants may not be real
+ * constants. Instead they are generated from resources and acquiring the values requires the
+ * {@link android.content.Context}, which is not available until setUp() is called. If those
+ * are referenced in static fields of a test class, they will be initialized at class loading
+ * time and result in a crash.</li>
+ * <li>There are cases that exposing public constants is arguably desired. For example, when the
+ * interaction is trivial (e.g. clicking a button that does not open a new page), and there
+ * are many similar elements on the page, thus adding distinct methods for them will bloat the
+ * page object class. In these cases you may define public constants, with a warning that
+ * "Don't use them in static fields of tests".</li>
+ * </ul>
+ *
+ * <h2>Common pitfalls</h2>
+ * <ul>
+ * <li>UI elements are generally views. Users can get attributes and perform actions. Note that
+ * actions often update a UiElement, so users are advised not to store instances of UiElement
+ * for later use - the instances could become stale. In other words, UiElement represents a
+ * dynamic object, while Finder represents a static object. Don't declare fields of the type
+ * UiElement; use Finder instead.</li>
+ * <li>{@link android.test.ActivityInstrumentationTestCase2#getActivity} calls
+ * {@link android.test.InstrumentationTestCase#launchActivityWithIntent}, which may hang in
+ * {@link android.app.Instrumentation#waitForIdleSync}. You can call
+ * {@link android.content.Context#startActivity} directly.</li>
+ * <li>startActivity does not wait until the new Activity is shown. This may cause problem when
+ * the old Activity on screen contains UiElements that match what are expected on the new
+ * Activity - interaction with the UiElements fails because the old Activity is closing.
+ * Sometimes it shows as a test passes when run alone but fails when run with other tests.
+ * The work-around is to add a delay after calling startActivity.</li>
+ * <li>Error "android.content.res.Resources$NotFoundException: Unable to find resource ID ..."?
+ * <br>
+ * This may occur if you reference the AUT's resource in tests, and the two APKs are out of
+ * sync. Solution: build and install both AUT and tests together.</li>
+ * <li>"You said the test runs on older devices as well as API18 devices, but mine is broken on
+ * X (e.g. GingerBread)!"
+ * <br>
+ * This may occur if your AUT has different implementations on older devices. In this case,
+ * your tests have to match the different execution paths of AUT, which requires insight into
+ * the implementation of the AUT. A tip for testing older devices: uiautomatorviewer does not
+ * work on ore-API16 devices (the "Device screenshot" button won't work), but you can use it
+ * with dumps from DroidDriver (use to-uiautomator.xsl to convert the format).</li>
+ * <li>"com.android.launcher has stopped unexpectedly" and logcat says OutOfMemoryError
+ * <br>
+ * This is sometimes seen on GingerBread or other low-memory and slow devices. GC is not fast
+ * enough to reclaim memory on those devices. A work-around: call gc more aggressively and
+ * sleep to let gc run, e.g.
+ * <pre>
+public void setUp() throws Exception {
+ super.setUp();
+ if (Build.VERSION.SDK_INT &lt;= Build.VERSION_CODES.GINGERBREAD_MR1) {
+ Runtime.getRuntime().gc();
+ SystemClock.sleep(1000L);
+ }
+}
+</pre></li>
+ * </ul>
+ */
+package io.appium.droiddriver.helpers;