mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1038 lines
38 KiB
Java
1038 lines
38 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.Driver;
|
|
import org.mozilla.gecko.Element;
|
|
import org.mozilla.gecko.FennecNativeActions;
|
|
import org.mozilla.gecko.FennecNativeDriver;
|
|
import org.mozilla.gecko.GeckoAppShell;
|
|
import org.mozilla.gecko.GeckoEvent;
|
|
import org.mozilla.gecko.GeckoProfile;
|
|
import org.mozilla.gecko.GeckoThread;
|
|
import org.mozilla.gecko.GeckoThread.LaunchState;
|
|
import org.mozilla.gecko.NewTabletUI;
|
|
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.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;
|
|
|
|
private static final String URL_HTTP_PREFIX = "http://";
|
|
|
|
private Activity mActivity;
|
|
private int mPreferenceRequestID = 0;
|
|
protected Solo mSolo;
|
|
protected Driver mDriver;
|
|
protected Actions mActions;
|
|
protected String mBaseUrl;
|
|
protected String mRawBaseUrl;
|
|
protected String mProfile;
|
|
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.checkLaunchState(LaunchState.GeckoRunning)) {
|
|
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();
|
|
|
|
// Create the intent to be used with all the important arguments.
|
|
mBaseUrl = mConfig.get("host").replaceAll("(/$)", "");
|
|
mRawBaseUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
|
|
Intent i = new Intent(Intent.ACTION_MAIN);
|
|
mProfile = mConfig.get("profile");
|
|
i.putExtra("args", "-no-remote -profile " + mProfile);
|
|
String envString = mConfig.get("envvars");
|
|
if (envString != "") {
|
|
String[] envStrings = envString.split(",");
|
|
for (int iter = 0; iter < envStrings.length; iter++) {
|
|
i.putExtra("env" + iter, envStrings[iter]);
|
|
}
|
|
}
|
|
|
|
// Start the activity.
|
|
setActivityIntent(i);
|
|
mActivity = getActivity();
|
|
// Set up Robotium.solo and Driver objects
|
|
mSolo = new Solo(getInstrumentation(), mActivity);
|
|
mDriver = new FennecNativeDriver(mActivity, mSolo, mRootPath);
|
|
mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter);
|
|
mDevice = new Device();
|
|
mDatabaseHelper = new DatabaseHelper(mActivity, 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");
|
|
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;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void tearDown() throws Exception {
|
|
try {
|
|
mAsserter.endTest();
|
|
// request a force quit of the browser and wait for it to take effect
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
|
|
mSolo.sleep(7000);
|
|
// if still running, finish activities as recommended by Robotium
|
|
mSolo.finishOpenedActivities();
|
|
} catch (Throwable e) {
|
|
e.printStackTrace();
|
|
}
|
|
super.tearDown();
|
|
}
|
|
|
|
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() {
|
|
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.enterText(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.
|
|
*/
|
|
protected final void inputAndLoadUrl(String url) {
|
|
enterUrl(url);
|
|
hitEnterAndWait();
|
|
}
|
|
|
|
/**
|
|
* Load <code>url</code> using reflection and the internal
|
|
* <code>org.mozilla.gecko.Tabs</code> API.
|
|
*
|
|
* This method does not wait for any confirmation from Gecko before
|
|
* returning.
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 mBaseUrl + "/" + url.replaceAll("(^/)", "");
|
|
}
|
|
|
|
protected final String getAbsoluteRawUrl(String url) {
|
|
return mRawBaseUrl + "/" + 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;
|
|
}
|
|
|
|
/**
|
|
* @deprecated use {@link #waitForCondition(Condition, int)} instead
|
|
*/
|
|
@Deprecated
|
|
protected final boolean waitForTest(final BooleanTest t, final int timeout) {
|
|
final boolean isSatisfied = mSolo.waitForCondition(new Condition() {
|
|
@Override
|
|
public boolean isSatisfied() {
|
|
return t.test();
|
|
}
|
|
}, timeout);
|
|
|
|
if (!isSatisfied) {
|
|
// log out wait failure for diagnostic purposes only;
|
|
// a failed wait may be normal and does not necessarily
|
|
// warrant a test assertion/failure
|
|
mAsserter.dumpLog("waitForTest timeout after " + timeout + " ms");
|
|
}
|
|
return isSatisfied;
|
|
}
|
|
|
|
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 verifyPageTitle(final String title, String url) {
|
|
// We are asserting visible state - we shouldn't know if the title is null.
|
|
mAsserter.isnot(title, null, "The title argument is not null");
|
|
mAsserter.isnot(url, null, "The url argument is not null");
|
|
|
|
// TODO: We should also check the title bar preference.
|
|
final String expected;
|
|
if (!NewTabletUI.isEnabled(mActivity)) {
|
|
expected = title;
|
|
} else {
|
|
if (StringHelper.ABOUT_HOME_URL.equals(url)) {
|
|
expected = StringHelper.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, title), 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");
|
|
}
|
|
|
|
// 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(mActivity, 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(StringHelper.PRIVACY_SECTION_LABEL, StringHelper.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 {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.BACK);
|
|
}
|
|
|
|
pageShowExpecter.blockForEvent();
|
|
pageShowExpecter.unregisterListener();
|
|
}
|
|
|
|
public void forward() {
|
|
Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
|
|
|
|
if (devType.equals("tablet")) {
|
|
Element fwdBtn = mDriver.findElement(getActivity(), R.id.forward);
|
|
fwdBtn.click();
|
|
} else {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
waitForText("^New Tab$");
|
|
if (!osVersion.equals("2.x")) {
|
|
Element fwdBtn = mDriver.findElement(getActivity(), R.id.forward);
|
|
fwdBtn.click();
|
|
} else {
|
|
mSolo.clickOnText("^Forward$");
|
|
}
|
|
ensureMenuClosed();
|
|
}
|
|
|
|
pageShowExpecter.blockForEvent();
|
|
pageShowExpecter.unregisterListener();
|
|
}
|
|
|
|
public void reload() {
|
|
if (devType.equals("tablet")) {
|
|
Element reloadBtn = mDriver.findElement(getActivity(), R.id.reload);
|
|
reloadBtn.click();
|
|
} else {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.MENU);
|
|
waitForText("^New Tab$");
|
|
if (!osVersion.equals("2.x")) {
|
|
Element reloadBtn = mDriver.findElement(getActivity(), R.id.reload);
|
|
reloadBtn.click();
|
|
} 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$")) {
|
|
mActions.sendSpecialKey(Actions.SpecialKey.BACK);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|