diff -r 000000000000 -r 6474c204b198 mobile/android/base/tests/BaseTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/tests/BaseTest.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,948 @@ +/* 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 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.GeckoThread; +import org.mozilla.gecko.GeckoThread.LaunchState; +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; + +/** + * 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; + 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 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 StringHelper mStringHelper; + protected int mScreenMidWidth; + protected int mScreenMidHeight; + + 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 = ((String) mConfig.get("host")).replaceAll("(/$)", ""); + mRawBaseUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", ""); + Intent i = new Intent(Intent.ACTION_MAIN); + mProfile = (String) mConfig.get("profile"); + i.putExtra("args", "-no-remote -profile " + mProfile); + String envString = (String) 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); + mStringHelper = new StringHelper(); + } + + @Override + protected void runTest() throws Throwable { + try { + super.runTest(); + } catch (Throwable t) { + // save screenshot -- written to /mnt/sdcard/Robotium-Screenshots + // as .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 + 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) { + final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text); + + focusUrlBar(); + + // 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 url by sending key strokes to the URL bar UI. + * + * This method waits synchronously for the DOMContentLoaded + * message from Gecko before returning. + */ + protected final void inputAndLoadUrl(String url) { + enterUrl(url); + hitEnterAndWait(); + } + + /** + * Load url using reflection and the internal + * org.mozilla.gecko.Tabs 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 TextView mTextView; + private 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 asssertion/failure. + mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms."); + } + return result; + } + + // TODO: With Robotium 4.2, we should use Condition and waitForCondition instead. + // Future boolean tests should not use this method. + protected final boolean waitForTest(BooleanTest t, int timeout) { + long end = SystemClock.uptimeMillis() + timeout; + while (SystemClock.uptimeMillis() < end) { + if (t.test()) { + return true; + } + mSolo.sleep(100); + } + // 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 false; + } + + // TODO: With Robotium 4.2, we should use Condition and waitForCondition instead. + // Future boolean tests should not implement this interface. + 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(String text) { + boolean rc = mSolo.waitForText(text); + 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 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 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 from Menu > "Settings" >
. + */ + 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)) { + 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(String title) { + 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_MS); + pageTitle = urlBarTitle.getText().toString(); + } + mAsserter.is(pageTitle, title, "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"); + Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); + mSolo.clickOnView(mSolo.getView(R.id.add_tab)); + pageShowExpecter.blockForEvent(); + 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); + } + + /** + * Gets the AdapterView of the tabs list. + * + * @return List view in the tabs tray + */ + private final AdapterView getTabsList() { + Element tabs = mDriver.findElement(getActivity(), R.id.tabs); + tabs.click(); + return (AdapterView) getActivity().findViewById(R.id.normal_tabs); + } + + /** + * Gets the view in the tabs tray at the specified index. + * + * @return View at index + */ + private View getTabViewAt(final int index) { + final View[] childView = { null }; + + final AdapterView view = getTabsList(); + + 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 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 String devType; + private 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 implements Condition { + public T mView; + private String mDescr; + private Class mCls; + + public DescriptionCondition(Class 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 waitForViewWithDescription(Class cls, String description) { + DescriptionCondition c = new DescriptionCondition(cls, description); + waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS); + return c.mView; + } + + /** + * Get an active view with the specified description . + */ + public T findViewWithContentDescription(Class 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(); + } +}