1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/tests/helpers/WaitHelper.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,171 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.tests.helpers; 1.9 + 1.10 +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; 1.11 +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; 1.12 + 1.13 +import java.util.regex.Pattern; 1.14 + 1.15 +import org.mozilla.gecko.Actions; 1.16 +import org.mozilla.gecko.Actions.EventExpecter; 1.17 +import org.mozilla.gecko.tests.UITestContext; 1.18 +import org.mozilla.gecko.tests.UITestContext.ComponentType; 1.19 +import org.mozilla.gecko.tests.components.ToolbarComponent; 1.20 + 1.21 +import com.jayway.android.robotium.solo.Condition; 1.22 +import com.jayway.android.robotium.solo.Solo; 1.23 + 1.24 +/** 1.25 + * Provides functionality related to waiting on certain events to happen. 1.26 + */ 1.27 +public final class WaitHelper { 1.28 + // TODO: Make public for when Solo.waitForCondition is used directly (i.e. do not want 1.29 + // assertion from waitFor)? 1.30 + private static final int DEFAULT_MAX_WAIT_MS = 5000; 1.31 + private static final int PAGE_LOAD_WAIT_MS = 10000; 1.32 + private static final int CHANGE_WAIT_MS = 10000; 1.33 + 1.34 + // TODO: via lucasr - Add ThrobberVisibilityChangeVerifier? 1.35 + private static final ChangeVerifier[] PAGE_LOAD_VERIFIERS = new ChangeVerifier[] { 1.36 + new ToolbarTitleTextChangeVerifier() 1.37 + }; 1.38 + 1.39 + private static UITestContext sContext; 1.40 + private static Solo sSolo; 1.41 + private static Actions sActions; 1.42 + 1.43 + private static ToolbarComponent sToolbar; 1.44 + 1.45 + private WaitHelper() { /* To disallow instantiation. */ } 1.46 + 1.47 + protected static void init(final UITestContext context) { 1.48 + sContext = context; 1.49 + sSolo = context.getSolo(); 1.50 + sActions = context.getActions(); 1.51 + 1.52 + sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR); 1.53 + } 1.54 + 1.55 + /** 1.56 + * Waits for the given {@link solo.Condition} using the default wait duration; will throw an 1.57 + * AssertionError if the duration is elapsed and the condition is not satisfied. 1.58 + */ 1.59 + public static void waitFor(String message, final Condition condition) { 1.60 + message = "Waiting for " + message + "."; 1.61 + fAssertTrue(message, sSolo.waitForCondition(condition, DEFAULT_MAX_WAIT_MS)); 1.62 + } 1.63 + 1.64 + /** 1.65 + * Waits for the given {@link solo.Condition} using the given wait duration; will throw an 1.66 + * AssertionError if the duration is elapsed and the condition is not satisfied. 1.67 + */ 1.68 + public static void waitFor(String message, final Condition condition, final int waitMillis) { 1.69 + message = "Waiting for " + message + " with timeout " + waitMillis + "."; 1.70 + fAssertTrue(message, sSolo.waitForCondition(condition, waitMillis)); 1.71 + } 1.72 + 1.73 + /** 1.74 + * Waits for the Gecko event declaring the page has loaded. Takes in and runs a Runnable 1.75 + * that will perform the action that will cause the page to load. 1.76 + */ 1.77 + public static void waitForPageLoad(final Runnable initiatingAction) { 1.78 + fAssertNotNull("initiatingAction is not null", initiatingAction); 1.79 + 1.80 + // Some changes to the UI occur in response to the same event we listen to for when 1.81 + // the page has finished loading (e.g. a page title update). As such, we ensure this 1.82 + // UI state has changed before returning from this method; here we store the initial 1.83 + // state. 1.84 + final ChangeVerifier[] pageLoadVerifiers = PAGE_LOAD_VERIFIERS; 1.85 + for (final ChangeVerifier verifier : pageLoadVerifiers) { 1.86 + verifier.storeState(); 1.87 + } 1.88 + 1.89 + // Wait for the page load and title changed event. 1.90 + final EventExpecter contentEventExpecter = sActions.expectGeckoEvent("DOMContentLoaded"); 1.91 + final EventExpecter titleEventExpecter = sActions.expectGeckoEvent("DOMTitleChanged"); 1.92 + 1.93 + initiatingAction.run(); 1.94 + 1.95 + contentEventExpecter.blockForEventDataWithTimeout(PAGE_LOAD_WAIT_MS); 1.96 + contentEventExpecter.unregisterListener(); 1.97 + titleEventExpecter.blockForEventDataWithTimeout(PAGE_LOAD_WAIT_MS); 1.98 + titleEventExpecter.unregisterListener(); 1.99 + 1.100 + // Verify remaining state has changed. 1.101 + for (final ChangeVerifier verifier : pageLoadVerifiers) { 1.102 + // If we timeout, either the state is set to the same value (which is fine), or 1.103 + // the state has not yet changed. Since we can't be sure it will ever change, move 1.104 + // on and let the assertions fail if applicable. 1.105 + final boolean hasTimedOut = !sSolo.waitForCondition(new Condition() { 1.106 + @Override 1.107 + public boolean isSatisfied() { 1.108 + return verifier.hasStateChanged(); 1.109 + } 1.110 + }, CHANGE_WAIT_MS); 1.111 + 1.112 + sContext.dumpLog(verifier.getLogTag(), 1.113 + (hasTimedOut ? "timed out." : "was satisfied.")); 1.114 + } 1.115 + } 1.116 + 1.117 + /** 1.118 + * Implementations of this interface verify that the state of the test has changed from 1.119 + * the invocation of storeState to the invocation of hasStateChanged. A boolean will be 1.120 + * returned from hasStateChanged, indicating this change of status. 1.121 + */ 1.122 + private static interface ChangeVerifier { 1.123 + public String getLogTag(); 1.124 + 1.125 + /** 1.126 + * Stores the initial state of the system. This system state is used to diff against 1.127 + * the end state to determine if the system has changed. Since this is just a diff 1.128 + * (with a timeout), this method could potentially store state inconsistent with 1.129 + * what is visible to the user. 1.130 + */ 1.131 + public void storeState(); 1.132 + public boolean hasStateChanged(); 1.133 + } 1.134 + 1.135 + private static class ToolbarTitleTextChangeVerifier implements ChangeVerifier { 1.136 + private static final String LOGTAG = ToolbarTitleTextChangeVerifier.class.getSimpleName(); 1.137 + 1.138 + // A regex that matches the page title that shows up while the page is loading. 1.139 + private static final Pattern LOADING_PREFIX = Pattern.compile("[A-Za-z]{3,9}://"); 1.140 + 1.141 + private CharSequence mOldTitleText; 1.142 + 1.143 + @Override 1.144 + public String getLogTag() { 1.145 + return LOGTAG; 1.146 + } 1.147 + 1.148 + @Override 1.149 + public void storeState() { 1.150 + mOldTitleText = sToolbar.getPotentiallyInconsistentTitle(); 1.151 + sContext.dumpLog(LOGTAG, "stored title, \"" + mOldTitleText + "\"."); 1.152 + } 1.153 + 1.154 + @Override 1.155 + public boolean hasStateChanged() { 1.156 + // TODO: Additionally, consider Solo.waitForText. 1.157 + // TODO: Robocop sleeps .5 sec between calls. Cache title view? 1.158 + final CharSequence title = sToolbar.getPotentiallyInconsistentTitle(); 1.159 + 1.160 + // TODO: Handle the case where the URL is shown instead of page title by preference. 1.161 + // HACK: We want to wait until the title changes to the state a tester may assert 1.162 + // (e.g. the page title). However, the title is set to the URL before the title is 1.163 + // loaded from the server and set as the final page title; we ignore the 1.164 + // intermediate URL loading state here. 1.165 + final boolean isLoading = LOADING_PREFIX.matcher(title).lookingAt(); 1.166 + final boolean hasStateChanged = !isLoading && !mOldTitleText.equals(title); 1.167 + 1.168 + if (hasStateChanged) { 1.169 + sContext.dumpLog(LOGTAG, "state changed to title, \"" + title + "\"."); 1.170 + } 1.171 + return hasStateChanged; 1.172 + } 1.173 + } 1.174 +}