GeckoThread.LaunchState now covers the entire GeckoThread lifetime and not just launch, so it's renamed to GeckoThread.State. More utility methods are added to check for the current state.
1017 lines
37 KiB
Java
1017 lines
37 KiB
Java
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.gecko.tests;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.PrintWriter;
|
|
import java.io.StringWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import org.mozilla.gecko.Actions;
|
|
import org.mozilla.gecko.Element;
|
|
import org.mozilla.gecko.GeckoAppShell;
|
|
import org.mozilla.gecko.GeckoEvent;
|
|
import org.mozilla.gecko.GeckoProfile;
|
|
import org.mozilla.gecko.GeckoThread;
|
|
import org.mozilla.gecko.R;
|
|
import org.mozilla.gecko.RobocopUtils;
|
|
import org.mozilla.gecko.Tab;
|
|
import org.mozilla.gecko.Tabs;
|
|
|
|
import android.app.Activity;
|
|
import android.content.ContentValues;
|
|
import android.content.Intent;
|
|
import android.content.pm.ActivityInfo;
|
|
import android.content.res.AssetManager;
|
|
import android.content.res.Resources;
|
|
import android.database.Cursor;
|
|
import android.os.Build;
|
|
import android.os.SystemClock;
|
|
import android.support.v4.app.Fragment;
|
|
import android.support.v4.app.FragmentActivity;
|
|
import android.support.v4.app.FragmentManager;
|
|
import android.text.TextUtils;
|
|
import android.util.DisplayMetrics;
|
|
import android.view.View;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.widget.AdapterView;
|
|
import android.widget.Button;
|
|
import android.widget.EditText;
|
|
import android.widget.ListAdapter;
|
|
import android.widget.TextView;
|
|
|
|
import com.jayway.android.robotium.solo.Condition;
|
|
import com.jayway.android.robotium.solo.Solo;
|
|
import com.jayway.android.robotium.solo.Timeout;
|
|
|
|
/**
|
|
* A convenient base class suitable for most Robocop tests.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
abstract class BaseTest extends BaseRobocopTest {
|
|
private static final int VERIFY_URL_TIMEOUT = 2000;
|
|
private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
|
|
private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
|
|
private static final int MAX_WAIT_VERIFY_PAGE_TITLE_MS = 15000;
|
|
public static final int MAX_WAIT_MS = 4500;
|
|
public static final int LONG_PRESS_TIME = 6000;
|
|
private static final int GECKO_READY_WAIT_MS = 180000;
|
|
public static final int MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS = 90000;
|
|
|
|
protected static final String URL_HTTP_PREFIX = "http://";
|
|
|
|
private int mPreferenceRequestID = 0;
|
|
public Device mDevice;
|
|
protected DatabaseHelper mDatabaseHelper;
|
|
protected int mScreenMidWidth;
|
|
protected int mScreenMidHeight;
|
|
private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
|
|
|
|
protected void blockForDelayedStartup() {
|
|
try {
|
|
Actions.EventExpecter delayedStartupExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup");
|
|
delayedStartupExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
|
|
delayedStartupExpector.unregisterListener();
|
|
} catch (Exception e) {
|
|
mAsserter.dumpLog("Exception in blockForDelayedStartup", e);
|
|
}
|
|
}
|
|
|
|
protected void blockForGeckoReady() {
|
|
try {
|
|
Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready");
|
|
if (!GeckoThread.isRunning()) {
|
|
geckoReadyExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
|
|
}
|
|
geckoReadyExpector.unregisterListener();
|
|
} catch (Exception e) {
|
|
mAsserter.dumpLog("Exception in blockForGeckoReady", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setUp() throws Exception {
|
|
super.setUp();
|
|
|
|
mDevice = new Device();
|
|
mDatabaseHelper = new DatabaseHelper(getActivity(), mAsserter);
|
|
|
|
// Ensure Robocop tests have access to network, and are run with Display powered on.
|
|
throwIfHttpGetFails();
|
|
throwIfScreenNotOn();
|
|
}
|
|
|
|
protected GeckoProfile getTestProfile() {
|
|
if (mProfile.startsWith("/")) {
|
|
return GeckoProfile.get(getActivity(), "default", mProfile);
|
|
}
|
|
|
|
return GeckoProfile.get(getActivity(), mProfile);
|
|
}
|
|
|
|
protected void initializeProfile() {
|
|
final GeckoProfile profile = getTestProfile();
|
|
|
|
// In Robocop tests, we typically don't get initialized correctly, because
|
|
// GeckoProfile doesn't create the profile directory.
|
|
profile.enqueueInitialization(profile.getDir());
|
|
}
|
|
|
|
@Override
|
|
protected void runTest() throws Throwable {
|
|
try {
|
|
super.runTest();
|
|
} catch (Throwable t) {
|
|
// save screenshot -- written to /mnt/sdcard/Robotium-Screenshots
|
|
// as <filename>.jpg
|
|
mSolo.takeScreenshot("robocop-screenshot-"+getClass().getName());
|
|
if (mAsserter != null) {
|
|
mAsserter.dumpLog("Exception caught during test!", t);
|
|
mAsserter.ok(false, "Exception caught", t.toString());
|
|
}
|
|
// re-throw to continue bail-out
|
|
throw t;
|
|
}
|
|
}
|
|
|
|
public void assertMatches(String value, String regex, String name) {
|
|
if (value == null) {
|
|
mAsserter.ok(false, name, "Expected /" + regex + "/, got null");
|
|
return;
|
|
}
|
|
mAsserter.ok(value.matches(regex), name, "Expected /" + regex +"/, got \"" + value + "\"");
|
|
}
|
|
|
|
/**
|
|
* Click on the URL bar to focus it and enter editing mode.
|
|
*/
|
|
protected final void focusUrlBar() {
|
|
// Click on the browser toolbar to enter editing mode
|
|
mSolo.waitForView(R.id.browser_toolbar);
|
|
final View toolbarView = mSolo.getView(R.id.browser_toolbar);
|
|
mSolo.clickOnView(toolbarView);
|
|
|
|
// Wait for highlighed text to gain focus
|
|
boolean success = waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
mSolo.waitForView(R.id.url_edit_text);
|
|
EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text);
|
|
if (urlEditText.isInputMethodTarget()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}, MAX_WAIT_ENABLED_TEXT_MS);
|
|
|
|
mAsserter.ok(success, "waiting for urlbar text to gain focus", "urlbar text gained focus");
|
|
}
|
|
|
|
protected final void enterUrl(String url) {
|
|
focusUrlBar();
|
|
|
|
final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text);
|
|
|
|
// Send the keys for the URL we want to enter
|
|
mSolo.clearEditText(urlEditView);
|
|
mSolo.typeText(urlEditView, url);
|
|
|
|
// Get the URL text from the URL bar EditText view
|
|
final String urlBarText = urlEditView.getText().toString();
|
|
mAsserter.is(url, urlBarText, "URL typed properly");
|
|
}
|
|
|
|
protected final Fragment getBrowserSearch() {
|
|
final FragmentManager fm = ((FragmentActivity) getActivity()).getSupportFragmentManager();
|
|
return fm.findFragmentByTag("browser_search");
|
|
}
|
|
|
|
protected final void hitEnterAndWait() {
|
|
Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
|
|
mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
|
|
// wait for screen to load
|
|
contentEventExpecter.blockForEvent();
|
|
contentEventExpecter.unregisterListener();
|
|
}
|
|
|
|
/**
|
|
* Load <code>url</code> by sending key strokes to the URL bar UI.
|
|
*
|
|
* This method waits synchronously for the <code>DOMContentLoaded</code>
|
|
* message from Gecko before returning.
|
|
*
|
|
* Unless you need to test text entry in the url bar, consider using loadUrl
|
|
* instead -- it loads pages more directly and quickly.
|
|
*/
|
|
protected final void inputAndLoadUrl(String url) {
|
|
enterUrl(url);
|
|
hitEnterAndWait();
|
|
}
|
|
|
|
/**
|
|
* Load <code>url</code> using the internal
|
|
* <code>org.mozilla.gecko.Tabs</code> API.
|
|
*
|
|
* This method does not wait for any confirmation from Gecko before
|
|
* returning -- consider using verifyUrlBarTitle or a similar approach
|
|
* to wait for the page to load, or at least use loadUrlAndWait.
|
|
*/
|
|
protected final void loadUrl(final String url) {
|
|
try {
|
|
Tabs.getInstance().loadUrl(url);
|
|
} catch (Exception e) {
|
|
mAsserter.dumpLog("Exception in loadUrl", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load <code>url</code> using the internal
|
|
* <code>org.mozilla.gecko.Tabs</code> API and wait for DOMContentLoaded.
|
|
*/
|
|
protected final void loadUrlAndWait(final String url) {
|
|
Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
|
|
loadUrl(url);
|
|
contentEventExpecter.blockForEvent();
|
|
contentEventExpecter.unregisterListener();
|
|
}
|
|
|
|
protected final void closeTab(int tabId) {
|
|
Tabs tabs = Tabs.getInstance();
|
|
Tab tab = tabs.getTab(tabId);
|
|
tabs.closeTab(tab);
|
|
}
|
|
|
|
public final void verifyUrl(String url) {
|
|
final EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text);
|
|
String urlBarText = null;
|
|
if (urlEditText != null) {
|
|
// wait for a short time for the expected text, in case there is a delay
|
|
// in updating the view
|
|
waitForCondition(new VerifyTextViewText(urlEditText, url), VERIFY_URL_TIMEOUT);
|
|
urlBarText = urlEditText.getText().toString();
|
|
|
|
}
|
|
mAsserter.is(urlBarText, url, "Browser toolbar URL stayed the same");
|
|
}
|
|
|
|
class VerifyTextViewText implements Condition {
|
|
private final TextView mTextView;
|
|
private final String mExpected;
|
|
public VerifyTextViewText(TextView textView, String expected) {
|
|
mTextView = textView;
|
|
mExpected = expected;
|
|
}
|
|
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
String textValue = mTextView.getText().toString();
|
|
return mExpected.equals(textValue);
|
|
}
|
|
}
|
|
|
|
protected final String getAbsoluteUrl(String url) {
|
|
return mBaseHostnameUrl + "/" + url.replaceAll("(^/)", "");
|
|
}
|
|
|
|
protected final String getAbsoluteRawUrl(String url) {
|
|
return mBaseIpUrl + "/" + url.replaceAll("(^/)", "");
|
|
}
|
|
|
|
/*
|
|
* Wrapper method for mSolo.waitForCondition with additional logging.
|
|
*/
|
|
protected final boolean waitForCondition(Condition condition, int timeout) {
|
|
boolean result = mSolo.waitForCondition(condition, timeout);
|
|
if (!result) {
|
|
// Log timeout failure for diagnostic purposes only; a failed wait may
|
|
// be normal and does not necessarily warrant a test assertion/failure.
|
|
mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms.");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
protected interface BooleanTest {
|
|
public boolean test();
|
|
}
|
|
|
|
public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) {
|
|
File profile = new File(mProfile);
|
|
String dbPath = new File(profile, dbName).getPath();
|
|
|
|
Cursor c = mActions.querySql(dbPath, sqlCommand);
|
|
SqliteCompare(c, cvs);
|
|
}
|
|
|
|
public void SqliteCompare(Cursor c, ContentValues[] cvs) {
|
|
mAsserter.is(c.getCount(), cvs.length, "List is correct length");
|
|
if (c.moveToFirst()) {
|
|
do {
|
|
boolean found = false;
|
|
for (int i = 0; !found && i < cvs.length; i++) {
|
|
if (CursorMatches(c, cvs[i])) {
|
|
found = true;
|
|
}
|
|
}
|
|
mAsserter.is(found, true, "Password was found");
|
|
} while (c.moveToNext());
|
|
}
|
|
}
|
|
|
|
public boolean CursorMatches(Cursor c, ContentValues cv) {
|
|
for (int i = 0; i < c.getColumnCount(); i++) {
|
|
String column = c.getColumnName(i);
|
|
if (cv.containsKey(column)) {
|
|
mAsserter.info("Comparing", "Column values for: " + column);
|
|
Object value = cv.get(column);
|
|
if (value == null) {
|
|
if (!c.isNull(i)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (c.isNull(i) || !value.toString().equals(c.getString(i))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public InputStream getAsset(String filename) throws IOException {
|
|
AssetManager assets = getInstrumentation().getContext().getAssets();
|
|
return assets.open(filename);
|
|
}
|
|
|
|
public boolean waitForText(final String text) {
|
|
// false is the default value for finding only
|
|
// visible views in `Solo.waitForText(String)`.
|
|
return waitForText(text, false);
|
|
}
|
|
|
|
public boolean waitForText(final String text, final boolean onlyVisibleViews) {
|
|
// We use the default robotium values from
|
|
// `Waiter.waitForText(String)` for unspecified arguments.
|
|
final boolean rc =
|
|
mSolo.waitForText(text, 0, Timeout.getLargeTimeout(), true, onlyVisibleViews);
|
|
if (!rc) {
|
|
// log out failed wait for diagnostic purposes only;
|
|
// waitForText failures are sometimes expected/normal
|
|
mAsserter.dumpLog("waitForText timeout on "+text);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
// waitForText usually scrolls down in a view when text is not visible.
|
|
// For PreferenceScreens and dialogs, Solo.waitForText scrolling does not
|
|
// work, so we use this hack to do the same thing.
|
|
protected boolean waitForPreferencesText(String txt) {
|
|
boolean foundText = waitForText(txt);
|
|
if (!foundText) {
|
|
if ((mScreenMidWidth == 0) || (mScreenMidHeight == 0)) {
|
|
mScreenMidWidth = mDriver.getGeckoWidth()/2;
|
|
mScreenMidHeight = mDriver.getGeckoHeight()/2;
|
|
}
|
|
|
|
// If we don't see the item, scroll down once in case it's off-screen.
|
|
// Hacky way to scroll down. solo.scroll* does not work in dialogs.
|
|
MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
|
|
meh.dragSync(mScreenMidWidth, mScreenMidHeight+100, mScreenMidWidth, mScreenMidHeight-100);
|
|
|
|
foundText = mSolo.waitForText(txt);
|
|
}
|
|
return foundText;
|
|
}
|
|
|
|
/**
|
|
* Wait for <text> to be visible and also be enabled/clickable.
|
|
*/
|
|
public boolean waitForEnabledText(String text) {
|
|
final String testText = text;
|
|
boolean rc = waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
// Solo.getText() could be used here, except that it sometimes
|
|
// hits an assertion when the requested text is not found.
|
|
ArrayList<View> views = mSolo.getCurrentViews();
|
|
for (View view : views) {
|
|
if (view instanceof TextView) {
|
|
TextView tv = (TextView)view;
|
|
String viewText = tv.getText().toString();
|
|
if (tv.isEnabled() && viewText != null && viewText.matches(testText)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}, MAX_WAIT_ENABLED_TEXT_MS);
|
|
if (!rc) {
|
|
// log out failed wait for diagnostic purposes only;
|
|
// failures are sometimes expected/normal
|
|
mAsserter.dumpLog("waitForEnabledText timeout on "+text);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
|
|
/**
|
|
* Select <item> from Menu > "Settings" > <section>.
|
|
*/
|
|
public void selectSettingsItem(String section, String item) {
|
|
String[] itemPath = { "Settings", section, item };
|
|
selectMenuItemByPath(itemPath);
|
|
}
|
|
|
|
/**
|
|
* Traverses the items in listItems in order in the menu.
|
|
*/
|
|
public void selectMenuItemByPath(String[] listItems) {
|
|
int listLength = listItems.length;
|
|
if (listLength > 0) {
|
|
selectMenuItem(listItems[0]);
|
|
}
|
|
if (listLength > 1) {
|
|
for (int i = 1; i < listLength; i++) {
|
|
String itemName = "^" + listItems[i] + "$";
|
|
mAsserter.ok(waitForPreferencesText(itemName), "Waiting for and scrolling once to find item " + itemName, itemName + " found");
|
|
mAsserter.ok(waitForEnabledText(itemName), "Waiting for enabled text " + itemName, itemName + " option is present and enabled");
|
|
mSolo.clickOnText(itemName);
|
|
}
|
|
}
|
|
}
|
|
|
|
public final void selectMenuItem(String menuItemName) {
|
|
// build the item name ready to be used
|
|
String itemName = "^" + menuItemName + "$";
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
if (waitForText(itemName, true)) {
|
|
mSolo.clickOnText(itemName);
|
|
} else {
|
|
// Older versions of Android have additional settings under "More",
|
|
// including settings that newer versions have under "Tools."
|
|
if (mSolo.searchText("(^More$|^Tools$)")) {
|
|
mSolo.clickOnText("(^More$|^Tools$)");
|
|
}
|
|
waitForText(itemName);
|
|
mSolo.clickOnText(itemName);
|
|
}
|
|
}
|
|
|
|
public final void verifyHomePagerHidden() {
|
|
final View homePagerContainer = mSolo.getView(R.id.home_pager_container);
|
|
|
|
boolean rc = waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
return homePagerContainer.getVisibility() != View.VISIBLE;
|
|
}
|
|
}, MAX_WAIT_HOME_PAGER_HIDDEN_MS);
|
|
|
|
if (!rc) {
|
|
mAsserter.ok(rc, "Verify HomePager is hidden", "HomePager is hidden");
|
|
}
|
|
}
|
|
|
|
public final void verifyUrlBarTitle(String url) {
|
|
mAsserter.isnot(url, null, "The url argument is not null");
|
|
|
|
final String expected;
|
|
if (mStringHelper.ABOUT_HOME_URL.equals(url)) {
|
|
expected = mStringHelper.ABOUT_HOME_TITLE;
|
|
} else if (url.startsWith(URL_HTTP_PREFIX)) {
|
|
expected = url.substring(URL_HTTP_PREFIX.length());
|
|
} else {
|
|
expected = url;
|
|
}
|
|
|
|
final TextView urlBarTitle = (TextView) mSolo.getView(R.id.url_bar_title);
|
|
String pageTitle = null;
|
|
if (urlBarTitle != null) {
|
|
// Wait for the title to make sure it has been displayed in case the view
|
|
// does not update fast enough
|
|
waitForCondition(new VerifyTextViewText(urlBarTitle, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS);
|
|
pageTitle = urlBarTitle.getText().toString();
|
|
}
|
|
mAsserter.is(pageTitle, expected, "Page title is correct");
|
|
}
|
|
|
|
public final void verifyTabCount(int expectedTabCount) {
|
|
Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter);
|
|
String tabCountText = tabCount.getText();
|
|
int tabCountInt = Integer.parseInt(tabCountText);
|
|
mAsserter.is(tabCountInt, expectedTabCount, "The correct number of tabs are opened");
|
|
}
|
|
|
|
public void verifyPinned(final boolean isPinned, final String gridItemTitle) {
|
|
boolean viewFound = waitForText(gridItemTitle);
|
|
mAsserter.ok(viewFound, "Found top site title: " + gridItemTitle, null);
|
|
|
|
boolean success = waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
// We set the left compound drawable (index 0) to the pin icon.
|
|
final TextView gridItemTextView = mSolo.getText(gridItemTitle);
|
|
return isPinned == (gridItemTextView.getCompoundDrawables()[0] != null);
|
|
}
|
|
}, MAX_WAIT_MS);
|
|
mAsserter.ok(success, "Top site item was pinned: " + isPinned, null);
|
|
}
|
|
|
|
public void pinTopSite(String gridItemTitle) {
|
|
verifyPinned(false, gridItemTitle);
|
|
mSolo.clickLongOnText(gridItemTitle);
|
|
boolean dialogOpened = mSolo.waitForDialogToOpen();
|
|
mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null);
|
|
boolean pinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_PIN_SITE);
|
|
mAsserter.ok(pinSiteFound, "Found pin site menu item", null);
|
|
mSolo.clickOnText(mStringHelper.CONTEXT_MENU_PIN_SITE);
|
|
verifyPinned(true, gridItemTitle);
|
|
}
|
|
|
|
public void unpinTopSite(String gridItemTitle) {
|
|
verifyPinned(true, gridItemTitle);
|
|
mSolo.clickLongOnText(gridItemTitle);
|
|
boolean dialogOpened = mSolo.waitForDialogToOpen();
|
|
mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null);
|
|
boolean unpinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_UNPIN_SITE);
|
|
mAsserter.ok(unpinSiteFound, "Found unpin site menu item", null);
|
|
mSolo.clickOnText(mStringHelper.CONTEXT_MENU_UNPIN_SITE);
|
|
verifyPinned(false, gridItemTitle);
|
|
}
|
|
|
|
// Used to perform clicks on pop-up buttons without having to close the virtual keyboard
|
|
public void clickOnButton(String label) {
|
|
final Button button = mSolo.getButton(label);
|
|
try {
|
|
runTestOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
button.performClick();
|
|
}
|
|
});
|
|
} catch (Throwable throwable) {
|
|
mAsserter.ok(false, "Unable to click the button","Was unable to click button ");
|
|
}
|
|
}
|
|
|
|
// Used to hide/show the virtual keyboard
|
|
public void toggleVKB() {
|
|
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
|
imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0);
|
|
}
|
|
|
|
public void addTab() {
|
|
mSolo.clickOnView(mSolo.getView(R.id.tabs));
|
|
// wait for addTab to appear (this is usually immediate)
|
|
boolean success = waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
View addTabView = mSolo.getView(R.id.add_tab);
|
|
if (addTabView == null) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}, MAX_WAIT_MS);
|
|
mAsserter.ok(success, "waiting for add tab view", "add tab view available");
|
|
final Actions.RepeatedEventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
|
|
mSolo.clickOnView(mSolo.getView(R.id.add_tab));
|
|
// Wait until we get a PageShow event for a new tab ID
|
|
for(;;) {
|
|
try {
|
|
JSONObject data = new JSONObject(pageShowExpecter.blockForEventData());
|
|
int tabID = data.getInt("tabID");
|
|
if (tabID == 0) {
|
|
mAsserter.dumpLog("addTab ignoring PageShow for tab 0");
|
|
continue;
|
|
}
|
|
if (!mKnownTabIDs.contains(tabID)) {
|
|
mKnownTabIDs.add(tabID);
|
|
break;
|
|
}
|
|
} catch(JSONException e) {
|
|
mAsserter.ok(false, "Exception in addTab", getStackTraceString(e));
|
|
}
|
|
}
|
|
pageShowExpecter.unregisterListener();
|
|
}
|
|
|
|
public void addTab(String url) {
|
|
addTab();
|
|
|
|
// Adding a new tab opens about:home, so now we just need to load the url in it.
|
|
inputAndLoadUrl(url);
|
|
}
|
|
|
|
public void closeAddedTabs() {
|
|
for(int tabID : mKnownTabIDs) {
|
|
closeTab(tabID);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the AdapterView of the tabs list.
|
|
*
|
|
* @return List view in the tabs panel
|
|
*/
|
|
private final AdapterView<ListAdapter> getTabsLayout() {
|
|
Element tabs = mDriver.findElement(getActivity(), R.id.tabs);
|
|
tabs.click();
|
|
return (AdapterView<ListAdapter>) getActivity().findViewById(R.id.normal_tabs);
|
|
}
|
|
|
|
/**
|
|
* Gets the view in the tabs panel at the specified index.
|
|
*
|
|
* @return View at index
|
|
*/
|
|
private View getTabViewAt(final int index) {
|
|
final View[] childView = { null };
|
|
|
|
final AdapterView<ListAdapter> view = getTabsLayout();
|
|
|
|
runOnUiThreadSync(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
view.setSelection(index);
|
|
|
|
// The selection isn't updated synchronously; posting a
|
|
// runnable to the view's queue guarantees we'll run after the
|
|
// layout pass.
|
|
view.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
// getChildAt() is relative to the list of visible
|
|
// views, but our index is relative to all views in the
|
|
// list. Subtract the first visible list position for
|
|
// the correct offset.
|
|
childView[0] = view.getChildAt(index - view.getFirstVisiblePosition());
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
boolean result = waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
return childView[0] != null;
|
|
}
|
|
}, MAX_WAIT_MS);
|
|
|
|
mAsserter.ok(result, "list item at index " + index + " exists", null);
|
|
|
|
return childView[0];
|
|
}
|
|
|
|
/**
|
|
* Selects the tab at the specified index.
|
|
*
|
|
* @param index Index of tab to select
|
|
*/
|
|
public void selectTabAt(final int index) {
|
|
mSolo.clickOnView(getTabViewAt(index));
|
|
}
|
|
|
|
/**
|
|
* Closes the tab at the specified index.
|
|
*
|
|
* @param index Index of tab to close
|
|
*/
|
|
public void closeTabAt(final int index) {
|
|
View closeButton = getTabViewAt(index).findViewById(R.id.close);
|
|
|
|
mSolo.clickOnView(closeButton);
|
|
}
|
|
|
|
public final void runOnUiThreadSync(Runnable runnable) {
|
|
RobocopUtils.runOnUiThreadSync(getActivity(), runnable);
|
|
}
|
|
|
|
/* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */
|
|
public void toggleBookmark() {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
waitForText("Settings");
|
|
|
|
// On ICS+ phones, there is no button labeled "Bookmarks"
|
|
// instead we have to just dig through every button on the screen
|
|
ArrayList<View> images = mSolo.getCurrentViews();
|
|
for (int i = 0; i < images.size(); i++) {
|
|
final View view = images.get(i);
|
|
boolean found = false;
|
|
found = "Bookmark".equals(view.getContentDescription());
|
|
|
|
// on older android versions, try looking at the button's text
|
|
if (!found) {
|
|
if (view instanceof TextView) {
|
|
found = "Bookmark".equals(((TextView)view).getText());
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
int[] xy = new int[2];
|
|
view.getLocationOnScreen(xy);
|
|
|
|
final int viewWidth = view.getWidth();
|
|
final int viewHeight = view.getHeight();
|
|
final float x = xy[0] + (viewWidth / 2.0f);
|
|
float y = xy[1] + (viewHeight / 2.0f);
|
|
|
|
mSolo.clickOnScreen(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void clearPrivateData() {
|
|
selectSettingsItem(mStringHelper.PRIVACY_SECTION_LABEL, mStringHelper.CLEAR_PRIVATE_DATA_LABEL);
|
|
Actions.EventExpecter clearData = mActions.expectGeckoEvent("Sanitize:Finished");
|
|
mSolo.clickOnText("Clear data");
|
|
clearData.blockForEvent();
|
|
clearData.unregisterListener();
|
|
}
|
|
|
|
class Device {
|
|
public final String version; // 2.x or 3.x or 4.x
|
|
public String type; // "tablet" or "phone"
|
|
public final int width;
|
|
public final int height;
|
|
public final float density;
|
|
|
|
public Device() {
|
|
// Determine device version
|
|
int sdk = Build.VERSION.SDK_INT;
|
|
if (sdk < Build.VERSION_CODES.HONEYCOMB) {
|
|
version = "2.x";
|
|
} else {
|
|
if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) {
|
|
version = "4.x";
|
|
} else {
|
|
version = "3.x";
|
|
}
|
|
}
|
|
// Determine with and height
|
|
DisplayMetrics dm = new DisplayMetrics();
|
|
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
|
|
height = dm.heightPixels;
|
|
width = dm.widthPixels;
|
|
density = dm.density;
|
|
// Determine device type
|
|
type = "phone";
|
|
try {
|
|
if (GeckoAppShell.isTablet()) {
|
|
type = "tablet";
|
|
}
|
|
} catch (Exception e) {
|
|
mAsserter.dumpLog("Exception in detectDevice", e);
|
|
}
|
|
}
|
|
|
|
public void rotate() {
|
|
if (getActivity().getRequestedOrientation () == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
|
mSolo.setActivityOrientation(Solo.PORTRAIT);
|
|
} else {
|
|
mSolo.setActivityOrientation(Solo.LANDSCAPE);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Navigation {
|
|
private final String devType;
|
|
private final String osVersion;
|
|
|
|
public Navigation(Device mDevice) {
|
|
devType = mDevice.type;
|
|
osVersion = mDevice.version;
|
|
}
|
|
|
|
public void back() {
|
|
Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
|
|
|
|
if (devType.equals("tablet")) {
|
|
Element backBtn = mDriver.findElement(getActivity(), R.id.back);
|
|
backBtn.click();
|
|
} else {
|
|
mSolo.goBack();
|
|
}
|
|
|
|
pageShowExpecter.blockForEvent();
|
|
pageShowExpecter.unregisterListener();
|
|
}
|
|
|
|
public void forward() {
|
|
Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
|
|
|
|
if (devType.equals("tablet")) {
|
|
mSolo.waitForView(R.id.forward);
|
|
mSolo.clickOnView(mSolo.getView(R.id.forward));
|
|
} else {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
waitForText("^New Tab$");
|
|
if (!osVersion.equals("2.x")) {
|
|
mSolo.waitForView(R.id.forward);
|
|
mSolo.clickOnView(mSolo.getView(R.id.forward));
|
|
} else {
|
|
mSolo.clickOnText("^Forward$");
|
|
}
|
|
ensureMenuClosed();
|
|
}
|
|
|
|
pageShowExpecter.blockForEvent();
|
|
pageShowExpecter.unregisterListener();
|
|
}
|
|
|
|
public void reload() {
|
|
if (devType.equals("tablet")) {
|
|
mSolo.waitForView(R.id.reload);
|
|
mSolo.clickOnView(mSolo.getView(R.id.reload));
|
|
} else {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
waitForText("^New Tab$");
|
|
if (!osVersion.equals("2.x")) {
|
|
mSolo.waitForView(R.id.reload);
|
|
mSolo.clickOnView(mSolo.getView(R.id.reload));
|
|
} else {
|
|
mSolo.clickOnText("^Reload$");
|
|
}
|
|
ensureMenuClosed();
|
|
}
|
|
}
|
|
|
|
// DEPRECATED!
|
|
// Use BaseTest.toggleBookmark() in new code.
|
|
public void bookmark() {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
waitForText("^New Tab$");
|
|
if (mSolo.searchText("^Bookmark$")) {
|
|
// This is the Android 2.x so the button has text
|
|
mSolo.clickOnText("^Bookmark$");
|
|
} else {
|
|
Element bookmarkBtn = mDriver.findElement(getActivity(), R.id.bookmark);
|
|
if (bookmarkBtn != null) {
|
|
// We are on Android 4.x so the button is an image button
|
|
bookmarkBtn.click();
|
|
}
|
|
}
|
|
ensureMenuClosed();
|
|
}
|
|
|
|
// On some devices, the menu may not be dismissed after clicking on an
|
|
// item. Close it here.
|
|
private void ensureMenuClosed() {
|
|
if (mSolo.searchText("^New Tab$")) {
|
|
mSolo.goBack();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the string representation of a stack trace.
|
|
*
|
|
* @param t Throwable to get stack trace for
|
|
* @return Stack trace as a string
|
|
*/
|
|
public static String getStackTraceString(Throwable t) {
|
|
StringWriter sw = new StringWriter();
|
|
t.printStackTrace(new PrintWriter(sw));
|
|
return sw.toString();
|
|
}
|
|
|
|
/**
|
|
* Condition class that waits for a view, and allows callers access it when done.
|
|
*/
|
|
private class DescriptionCondition<T extends View> implements Condition {
|
|
public T mView;
|
|
private final String mDescr;
|
|
private final Class<T> mCls;
|
|
|
|
public DescriptionCondition(Class<T> cls, String descr) {
|
|
mDescr = descr;
|
|
mCls = cls;
|
|
}
|
|
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
mView = findViewWithContentDescription(mCls, mDescr);
|
|
return (mView != null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for a view with the specified description .
|
|
*/
|
|
public <T extends View> T waitForViewWithDescription(Class<T> cls, String description) {
|
|
DescriptionCondition<T> c = new DescriptionCondition<T>(cls, description);
|
|
waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS);
|
|
return c.mView;
|
|
}
|
|
|
|
/**
|
|
* Get an active view with the specified description .
|
|
*/
|
|
public <T extends View> T findViewWithContentDescription(Class<T> cls, String description) {
|
|
for (T view : mSolo.getCurrentViews(cls)) {
|
|
final String descr = (String) view.getContentDescription();
|
|
if (TextUtils.isEmpty(descr)) {
|
|
continue;
|
|
}
|
|
|
|
if (TextUtils.equals(description, descr)) {
|
|
return view;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Abstract class for running small test cases within a BaseTest.
|
|
*/
|
|
abstract class TestCase implements Runnable {
|
|
/**
|
|
* Implement tests here. setUp and tearDown for the test case
|
|
* should be handled by the parent test. This is so we can avoid the
|
|
* overhead of starting Gecko and creating profiles.
|
|
*/
|
|
protected abstract void test() throws Exception;
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
test();
|
|
} catch (Exception e) {
|
|
mAsserter.ok(false,
|
|
"Test " + this.getClass().getName() + " threw exception: " + e,
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the preference and wait for it to change before proceeding with the test.
|
|
*/
|
|
public void setPreferenceAndWaitForChange(final JSONObject jsonPref) {
|
|
mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
|
|
|
|
// Get the preference name from the json and store it in an array. This array
|
|
// will be used later while fetching the preference data.
|
|
String[] prefNames = new String[1];
|
|
try {
|
|
prefNames[0] = jsonPref.getString("name");
|
|
} catch (JSONException e) {
|
|
mAsserter.ok(false, "Exception in setPreferenceAndWaitForChange", getStackTraceString(e));
|
|
}
|
|
|
|
// Wait for confirmation of the pref change before proceeding with the test.
|
|
final int ourRequestID = mPreferenceRequestID--;
|
|
final Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
|
|
mActions.sendPreferencesGetEvent(ourRequestID, prefNames);
|
|
|
|
// Wait until we get the correct "Preferences:Data" event
|
|
waitForCondition(new Condition() {
|
|
final long endTime = SystemClock.elapsedRealtime() + MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS;
|
|
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
try {
|
|
long timeout = endTime - SystemClock.elapsedRealtime();
|
|
if (timeout < 0) {
|
|
timeout = 0;
|
|
}
|
|
|
|
JSONObject data = new JSONObject(eventExpecter.blockForEventDataWithTimeout(timeout));
|
|
int requestID = data.getInt("requestId");
|
|
if (requestID != ourRequestID) {
|
|
return false;
|
|
}
|
|
|
|
JSONArray preferences = data.getJSONArray("preferences");
|
|
mAsserter.is(preferences.length(), 1, "Expecting preference array to have one element");
|
|
JSONObject prefs = (JSONObject) preferences.get(0);
|
|
mAsserter.is(prefs.getString("name"), jsonPref.getString("name"),
|
|
"Expecting returned preference name to be the same as the set name");
|
|
mAsserter.is(prefs.getString("type"), jsonPref.getString("type"),
|
|
"Expecting returned preference type to be the same as the set type");
|
|
mAsserter.is(prefs.get("value"), jsonPref.get("value"),
|
|
"Expecting returned preference value to be the same as the set value");
|
|
return true;
|
|
} catch(JSONException e) {
|
|
mAsserter.ok(false, "Exception in setPreferenceAndWaitForChange", getStackTraceString(e));
|
|
// Please the java compiler
|
|
return false;
|
|
}
|
|
}
|
|
}, MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS);
|
|
|
|
eventExpecter.unregisterListener();
|
|
}
|
|
}
|