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