Slide 1

Slide 1 text

TAKING CARE OF YOUR UI TESTS

Slide 2

Slide 2 text

ESPRESSO

Slide 3

Slide 3 text

Maintainable Extendable Readable Stable

Slide 4

Slide 4 text

@florianmski

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

PAGEOBJECT

Slide 8

Slide 8 text

What? Wraps screen with an application-specific API Allow interacting with screen elements easily Test code not affected by UI changes

Slide 9

Slide 9 text

Why? Test code easier to understand Logic is about the intention of the test Don’t care about UI details

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

onView(withId(R.id.track_detail_actions_more)) .perform(click()); onView(withId(R.id.bottom_options_recycler_view)) .perform(actionOnItem(hasDescendant(allOf( withId(R.id.bottom_options_item_title), withText(R.string.track_detail_action_private))), click())); onView(withId(android.R.id.button1)).perform(click());

Slide 12

Slide 12 text

onView(withId(R.id.track_detail_actions_more)) .perform(click()); onView(withId(R.id.bottom_options_recycler_view)) .perform(actionOnItem(hasDescendant(allOf( withId(R.id.bottom_options_item_title), withText(R.string.track_detail_action_private))), click())); onView(withId(android.R.id.button1)).perform(click());

Slide 13

Slide 13 text

onView(withId(R.id.track_detail_actions_more)) .perform(click()); onView(withId(R.id.bottom_options_recycler_view)) .perform(actionOnItem(hasDescendant(allOf( withId(R.id.bottom_options_item_title), withText(R.string.track_detail_action_private))), click())); onView(withId(android.R.id.button1)).perform(click());

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

trackDetailScreen.showTrackOptions() .tapOnMakeTrackPrivateOption() .tapOnConfirmInDialog();

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

SCREENS, INTERACTORS & ASSERTIONS

Slide 18

Slide 18 text

Screens public class TrackDetailScreen extends Screen {
 
 private final TrackDetailInteractors interactors;
 
 public TrackDetailScreen() {
 super(R.id.track_detail_container);
 interactors = new TrackDetailInteractors();
 }
 
 public Options showTrackOptions() {
 interactors.trackActions().perform(click());
 return goTo(new Options());
 } }

Slide 19

Slide 19 text

And part of screens… public static class Options extends BottomSheetScreen {
 
 public DialogScreen tapOnMakeTrackPrivateOption() {
 return tapOnOptionWithString(R.string.track_detail_action_private).goTo(dialog());
 }
 
 public DialogScreen tapOnMakeTrackPublicOption() {
 return tapOnOptionWithString(R.string.track_detail_action_public).goTo(dialog());
 }
 
 public UnknownScreen tapOnPlayTrackOption() {
 return tapOnOptionWithString(R.string.track_detail_action_play) .goTo(new UnknownScreen());
 }
 
 }

Slide 20

Slide 20 text

And part of screens… public static class Options extends BottomSheetScreen {
 
 public DialogScreen tapOnMakeTrackPrivateOption() {
 return tapOnOptionWithString(R.string.track_detail_action_private).goTo(dialog());
 }
 
 public DialogScreen tapOnMakeTrackPublicOption() {
 return tapOnOptionWithString(R.string.track_detail_action_public).goTo(dialog());
 }
 
 public UnknownScreen tapOnPlayTrackOption() {
 return tapOnOptionWithString(R.string.track_detail_action_play) .goTo(new UnknownScreen());
 }
 
 }

Slide 21

Slide 21 text

As well as dialogs… public class DialogScreen extends Screen {
 
 public Screen tapOnConfirmInDialog() {
 return tapOnButton(android.R.id.button1);
 }
 
 public Screen tapOnCancelInDialog() {
 return tapOnButton(android.R.id.button2);
 }
 
 protected Screen tapOnButton(@IdRes int buttonId) {
 onView(withId(buttonId)).perform(click());
 return previousScreen();
 }
 
 }

Slide 22

Slide 22 text

Interactors public class TrackDetailInteractors {
 
 BetterViewInteraction trackActions() {
 return BetterViewInteraction.wrap(onView(withId(R.id.track_detail_actions_more)));
 }
 
 BetterViewInteraction privateIndicator() {
 return BetterViewInteraction.wrap(onView(withText(R.string.track_item_private)));
 }
 
 RecyclerViewScreen recyclerView() {
 return new RecyclerViewScreen(allOf(
 withId(R.id.ak_recycler_view),
 isDescendantOfA(withId(R.id.track_detail_container))));
 } 
 }

Slide 23

Slide 23 text

Assertions public class TrackDetailAssertions {
 
 private final TrackDetailInteractors interactors;
 
 private TrackDetailAssertions(TrackDetailInteractors interactors) {
 this.interactors = interactors;
 }
 
 public static TrackDetailAssertions assertTrackDetail() {
 return new TrackDetailAssertions(new TrackDetailInteractors());
 }
 
 public TrackDetailAssertions hasPrivacySetToPrivate() {
 interactors.privateIndicator()
 .check(matches(isDisplayed()), "Track is not displayed as being private.");
 return this;
 } }

Slide 24

Slide 24 text

@Test
 public void shouldChangeTrackToPrivateInTrackDetails() {
 givenPublicTrackDetail();
 
 trackDetailScreen.showTrackOptions()
 .tapOnMakeTrackPrivateOption()
 .tapOnConfirmInDialog();
 
 assertTrackDetail().hasPrivacySetToPrivate();
 }

Slide 25

Slide 25 text

@Test
 public void shouldChangeTrackToPrivateInTrackDetails() {
 givenPublicTrackDetail();
 
 trackDetailScreen.showTrackOptions()
 .tapOnMakeTrackPrivateOption()
 .tapOnConfirmInDialog();
 
 assertTrackDetail().hasPrivacySetToPrivate();
 }

Slide 26

Slide 26 text

@Test
 public void shouldChangeTrackToPrivateInTrackDetails() {
 givenPublicTrackDetail();
 
 trackDetailScreen.showTrackOptions()
 .tapOnMakeTrackPrivateOption()
 .tapOnConfirmInDialog();
 
 assertTrackDetail().hasPrivacySetToPrivate();
 }

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

INSTRUMENTATION TESTING ROBOTS https://realm.io/news/kau-jake-wharton-testing-robots/

Slide 29

Slide 29 text

CONFIGURING

Slide 30

Slide 30 text

Test classes Single screen Event propagation Navigation

Slide 31

Slide 31 text

Test class “header” @Rule public final ActivityTestRule activityTestRule = new ActivityTestRule<>(UserPreviewActivity.class, true, false); @ClassRule public static final TestRule chain = RuleChain.outerRule(AccountTestRule.add(POPULAR_CREATOR)) .around(applicationComponentTestRule()) .around(new IdlingResourceTestRule(rxIdlingResource));

Slide 32

Slide 32 text

Test class “header” @Rule public final ActivityTestRule activityTestRule = new ActivityTestRule<>(UserPreviewActivity.class, true, false); @ClassRule public static final TestRule chain = RuleChain.outerRule(AccountTestRule.add(POPULAR_CREATOR)) .around(applicationComponentTestRule()) .around(new IdlingResourceTestRule(rxIdlingResource));

Slide 33

Slide 33 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 34

Slide 34 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 35

Slide 35 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 36

Slide 36 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 37

Slide 37 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 38

Slide 38 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 39

Slide 39 text

starting account rule starting application component rule starting idling resource rule execute all tests finished idling resource rule finished application component rule finished account rule

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

LocaleTestRule @Override
 public Statement apply(final Statement base, Description description) {
 return new Statement() {
 @Override
 public void evaluate() throws Throwable {
 Locale defaultLocale = Locale.getDefault();
 for (Locale locale : locales) {
 changeLocaleTo(locale);
 base.evaluate();
 }
 changeLocaleTo(defaultLocale);
 }
 };
 }

Slide 42

Slide 42 text

LocaleTestRule @Override
 public Statement apply(final Statement base, Description description) {
 return new Statement() {
 @Override
 public void evaluate() throws Throwable {
 Locale defaultLocale = Locale.getDefault();
 for (Locale locale : locales) {
 changeLocaleTo(locale);
 base.evaluate();
 }
 changeLocaleTo(defaultLocale);
 }
 };
 }

Slide 43

Slide 43 text

LocaleTestRule @Override
 public Statement apply(final Statement base, Description description) {
 return new Statement() {
 @Override
 public void evaluate() throws Throwable {
 Locale defaultLocale = Locale.getDefault();
 for (Locale locale : locales) {
 changeLocaleTo(locale);
 base.evaluate();
 }
 changeLocaleTo(defaultLocale);
 }
 };
 }

Slide 44

Slide 44 text

LocaleTestRule @Override
 public Statement apply(final Statement base, Description description) {
 return new Statement() {
 @Override
 public void evaluate() throws Throwable {
 Locale defaultLocale = Locale.getDefault();
 for (Locale locale : locales) {
 changeLocaleTo(locale);
 base.evaluate();
 }
 changeLocaleTo(defaultLocale);
 }
 };
 }

Slide 45

Slide 45 text

AccountTestRule Add / Remove account from the AccountManager Test app with popular / normal / empty user Test Sign-in

Slide 46

Slide 46 text

ApplicationComponentTestRule Swap dagger graph Disable crash logs / tracking Override components

Slide 47

Slide 47 text

By default, Espresso waits for UI events in the current message queue to be handled and for default AsyncTasks to complete before it moves on to the next test operation. There are instances where applications perform background operations (such as communicating with web services) via non-standard means; for example: direct creation and management of threads. In such cases, you have to use Idling Resources to inform Espresso of the app’s long-running operations.

Slide 48

Slide 48 text

By default, Espresso waits for UI events in the current message queue to be handled and for default AsyncTasks to complete before it moves on to the next test operation. There are instances where applications perform background operations (such as communicating with web services) via non-standard means; for example: direct creation and management of threads. In such cases, you have to use Idling Resources to inform Espresso of the app’s long-running operations.

Slide 49

Slide 49 text

IdlingResourceTestRule Register / unregister idling resource before / after test(s) Used mostly in combination with RxIdlingResource

Slide 50

Slide 50 text

public class ExecutionStrategy implements Observable.Transformer {
 
 private final Scheduler subscribeOnScheduler;
 private final Scheduler observeOnScheduler;
 
 public ExecutionStrategy(Scheduler subscribeOnScheduler, Scheduler observeOnScheduler) {
 this.subscribeOnScheduler = subscribeOnScheduler;
 this.observeOnScheduler = observeOnScheduler;
 }
 
 @Override
 public Observable call(Observable observable) {
 return observable
 .subscribeOn(subscribeOnScheduler)
 .observeOn(observeOnScheduler);
 }
 
 }

Slide 51

Slide 51 text

public class ExecutionStrategy implements Observable.Transformer {
 
 private final Scheduler subscribeOnScheduler;
 private final Scheduler observeOnScheduler;
 
 public ExecutionStrategy(Scheduler subscribeOnScheduler, Scheduler observeOnScheduler) {
 this.subscribeOnScheduler = subscribeOnScheduler;
 this.observeOnScheduler = observeOnScheduler;
 }
 
 @Override
 public Observable call(Observable observable) {
 return observable
 .subscribeOn(subscribeOnScheduler)
 .observeOn(observeOnScheduler);
 }
 
 }

Slide 52

Slide 52 text

RxIdlingResource public ExecutionStrategy wrap(ExecutionStrategy strategy) {
 
 return new ExecutionStrategy() {
 @Override
 public Observable call(Observable observable) {
 return wrap(executionStrategy.call(observable));
 }
 }; 
 }

Slide 53

Slide 53 text

RxIdlingResource public ExecutionStrategy wrap(ExecutionStrategy strategy) {
 
 return new ExecutionStrategy() {
 @Override
 public Observable call(Observable observable) {
 return wrap(executionStrategy.call(observable));
 }
 }; 
 }

Slide 54

Slide 54 text

RxIdlingResource public Observable wrap(final Observable observable) {
 return observable
 .doOnSubscribe(new Action0() {
 public void call() {
 isIdle.compareAndSet(true, false);
 }
 })
 .finallyDo(new Action0() {
 public void call() {
 if (isIdle.compareAndSet(false, true)) {
 if (resourceCallback != null) {
 resourceCallback.onTransitionToIdle();
 }
 }
 }
 });
 }

Slide 55

Slide 55 text

public Observable wrap(final Observable observable) {
 return observable
 .doOnSubscribe(new Action0() {
 public void call() {
 isIdle.compareAndSet(true, false);
 }
 })
 .finallyDo(new Action0() {
 public void call() {
 if (isIdle.compareAndSet(false, true)) {
 if (resourceCallback != null) {
 resourceCallback.onTransitionToIdle();
 }
 }
 }
 });
 } RxIdlingResource

Slide 56

Slide 56 text

public Observable wrap(final Observable observable) {
 return observable
 .doOnSubscribe(new Action0() {
 public void call() {
 isIdle.compareAndSet(true, false);
 }
 })
 .finallyDo(new Action0() {
 public void call() {
 if (isIdle.compareAndSet(false, true)) {
 if (resourceCallback != null) {
 resourceCallback.onTransitionToIdle();
 }
 }
 }
 });
 } RxIdlingResource

Slide 57

Slide 57 text

Rules are good for you Reusable Unclutter your tests from unnecessary details Separate configuration and actual tests

Slide 58

Slide 58 text

BETTER FAILURES

Slide 59

Slide 59 text

Making ViewInteraction great again! Wrap ViewInteraction Use a FailureHandler to provide a human explanation of what went wrong

Slide 60

Slide 60 text

Caused by: junit.framework.AssertionFailedError: 'is displayed on the screen to the user' doesn't match the selected view. Expected: is displayed on the screen to the user Got: "ab{id=2131689841, res-name=track_item_private_indicator, visibility=GONE, width=0, height=0, has-focus=false, has- focusable=false, has-window-focus=true, is-clickable=false, is- enabled=true, is-focused=false, is-focusable=false, is-layout- requested=true, is-selected=false, root-is-layout- requested=false, has-input-connection=false, x=0.0, y=0.0, text=Private, input-type=0, ime-target=false, has-links=false}” […]

Slide 61

Slide 61 text

public BetterViewInteraction check(ViewAssertion viewAssert, String failureMessage, Object... args) {
 viewInteraction.withFailureHandler(new FailureHandler() {
 @Override
 public void handle(Throwable t, Matcher viewMatcher) { String errorMessage = String.format(failureMessage, args);
 Throwable error = new AssertionFailed(errorMessage, t);
 defaultFailureHandler.handle(error, viewMatcher);
 }
 }).check(viewAssert);
 return this;
 }

Slide 62

Slide 62 text

public BetterViewInteraction check(ViewAssertion viewAssert, String failureMessage, Object... args) {
 viewInteraction.withFailureHandler(new FailureHandler() {
 @Override
 public void handle(Throwable t, Matcher viewMatcher) { String errorMessage = String.format(failureMessage, args);
 Throwable error = new AssertionFailed(errorMessage, t);
 defaultFailureHandler.handle(error, viewMatcher);
 }
 }).check(viewAssert);
 return this;
 }

Slide 63

Slide 63 text

public BetterViewInteraction check(ViewAssertion viewAssert, String failureMessage, Object... args) {
 viewInteraction.withFailureHandler(new FailureHandler() {
 @Override
 public void handle(Throwable t, Matcher viewMatcher) { String errorMessage = String.format(failureMessage, args);
 Throwable error = new AssertionFailed(errorMessage, t);
 defaultFailureHandler.handle(error, viewMatcher);
 }
 }).check(viewAssert);
 return this;
 }

Slide 64

Slide 64 text

com.soundcloud.espresso.EspressoAssertionFailedError: Track is not displayed as being private. […] Caused by: junit.framework.AssertionFailedError: 'is displayed on the screen to the user' doesn't match the selected view. Expected: is displayed on the screen to the user Got: "ab{id=2131689841, res-name=track_item_private_indicator, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, root-is- layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=Private, input-type=0, ime-target=false, has-links=false}” […]

Slide 65

Slide 65 text

GOING DEEPER

Slide 66

Slide 66 text

Feature flags Based on the flavour Developing continuously Switching a flag when we want to release the feature

Slide 67

Slide 67 text

GREAT BUT… HOW DO WE DEAL WITH TESTING BOTH NEW & OLD BEHAVIOUR?

Slide 68

Slide 68 text

A CUSTOM TEST RUNNER! OR, HOW TO SKIP TESTS BASED ON A PREDICATE

Slide 69

Slide 69 text

@RunWhen( value = SomePredicate.class, expected = false)


Slide 70

Slide 70 text

public final class SomeFeatureFlagEnabled extends Predicate { 
 @Override
 public boolean evaluate() {
 return featureFlags().someFeature();
 }
 
 @Override
 public String description() {
 return “Some feature flag enabled";
 } 
 }

Slide 71

Slide 71 text

public class SkippableAndroidJUnit4 extends BlockJUnit4ClassRunner {
 
 @Override
 protected Statement methodBlock(FrameworkMethod testMethod) {
 Statement base = super.methodBlock(testMethod);
 Statement statement = applyRunWhenTo(testMethod.getMethod(), base);
 
 Class> clazz = testMethod.getDeclaringClass();
 do {
 statement = applyRunWhenTo(clazz, statement);
 } while ((clazz = clazz.getSuperclass()) != null);
 return statement;
 }
 
 private Statement applyRunWhenTo(AnnotatedElement element, final Statement base) {
 RunWhen annotation = element.getAnnotation(RunWhen.class);
 if (annotation == null) {
 return base;
 }
 return new PredicateStatement(base, annotation.value(), annotation.expected());
 }
 
 }

Slide 72

Slide 72 text

public class SkippableAndroidJUnit4 extends BlockJUnit4ClassRunner {
 
 @Override
 protected Statement methodBlock(FrameworkMethod testMethod) {
 Statement base = super.methodBlock(testMethod);
 Statement statement = applyRunWhenTo(testMethod.getMethod(), base);
 
 Class> clazz = testMethod.getDeclaringClass();
 do {
 statement = applyRunWhenTo(clazz, statement);
 } while ((clazz = clazz.getSuperclass()) != null);
 return statement;
 }
 
 private Statement applyRunWhenTo(AnnotatedElement element, final Statement base) {
 RunWhen annotation = element.getAnnotation(RunWhen.class);
 if (annotation == null) {
 return base;
 }
 return new PredicateStatement(base, annotation.value(), annotation.expected());
 }
 
 }

Slide 73

Slide 73 text

class PredicateStatement extends Statement {
 private final Statement base;
 private final Class extends Predicate> predicateClass;
 private final Boolean expected;
 
 public PredicateStatement(Statement base, Class extends Predicate> predicateClass, Boolean expected) {
 this.base = base;
 this.predicateClass = predicateClass;
 this.expected = expected;
 }
 
 @Override
 public void evaluate() throws Throwable {
 Predicate predicate = PredicateBuilder.build(predicateClass);
 assumeThat(predicate.description(), predicate.evaluate(), Matchers.is(expected));
 base.evaluate();
 }
 }

Slide 74

Slide 74 text

class PredicateStatement extends Statement {
 private final Statement base;
 private final Class extends Predicate> predicateClass;
 private final Boolean expected;
 
 public PredicateStatement(Statement base, Class extends Predicate> predicateClass, Boolean expected) {
 this.base = base;
 this.predicateClass = predicateClass;
 this.expected = expected;
 }
 
 @Override
 public void evaluate() throws Throwable {
 Predicate predicate = PredicateBuilder.build(predicateClass);
 assumeThat(predicate.description(), predicate.evaluate(), Matchers.is(expected));
 base.evaluate();
 }
 }

Slide 75

Slide 75 text

GRADLE

Slide 76

Slide 76 text

def unlockAll = project.task('unlockAllDevices')
 
 command.devices().eachWithIndex { device, index ->
 
 def unlocker = project.task("unlockDevice${index}", type: ForceDeviceUnlock) {
 deviceId = device.id
 }
 unlockAll.dependsOn unlocker
 }
 
 project.afterEvaluate {
 project.tasks.findAll {
 it.name.startsWith('connected') && it.name.endsWith('AndroidTest')
 }*.dependsOn(unlockAll)
 }

Slide 77

Slide 77 text

def unlockAll = project.task('unlockAllDevices')
 
 command.devices().eachWithIndex { device, index ->
 
 def unlocker = project.task("unlockDevice${index}", type: ForceDeviceUnlock) {
 deviceId = device.id
 }
 unlockAll.dependsOn unlocker
 }
 
 project.afterEvaluate {
 project.tasks.findAll {
 it.name.startsWith('connected') && it.name.endsWith('AndroidTest')
 }*.dependsOn(unlockAll)
 }

Slide 78

Slide 78 text

class ForceDeviceUnlock extends AdbTask {
 
 @TaskAction
 void unlockDevice() {
 println 'Trying to unlock device using UnlockHelperApp (if installed)'
 def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']
 assertDeviceAndRunCommand(startHelperAppCommand)
 
 println 'Trying to unlock device using unlock keycode'
 def pressUnlockKey = ['shell', 'input', 'keyevent', '82']
 assertDeviceAndRunCommand(pressUnlockKey)
 
 println 'Sending "home" keycode to reset the screen'
 def pressHomeKey = ['shell', 'input', 'keyevent', '3']
 assertDeviceAndRunCommand(pressHomeKey)
 }
 }

Slide 79

Slide 79 text

class ForceDeviceUnlock extends AdbTask {
 
 @TaskAction
 void unlockDevice() {
 println 'Trying to unlock device using UnlockHelperApp (if installed)'
 def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']
 assertDeviceAndRunCommand(startHelperAppCommand)
 
 println 'Trying to unlock device using unlock keycode'
 def pressUnlockKey = ['shell', 'input', 'keyevent', '82']
 assertDeviceAndRunCommand(pressUnlockKey)
 
 println 'Sending "home" keycode to reset the screen'
 def pressHomeKey = ['shell', 'input', 'keyevent', '3']
 assertDeviceAndRunCommand(pressHomeKey)
 }
 }

Slide 80

Slide 80 text

class ForceDeviceUnlock extends AdbTask {
 
 @TaskAction
 void unlockDevice() {
 println 'Trying to unlock device using UnlockHelperApp (if installed)'
 def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']
 assertDeviceAndRunCommand(startHelperAppCommand)
 
 println 'Trying to unlock device using unlock keycode'
 def pressUnlockKey = ['shell', 'input', 'keyevent', '82']
 assertDeviceAndRunCommand(pressUnlockKey)
 
 println 'Sending "home" keycode to reset the screen'
 def pressHomeKey = ['shell', 'input', 'keyevent', '3']
 assertDeviceAndRunCommand(pressHomeKey)
 }
 }

Slide 81

Slide 81 text

class ForceDeviceUnlock extends AdbTask {
 
 @TaskAction
 void unlockDevice() {
 println 'Trying to unlock device using UnlockHelperApp (if installed)'
 def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']
 assertDeviceAndRunCommand(startHelperAppCommand)
 
 println 'Trying to unlock device using unlock keycode'
 def pressUnlockKey = ['shell', 'input', 'keyevent', '82']
 assertDeviceAndRunCommand(pressUnlockKey)
 
 println 'Sending "home" keycode to reset the screen'
 def pressHomeKey = ['shell', 'input', 'keyevent', '3']
 assertDeviceAndRunCommand(pressHomeKey)
 }
 }

Slide 82

Slide 82 text

DEVICE GOTCHAS

Slide 83

Slide 83 text

“Ok Google” Autocomplete Updating ADB Update Dialog ☀ &

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

QUESTIONS? @florianmski