diff options
Diffstat (limited to 'src/io/appium/droiddriver/helpers')
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 <= Build.VERSION_CODES.GINGERBREAD_MR1) { + Runtime.getRuntime().gc(); + SystemClock.sleep(1000L); + } +} +</pre></li> + * </ul> + */ +package io.appium.droiddriver.helpers; |