summaryrefslogtreecommitdiffstats
path: root/mobile/android/tests/browser/robocop/src/org/mozilla/gecko
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/tests/browser/robocop/src/org/mozilla/gecko
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/tests/browser/robocop/src/org/mozilla/gecko')
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java126
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java25
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java44
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java27
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java79
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java254
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java482
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java392
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java116
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java74
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java40
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java105
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java24
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java17
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java17
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java58
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java188
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java252
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java288
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java976
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java135
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java255
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java170
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java107
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java87
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java210
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java224
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java117
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java56
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java407
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java401
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java203
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java51
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java193
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java295
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java36
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java343
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java56
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java326
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java112
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java108
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java94
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java50
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java49
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java30
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java394
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java100
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java104
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java43
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java215
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java240
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java107
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java57
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java47
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java76
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java94
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java172
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java79
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java39
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java77
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java58
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java47
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java72
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java169
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java28
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java46
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java174
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java150
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java13
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java1921
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java69
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java31
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java61
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java61
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java70
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java556
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java205
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java450
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java133
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java107
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java104
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java295
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java121
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java159
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java74
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java12
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java94
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java118
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java238
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java349
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java136
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java70
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java69
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java37
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java23
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java387
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java26
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java288
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java65
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java137
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java49
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java125
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java104
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java72
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java81
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java89
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java47
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java62
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java19
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java12
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java217
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java39
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java48
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java379
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java115
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java37
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java54
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java87
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java265
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java40
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java90
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java116
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java65
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java56
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java265
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java9
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java105
131 files changed, 19621 insertions, 0 deletions
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java
new file mode 100644
index 000000000..05e6bfa52
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java
@@ -0,0 +1,126 @@
+/* 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;
+import android.database.Cursor;
+
+public interface Actions {
+
+ /** Special keys supported by sendSpecialKey() */
+ public enum SpecialKey {
+ DOWN,
+ UP,
+ LEFT,
+ RIGHT,
+ ENTER,
+ MENU,
+ DELETE,
+ }
+
+ public interface EventExpecter {
+ /** Blocks until the event has been received. Subsequent calls will return immediately. */
+ public void blockForEvent();
+ public void blockForEvent(long millis, boolean failOnTimeout);
+
+ /** Blocks until the event has been received and returns data associated with the event. */
+ public String blockForEventData();
+
+ /**
+ * Blocks until the event has been received, or until the timeout has been exceeded.
+ * Returns the data associated with the event, if applicable.
+ */
+ public String blockForEventDataWithTimeout(long millis);
+
+ /** Polls to see if the event has been received. Once this returns true, subsequent calls will also return true. */
+ public boolean eventReceived();
+
+ /** Stop listening for events. */
+ public void unregisterListener();
+ }
+
+ public interface RepeatedEventExpecter extends EventExpecter {
+ /** Blocks until at least one event has been received, and no events have been received in the last <code>millis</code> milliseconds. */
+ public void blockUntilClear(long millis);
+ }
+
+ /**
+ * Sends an event to Gecko.
+ *
+ * @param geckoEvent The geckoEvent JSONObject's type
+ */
+ void sendGeckoEvent(String geckoEvent, String data);
+
+ public interface PrefWaiter {
+ boolean isFinished();
+ void waitForFinish();
+ void waitForFinish(long timeoutMillis, boolean failOnTimeout);
+ }
+
+ public abstract static class PrefHandlerBase implements PrefsHelper.PrefHandler {
+ /* package */ Assert asserter;
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, boolean value) {
+ asserter.ok(false, "Unexpected pref callback", "");
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, int value) {
+ asserter.ok(false, "Unexpected pref callback", "");
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, String value) {
+ asserter.ok(false, "Unexpected pref callback", "");
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void finish() {
+ }
+ }
+
+ PrefWaiter getPrefs(String[] prefNames, PrefHandlerBase handler);
+ void setPref(String pref, Object value, boolean flush);
+ PrefWaiter addPrefsObserver(String[] prefNames, PrefHandlerBase handler);
+ void removePrefsObserver(PrefWaiter handler);
+
+ /**
+ * Listens for a gecko event to be sent from the Gecko instance.
+ * The returned object can be used to test if the event has been
+ * received. Note that only one event is listened for.
+ *
+ * @param geckoEvent The geckoEvent JSONObject's type
+ */
+ RepeatedEventExpecter expectGeckoEvent(String geckoEvent);
+
+ /**
+ * Listens for a paint event. Note that calling expectPaint() will
+ * invalidate the event expecters returned from any previous calls
+ * to expectPaint(); calling any methods on those invalidated objects
+ * will result in undefined behaviour.
+ */
+ RepeatedEventExpecter expectPaint();
+
+ /**
+ * Send a string to the application
+ *
+ * @param keysToSend The string to send
+ */
+ void sendKeys(String keysToSend);
+
+ /**
+ * Send a special keycode to the element
+ *
+ * @param key The special key to send
+ */
+ void sendSpecialKey(SpecialKey key);
+ void sendKeyCode(int keyCode);
+
+ void drag(int startingX, int endingX, int startingY, int endingY);
+
+ /**
+ * Run a sql query on the specified database
+ */
+ public Cursor querySql(String dbPath, String sql);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java
new file mode 100644
index 000000000..aa76dcf2b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java
@@ -0,0 +1,25 @@
+/* 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;
+
+public interface Assert {
+ void dumpLog(String message);
+ void dumpLog(String message, Throwable t);
+ void setLogFile(String filename);
+ void setTestName(String testName);
+ void endTest();
+
+ void ok(boolean condition, String name, String diag);
+ void is(Object actual, Object expected, String name);
+ void isnot(Object actual, Object notExpected, String name);
+ void todo(boolean condition, String name, String diag);
+ void todo_is(Object actual, Object expected, String name);
+ void todo_isnot(Object actual, Object notExpected, String name);
+ void info(String name, String message);
+
+ // robocop-specific asserts
+ void ispixel(int actual, int r, int g, int b, String name);
+ void isnotpixel(int actual, int r, int g, int b, String name);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java
new file mode 100644
index 000000000..4c8373c5b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java
@@ -0,0 +1,44 @@
+/* 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;
+
+import android.app.Activity;
+
+public interface Driver {
+ /**
+ * Find the first Element using the given method.
+ *
+ * @param activity The activity the element belongs to
+ * @param id The resource id of the element
+ * @return The first matching element on the current context, or null if not found.
+ */
+ Element findElement(Activity activity, int id);
+
+ /**
+ * Sets up scroll handling so that data is received from the extension.
+ */
+ void setupScrollHandling();
+
+ int getPageHeight();
+ int getScrollHeight();
+ int getHeight();
+ int getGeckoTop();
+ int getGeckoLeft();
+ int getGeckoWidth();
+ int getGeckoHeight();
+
+ void startFrameRecording();
+ int stopFrameRecording();
+
+ void startCheckerboardRecording();
+ float stopCheckerboardRecording();
+
+ /**
+ * Get a copy of the painted content region.
+ * @return A 2-D array of pixels (indexed by y, then x). The pixels
+ * are in ARGB-8888 format.
+ */
+ PaintedSurface getPaintedSurface();
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java
new file mode 100644
index 000000000..97610ff32
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java
@@ -0,0 +1,27 @@
+/* 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;
+
+/**
+ * Element provides access to a specific UI view (android.view.View).
+ * See also Driver.findElement().
+ */
+public interface Element {
+
+ /** Click on the element's view. Returns true on success. */
+ boolean click();
+
+ /** Returns true if the element is currently displayed */
+ boolean isDisplayed();
+
+ /**
+ * Returns the text currently displayed on the element, or null
+ * if the text cannot be retrieved.
+ */
+ String getText();
+
+ /** Returns the view ID */
+ Integer getId();
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java
new file mode 100644
index 000000000..6bcb4e102
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java
@@ -0,0 +1,79 @@
+/* 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;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.test.InstrumentationTestRunner;
+import android.util.Log;
+
+import static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP;
+import static android.os.PowerManager.FULL_WAKE_LOCK;
+import static android.os.PowerManager.ON_AFTER_RELEASE;
+
+public class FennecInstrumentationTestRunner extends InstrumentationTestRunner {
+ private static Bundle sArguments;
+ private PowerManager.WakeLock wakeLock;
+ private KeyguardManager.KeyguardLock keyguardLock;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ sArguments = arguments;
+ if (sArguments == null) {
+ Log.e("Robocop", "FennecInstrumentationTestRunner.onCreate got null bundle");
+ }
+ super.onCreate(arguments);
+ }
+
+ // unfortunately we have to make this static because test classes that don't extend
+ // from ActivityInstrumentationTestCase2 can't get a reference to this class.
+ public static Bundle getFennecArguments() {
+ if (sArguments == null) {
+ Log.e("Robocop", "FennecInstrumentationTestCase.getFennecArguments returns null bundle");
+ }
+ return sArguments;
+ }
+
+ @Override
+ public void onStart() {
+ final Context context = getContext(); // The Robocop package itself has DISABLE_KEYGUARD and WAKE_LOCK.
+ if (context != null) {
+ try {
+ String name = FennecInstrumentationTestRunner.class.getSimpleName();
+ // Unlock the device so that the tests can input keystrokes.
+ final KeyguardManager keyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+ // Deprecated in favour of window flags, which aren't appropriate here.
+ keyguardLock = keyguard.newKeyguardLock(name);
+ keyguardLock.disableKeyguard();
+
+ // Wake up the screen.
+ final PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wakeLock = power.newWakeLock(FULL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP | ON_AFTER_RELEASE, name);
+ wakeLock.acquire();
+ } catch (SecurityException e) {
+ Log.w("GeckoInstTestRunner", "Got SecurityException: not disabling keyguard and not taking wakelock.");
+ }
+ } else {
+ Log.w("GeckoInstTestRunner", "Application target context is null: not disabling keyguard and not taking wakelock.");
+ }
+
+ super.onStart();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (wakeLock != null) {
+ wakeLock.release();
+ }
+ if (keyguardLock != null) {
+ // Deprecated in favour of window flags, which aren't appropriate here.
+ keyguardLock.reenableKeyguard();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java
new file mode 100644
index 000000000..cb7c3c464
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java
@@ -0,0 +1,254 @@
+/* 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;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import android.os.SystemClock;
+
+public class FennecMochitestAssert implements Assert {
+ // Internal state variables to make logging match up with existing mochitests
+ private int mPassed = 0;
+ private int mFailed = 0;
+ private int mTodo = 0;
+
+ // Used to write the first line of the test file
+ private boolean mLogStarted = false;
+
+ // Used to write the test-start/test-end log lines
+ private String mLogTestName = "";
+
+ // Measure the time it takes to run test case
+ private long mStartTime = 0;
+
+ // Structured logger
+ private StructuredLogger mLogger;
+
+ /** Write information to a logfile and logcat */
+ public void dumpLog(String message) {
+ mLogger.info(message);
+ }
+
+ public void dumpLog(String message, Throwable t) {
+ Writer sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ t.printStackTrace(pw);
+ mLogger.error(message + " - " + sw.toString());
+ }
+
+ /** Write information to a logfile and logcat */
+ static class DumpLogCallback implements StructuredLogger.LoggerCallback {
+ public void call(String output) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, output);
+ }
+ }
+
+
+ public FennecMochitestAssert() {
+ mLogger = new StructuredLogger("robocop", new DumpLogCallback());
+ }
+
+ /** Set the filename used for dumpLog. */
+ public void setLogFile(String filename) {
+ FennecNativeDriver.setLogFile(filename);
+
+ String message;
+ if (!mLogStarted) {
+ mLogger.info("SimpleTest START");
+ mLogStarted = true;
+ }
+
+ if (mLogTestName != "") {
+ long diff = SystemClock.uptimeMillis() - mStartTime;
+ mLogger.testEnd(mLogTestName, "OK", "finished in " + diff + "ms");
+ }
+ }
+
+ public void setTestName(String testName) {
+ String[] nameParts = testName.split("\\.");
+ mLogTestName = nameParts[nameParts.length - 1];
+ mStartTime = SystemClock.uptimeMillis();
+
+ mLogger.testStart(mLogTestName);
+ }
+
+ class testInfo {
+ public boolean mResult;
+ public String mName;
+ public String mDiag;
+ public boolean mTodo;
+ public boolean mInfo;
+ public testInfo(boolean r, String n, String d, boolean t, boolean i) {
+ mResult = r;
+ mName = n;
+ mDiag = d;
+ mTodo = t;
+ mInfo = i;
+ }
+
+ }
+
+ /** Used to log a subtest's result.
+ * test represents the subtest (an assertion).
+ * passStatus and passExpected are the actual status and the expected status if the assertion is true.
+ * failStatus and failExpected are the actual status and the expected status otherwise.
+ */
+ private void _logMochitestResult(testInfo test, String passStatus, String passExpected, String failStatus, String failExpected) {
+ boolean isError = true;
+ if (test.mResult || test.mTodo) {
+ isError = false;
+ }
+ if (test.mResult)
+ {
+ mLogger.testStatus(mLogTestName, test.mName, passStatus, passExpected, test.mDiag);
+ } else {
+ mLogger.testStatus(mLogTestName, test.mName, failStatus, failExpected, test.mDiag);
+ }
+
+ if (test.mInfo) {
+ // do not count TEST-INFO messages
+ } else if (test.mTodo) {
+ mTodo++;
+ } else if (isError) {
+ mFailed++;
+ } else {
+ mPassed++;
+ }
+ if (isError) {
+ String message = "TEST-UNEXPECTED-" + failStatus + " | " + mLogTestName + " | "
+ + test.mName + " - " + test.mDiag;
+ junit.framework.Assert.fail(message);
+ }
+ }
+
+ public void endTest() {
+ String message;
+
+ if (mLogTestName != "") {
+ long diff = SystemClock.uptimeMillis() - mStartTime;
+ mLogger.testEnd(mLogTestName, "OK", "finished in " + diff + "ms");
+ }
+
+ mLogger.info("TEST-START | Shutdown");
+ mLogger.info("Passed: " + Integer.toString(mPassed));
+ mLogger.info("Failed: " + Integer.toString(mFailed));
+ mLogger.info("Todo: " + Integer.toString(mTodo));
+ mLogger.info("SimpleTest FINISHED");
+ }
+
+ public void ok(boolean condition, String name, String diag) {
+ testInfo test = new testInfo(condition, name, diag, false, false);
+ _logMochitestResult(test, "PASS", "PASS", "FAIL", "PASS");
+ }
+
+ public void is(Object actual, Object expected, String name) {
+ boolean pass = checkObjectsEqual(actual, expected);
+ ok(pass, name, getEqualString(actual, expected, pass));
+ }
+
+ public void isnot(Object actual, Object notExpected, String name) {
+ boolean pass = checkObjectsNotEqual(actual, notExpected);
+ ok(pass, name, getNotEqualString(actual, notExpected, pass));
+ }
+
+ public void ispixel(int actual, int r, int g, int b, String name) {
+ int aAlpha = ((actual >> 24) & 0xFF);
+ int aR = ((actual >> 16) & 0xFF);
+ int aG = ((actual >> 8) & 0xFF);
+ int aB = (actual & 0xFF);
+ boolean pass = checkPixel(actual, r, g, b);
+ ok(pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (pass ? " " : " not") + " close enough to expected rgb(" + r + "," + g + "," + b + ")");
+ }
+
+ public void isnotpixel(int actual, int r, int g, int b, String name) {
+ int aAlpha = ((actual >> 24) & 0xFF);
+ int aR = ((actual >> 16) & 0xFF);
+ int aG = ((actual >> 8) & 0xFF);
+ int aB = (actual & 0xFF);
+ boolean pass = checkPixel(actual, r, g, b);
+ ok(!pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (!pass ? " is" : " is not") + " different enough from rgb(" + r + "," + g + "," + b + ")");
+ }
+
+ private boolean checkPixel(int actual, int r, int g, int b) {
+ // When we read GL pixels the GPU has already processed them and they
+ // are usually off by a little bit. For example a CSS-color pixel of color #64FFF5
+ // was turned into #63FFF7 when it came out of glReadPixels. So in order to compare
+ // against the expected value, we use a little fuzz factor. For the alpha we just
+ // make sure it is always 0xFF. There is also bug 691354 which crops up every so
+ // often just to make our lives difficult. However the individual color components
+ // should never be off by more than 8.
+ int aAlpha = ((actual >> 24) & 0xFF);
+ int aR = ((actual >> 16) & 0xFF);
+ int aG = ((actual >> 8) & 0xFF);
+ int aB = (actual & 0xFF);
+ boolean pass = (aAlpha == 0xFF) /* alpha */
+ && (Math.abs(aR - r) <= 8) /* red */
+ && (Math.abs(aG - g) <= 8) /* green */
+ && (Math.abs(aB - b) <= 8); /* blue */
+ if (pass) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void todo(boolean condition, String name, String diag) {
+ testInfo test = new testInfo(condition, name, diag, true, false);
+ _logMochitestResult(test, "PASS", "FAIL", "FAIL", "FAIL");
+ }
+
+ public void todo_is(Object actual, Object expected, String name) {
+ boolean pass = checkObjectsEqual(actual, expected);
+ todo(pass, name, getEqualString(actual, expected, pass));
+ }
+
+ public void todo_isnot(Object actual, Object notExpected, String name) {
+ boolean pass = checkObjectsNotEqual(actual, notExpected);
+ todo(pass, name, getNotEqualString(actual, notExpected, pass));
+ }
+
+ private boolean checkObjectsEqual(Object a, Object b) {
+ if (a == null || b == null) {
+ if (a == null && b == null) {
+ return true;
+ }
+ return false;
+ } else {
+ return a.equals(b);
+ }
+ }
+
+ private String getEqualString(Object a, Object b, boolean pass) {
+ if (pass) {
+ return a + " should equal " + b;
+ }
+ return "got " + a + ", expected " + b;
+ }
+
+ private boolean checkObjectsNotEqual(Object a, Object b) {
+ if (a == null || b == null) {
+ if ((a == null && b != null) || (a != null && b == null)) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return !a.equals(b);
+ }
+ }
+
+ private String getNotEqualString(Object a, Object b, boolean pass) {
+ if(pass) {
+ return a + " should not equal " + b;
+ }
+ return "didn't expect " + a + ", but got it";
+ }
+
+ public void info(String name, String message) {
+ mLogger.info(name + " | " + message);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
new file mode 100644
index 000000000..7faccdf43
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
@@ -0,0 +1,482 @@
+/* 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;
+
+import java.util.ArrayList;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.FennecNativeDriver.LogLevel;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.sqlite.SQLiteBridge;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.database.Cursor;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+
+import com.robotium.solo.Solo;
+
+public class FennecNativeActions implements Actions {
+ private static final String LOGTAG = "FennecNativeActions";
+
+ private Solo mSolo;
+ private Instrumentation mInstr;
+ private Assert mAsserter;
+
+ public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation, Assert asserter) {
+ mSolo = robocop;
+ mInstr = instrumentation;
+ mAsserter = asserter;
+
+ GeckoLoader.loadSQLiteLibs(activity, activity.getApplication().getPackageResourcePath());
+ }
+
+ class GeckoEventExpecter implements RepeatedEventExpecter {
+ private static final int MAX_WAIT_MS = 180000;
+
+ private volatile boolean mIsRegistered;
+
+ private final String mGeckoEvent;
+ private final GeckoEventListener mListener;
+
+ private volatile boolean mEventEverReceived;
+ private String mEventData;
+ private BlockingQueue<String> mEventDataQueue;
+
+ GeckoEventExpecter(final String geckoEvent) {
+ if (TextUtils.isEmpty(geckoEvent)) {
+ throw new IllegalArgumentException("geckoEvent must not be empty");
+ }
+
+ mGeckoEvent = geckoEvent;
+ mEventDataQueue = new LinkedBlockingQueue<String>();
+
+ final GeckoEventExpecter expecter = this;
+ mListener = new GeckoEventListener() {
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "handleMessage called for: " + event + "; expecting: " + mGeckoEvent);
+ mAsserter.is(event, mGeckoEvent, "Given message occurred for registered event: " + message);
+
+ expecter.notifyOfEvent(message);
+ }
+ };
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(mListener, mGeckoEvent);
+ mIsRegistered = true;
+ }
+
+ public void blockForEvent() {
+ blockForEvent(MAX_WAIT_MS, true);
+ }
+
+ public void blockForEvent(long millis, boolean failOnTimeout) {
+ if (!mIsRegistered) {
+ throw new IllegalStateException("listener not registered");
+ }
+
+ try {
+ mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ }
+ if (mEventData == null) {
+ if (failOnTimeout) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "GeckoEventExpecter",
+ "blockForEvent timeout: "+mGeckoEvent);
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "blockForEvent timeout: "+mGeckoEvent);
+ }
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "unblocked on expecter for " + mGeckoEvent);
+ }
+ }
+
+ public void blockUntilClear(long millis) {
+ if (!mIsRegistered) {
+ throw new IllegalStateException("listener not registered");
+ }
+ if (millis <= 0) {
+ throw new IllegalArgumentException("millis must be > 0");
+ }
+
+ // wait for at least one event
+ try {
+ mEventData = mEventDataQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ }
+ if (mEventData == null) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "GeckoEventExpecter", "blockUntilClear timeout");
+ return;
+ }
+ // now wait for a period of millis where we don't get an event
+ while (true) {
+ try {
+ mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.INFO, ie);
+ }
+ if (mEventData == null) {
+ // success
+ break;
+ }
+ }
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "unblocked on expecter for " + mGeckoEvent);
+ }
+
+ public String blockForEventData() {
+ blockForEvent();
+ return mEventData;
+ }
+
+ public String blockForEventDataWithTimeout(long millis) {
+ blockForEvent(millis, false);
+ return mEventData;
+ }
+
+ public void unregisterListener() {
+ if (!mIsRegistered) {
+ throw new IllegalStateException("listener not registered");
+ }
+
+ FennecNativeDriver.log(LogLevel.INFO,
+ "EventExpecter: no longer listening for " + mGeckoEvent);
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(mListener, mGeckoEvent);
+ mIsRegistered = false;
+ }
+
+ public boolean eventReceived() {
+ return mEventEverReceived;
+ }
+
+ void notifyOfEvent(final JSONObject message) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "received event " + mGeckoEvent);
+
+ mEventEverReceived = true;
+
+ try {
+ mEventDataQueue.put(message.toString());
+ } catch (InterruptedException e) {
+ FennecNativeDriver.log(LogLevel.ERROR,
+ "EventExpecter dropped event: " + message.toString(), e);
+ }
+ }
+ }
+
+ public RepeatedEventExpecter expectGeckoEvent(final String geckoEvent) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, "waiting for " + geckoEvent);
+ return new GeckoEventExpecter(geckoEvent);
+ }
+
+ public void sendGeckoEvent(final String geckoEvent, final String data) {
+ GeckoAppShell.notifyObservers(geckoEvent, data);
+ }
+
+ public static final class PrefProxy implements PrefsHelper.PrefHandler, PrefWaiter {
+ public static final int MAX_WAIT_MS = 180000;
+
+ /* package */ final PrefHandlerBase target;
+ private final String[] expectedPrefs;
+ private final ArrayList<String> seenPrefs = new ArrayList<>();
+ private boolean finished = false;
+
+ /* package */ PrefProxy(PrefHandlerBase target, String[] expectedPrefs, Assert asserter) {
+ this.target = target;
+ this.expectedPrefs = expectedPrefs;
+ target.asserter = asserter;
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, boolean value) {
+ target.prefValue(pref, value);
+ seenPrefs.add(pref);
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, int value) {
+ target.prefValue(pref, value);
+ seenPrefs.add(pref);
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, String value) {
+ target.prefValue(pref, value);
+ seenPrefs.add(pref);
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public synchronized void finish() {
+ target.finish();
+
+ for (String pref : expectedPrefs) {
+ target.asserter.ok(seenPrefs.remove(pref), "Checking pref was seen", pref);
+ }
+ target.asserter.ok(seenPrefs.isEmpty(), "Checking unexpected prefs",
+ TextUtils.join(", ", seenPrefs));
+
+ finished = true;
+ this.notifyAll();
+ }
+
+ @Override // PrefWaiter
+ public synchronized boolean isFinished() {
+ return finished;
+ }
+
+ @Override // PrefWaiter
+ public void waitForFinish() {
+ waitForFinish(MAX_WAIT_MS, /* failOnTimeout */ true);
+ }
+
+ @Override // PrefWaiter
+ public synchronized void waitForFinish(long timeoutMillis, boolean failOnTimeout) {
+ final long startTime = System.nanoTime();
+ while (!finished) {
+ if (System.nanoTime() - startTime
+ >= timeoutMillis * 1e6 /* ns per ms */) {
+ final String prefsLog = "expected " +
+ TextUtils.join(", ", expectedPrefs) + "; got " +
+ TextUtils.join(", ", seenPrefs.toArray()) + ".";
+ if (failOnTimeout) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ target.asserter.ok(false, "Timeout waiting for pref", prefsLog);
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "Pref timeout (" + prefsLog + ")");
+ }
+ break;
+ }
+ try {
+ this.wait(1000); // Wait for 1 second at a time.
+ } catch (final InterruptedException e) {
+ // Attempt waiting again.
+ }
+ }
+ finished = false;
+ }
+ }
+
+ @Override // Actions
+ public PrefWaiter getPrefs(String[] prefNames, PrefHandlerBase handler) {
+ final PrefProxy proxy = new PrefProxy(handler, prefNames, mAsserter);
+ PrefsHelper.getPrefs(prefNames, proxy);
+ return proxy;
+ }
+
+ @Override // Actions
+ public void setPref(String pref, Object value, boolean flush) {
+ PrefsHelper.setPref(pref, value, flush);
+ }
+
+ @Override // Actions
+ public PrefWaiter addPrefsObserver(String[] prefNames, PrefHandlerBase handler) {
+ final PrefProxy proxy = new PrefProxy(handler, prefNames, mAsserter);
+ PrefsHelper.addObserver(prefNames, proxy);
+ return proxy;
+ }
+
+ @Override // Actions
+ public void removePrefsObserver(PrefWaiter proxy) {
+ PrefsHelper.removeObserver((PrefProxy) proxy);
+ }
+
+ class PaintExpecter implements RepeatedEventExpecter {
+ private static final int MAX_WAIT_MS = 90000;
+
+ private boolean mPaintDone;
+ private boolean mListening;
+
+ private final LayerView mLayerView;
+ private final DrawListener mDrawListener;
+
+ PaintExpecter() {
+ final PaintExpecter expecter = this;
+ mLayerView = GeckoAppShell.getLayerView();
+ mDrawListener = new DrawListener() {
+ @Override
+ public void drawFinished() {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "Received drawFinished notification");
+ expecter.notifyOfEvent();
+ }
+ };
+ mLayerView.addDrawListener(mDrawListener);
+ mListening = true;
+ }
+
+ private synchronized void notifyOfEvent() {
+ mPaintDone = true;
+ this.notifyAll();
+ }
+
+ public synchronized void blockForEvent(long millis, boolean failOnTimeout) {
+ if (!mListening) {
+ throw new IllegalStateException("draw listener not registered");
+ }
+ long startTime = SystemClock.uptimeMillis();
+ long endTime = 0;
+ while (!mPaintDone) {
+ try {
+ this.wait(millis);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ break;
+ }
+ endTime = SystemClock.uptimeMillis();
+ if (!mPaintDone && (endTime - startTime >= millis)) {
+ if (failOnTimeout) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "PaintExpecter", "blockForEvent timeout");
+ }
+ return;
+ }
+ }
+ }
+
+ public synchronized void blockForEvent() {
+ blockForEvent(MAX_WAIT_MS, true);
+ }
+
+ public synchronized String blockForEventData() {
+ blockForEvent();
+ return null;
+ }
+
+ public synchronized String blockForEventDataWithTimeout(long millis) {
+ blockForEvent(millis, false);
+ return null;
+ }
+
+ public synchronized boolean eventReceived() {
+ return mPaintDone;
+ }
+
+ public synchronized void blockUntilClear(long millis) {
+ if (!mListening) {
+ throw new IllegalStateException("draw listener not registered");
+ }
+ if (millis <= 0) {
+ throw new IllegalArgumentException("millis must be > 0");
+ }
+ // wait for at least one event
+ long startTime = SystemClock.uptimeMillis();
+ long endTime = 0;
+ while (!mPaintDone) {
+ try {
+ this.wait(MAX_WAIT_MS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ break;
+ }
+ endTime = SystemClock.uptimeMillis();
+ if (!mPaintDone && (endTime - startTime >= MAX_WAIT_MS)) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "PaintExpecter", "blockUtilClear timeout");
+ return;
+ }
+ }
+ // now wait for a period of millis where we don't get an event
+ startTime = SystemClock.uptimeMillis();
+ while (true) {
+ try {
+ this.wait(millis);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ break;
+ }
+ endTime = SystemClock.uptimeMillis();
+ if (endTime - startTime >= millis) {
+ // success
+ break;
+ }
+
+ // we got a notify() before we could wait long enough, so we need to start over
+ // Note, moving the goal post might have us race against a "drawFinished" flood
+ startTime = endTime;
+ }
+ }
+
+ public synchronized void unregisterListener() {
+ if (!mListening) {
+ throw new IllegalStateException("listener not registered");
+ }
+
+ FennecNativeDriver.log(LogLevel.INFO,
+ "PaintExpecter: no longer listening for events");
+ mLayerView.removeDrawListener(mDrawListener);
+ mListening = false;
+ }
+ }
+
+ public RepeatedEventExpecter expectPaint() {
+ return new PaintExpecter();
+ }
+
+ public void sendSpecialKey(SpecialKey button) {
+ switch(button) {
+ case DOWN:
+ sendKeyCode(Solo.DOWN);
+ break;
+ case UP:
+ sendKeyCode(Solo.UP);
+ break;
+ case LEFT:
+ sendKeyCode(Solo.LEFT);
+ break;
+ case RIGHT:
+ sendKeyCode(Solo.RIGHT);
+ break;
+ case ENTER:
+ sendKeyCode(Solo.ENTER);
+ break;
+ case MENU:
+ sendKeyCode(Solo.MENU);
+ break;
+ case DELETE:
+ sendKeyCode(Solo.DELETE);
+ break;
+ default:
+ mAsserter.ok(false, "sendSpecialKey", "Unknown SpecialKey " + button);
+ break;
+ }
+ }
+
+ public void sendKeyCode(int keyCode) {
+ if (keyCode <= 0 || keyCode > KeyEvent.getMaxKeyCode()) {
+ mAsserter.ok(false, "sendKeyCode", "Unknown keyCode " + keyCode);
+ }
+ mSolo.sendKey(keyCode);
+ }
+
+ @Override
+ public void sendKeys(String input) {
+ mInstr.sendStringSync(input);
+ }
+
+ public void drag(int startingX, int endingX, int startingY, int endingY) {
+ mSolo.drag(startingX, endingX, startingY, endingY, 10);
+ }
+
+ public Cursor querySql(final String dbPath, final String sql) {
+ return new SQLiteBridge(dbPath).rawQuery(sql, null);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java
new file mode 100644
index 000000000..3931b7e20
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java
@@ -0,0 +1,392 @@
+/* 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;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.IntBuffer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.PanningPerfAPI;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import android.app.Activity;
+import android.util.Log;
+import android.view.View;
+
+import com.robotium.solo.Solo;
+
+public class FennecNativeDriver implements Driver {
+ private static final int FRAME_TIME_THRESHOLD = 25; // allow 25ms per frame (40fps)
+
+ private final Activity mActivity;
+ private final Solo mSolo;
+ private final String mRootPath;
+
+ private static String mLogFile;
+ private static LogLevel mLogLevel = LogLevel.INFO;
+
+ public enum LogLevel {
+ DEBUG(1),
+ INFO(2),
+ WARN(3),
+ ERROR(4);
+
+ private final int mValue;
+ LogLevel(int value) {
+ mValue = value;
+ }
+ public boolean isEnabled(LogLevel configuredLevel) {
+ return mValue >= configuredLevel.getValue();
+ }
+ private int getValue() {
+ return mValue;
+ }
+ }
+
+ public FennecNativeDriver(Activity activity, Solo robocop, String rootPath) {
+ mActivity = activity;
+ mSolo = robocop;
+ mRootPath = rootPath;
+ }
+
+ //Information on the location of the Gecko Frame.
+ private boolean mGeckoInfo = false;
+ private int mGeckoTop = 100;
+ private int mGeckoLeft = 0;
+ private int mGeckoHeight= 700;
+ private int mGeckoWidth = 1024;
+
+ private void getGeckoInfo() {
+ View geckoLayout = mActivity.findViewById(R.id.gecko_layout);
+ if (geckoLayout != null) {
+ int[] pos = new int[2];
+ geckoLayout.getLocationOnScreen(pos);
+ mGeckoTop = pos[1];
+ mGeckoLeft = pos[0];
+ mGeckoWidth = geckoLayout.getWidth();
+ mGeckoHeight = geckoLayout.getHeight();
+ mGeckoInfo = true;
+ } else {
+ throw new RoboCopException("Unable to find view gecko_layout");
+ }
+ }
+
+ @Override
+ public int getGeckoTop() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoTop;
+ }
+
+ @Override
+ public int getGeckoLeft() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoLeft;
+ }
+
+ @Override
+ public int getGeckoHeight() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoHeight;
+ }
+
+ @Override
+ public int getGeckoWidth() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoWidth;
+ }
+
+ /** Find the element with given id.
+ *
+ * @return An Element representing the view, or null if the view is not found.
+ */
+ @Override
+ public Element findElement(Activity activity, int id) {
+ return new FennecNativeElement(id, activity);
+ }
+
+ @Override
+ public void startFrameRecording() {
+ PanningPerfAPI.startFrameTimeRecording();
+ }
+
+ @Override
+ public int stopFrameRecording() {
+ final List<Long> frames = PanningPerfAPI.stopFrameTimeRecording();
+ int badness = 0;
+ for (int i = 1; i < frames.size(); i++) {
+ long frameTime = frames.get(i) - frames.get(i - 1);
+ int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
+ // for each frame we miss, add the square of the delay. This
+ // makes large delays much worse than small delays.
+ if (delay > 0) {
+ badness += delay * delay;
+ }
+ }
+
+ // Don't do any averaging of the numbers because really we want to
+ // know how bad the jank was at its worst
+ return badness;
+ }
+
+ @Override
+ public void startCheckerboardRecording() {
+ PanningPerfAPI.startCheckerboardRecording();
+ }
+
+ @Override
+ public float stopCheckerboardRecording() {
+ final List<Float> checkerboard = PanningPerfAPI.stopCheckerboardRecording();
+ float total = 0;
+ for (float val : checkerboard) {
+ total += val;
+ }
+ return total * 100.0f;
+ }
+
+ private LayerView getSurfaceView() {
+ final LayerView layerView = mSolo.getView(LayerView.class, 0);
+
+ if (layerView == null) {
+ log(LogLevel.WARN, "getSurfaceView could not find LayerView");
+ for (final View v : mSolo.getViews()) {
+ log(LogLevel.WARN, " View: " + v);
+ }
+ }
+ return layerView;
+ }
+
+ @Override
+ public PaintedSurface getPaintedSurface() {
+ final LayerView view = getSurfaceView();
+ if (view == null) {
+ return null;
+ }
+
+ final IntBuffer pixelBuffer = view.getPixels();
+
+ // now we need to (1) flip the image, because GL likes to do things up-side-down,
+ // and (2) rearrange the bits from AGBR-8888 to ARGB-8888.
+ int w = view.getWidth();
+ int h = view.getHeight();
+ pixelBuffer.position(0);
+ String mapFile = mRootPath + "/pixels.map";
+
+ FileOutputStream fos = null;
+ BufferedOutputStream bos = null;
+ DataOutputStream dos = null;
+ try {
+ fos = new FileOutputStream(mapFile);
+ bos = new BufferedOutputStream(fos);
+ dos = new DataOutputStream(bos);
+
+ for (int y = h - 1; y >= 0; y--) {
+ for (int x = 0; x < w; x++) {
+ int agbr = pixelBuffer.get();
+ dos.writeInt((agbr & 0xFF00FF00) | ((agbr >> 16) & 0x000000FF) | ((agbr << 16) & 0x00FF0000));
+ }
+ }
+ } catch (IOException e) {
+ throw new RoboCopException("exception with pixel writer on file: " + mapFile);
+ } finally {
+ try {
+ if (dos != null) {
+ dos.flush();
+ dos.close();
+ }
+ // closing dos automatically closes bos
+ if (fos != null) {
+ fos.flush();
+ fos.close();
+ }
+ } catch (IOException e) {
+ log(LogLevel.ERROR, e);
+ throw new RoboCopException("exception closing pixel writer on file: " + mapFile);
+ }
+ }
+ return new PaintedSurface(mapFile, w, h);
+ }
+
+ public int mHeight=0;
+ public int mScrollHeight=0;
+ public int mPageHeight=10;
+
+ @Override
+ public int getScrollHeight() {
+ return mScrollHeight;
+ }
+ @Override
+ public int getPageHeight() {
+ return mPageHeight;
+ }
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void setupScrollHandling() {
+ GeckoApp.getEventDispatcher().registerGeckoThreadListener(new GeckoEventListener() {
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ try {
+ mScrollHeight = message.getInt("y");
+ mHeight = message.getInt("cheight");
+ // We don't want a height of 0. That means it's a bad response.
+ if (mHeight > 0) {
+ mPageHeight = message.getInt("height");
+ }
+ } catch (JSONException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+ "WARNING: ScrollReceived, but message does not contain " +
+ "expected fields: " + e);
+ }
+ }
+ }, "robocop:scroll");
+ }
+
+ /**
+ * Takes a filename, loads the file, and returns a string version of the entire file.
+ */
+ public static String getFile(String filename)
+ {
+ StringBuilder text = new StringBuilder();
+
+ BufferedReader br = null;
+ try {
+ br = new BufferedReader(new FileReader(filename));
+ String line;
+
+ while ((line = br.readLine()) != null) {
+ text.append(line);
+ text.append('\n');
+ }
+ } catch (IOException e) {
+ log(LogLevel.ERROR, e);
+ } finally {
+ try {
+ if (br != null) {
+ br.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ return text.toString();
+ }
+
+ /**
+ * Takes a string of "key=value" pairs split by \n and creates a hash table.
+ */
+ public static Map<String, String> convertTextToTable(String data)
+ {
+ HashMap<String, String> retVal = new HashMap<String, String>();
+
+ String[] lines = data.split("\n");
+ for (int i = 0; i < lines.length; i++) {
+ String[] parts = lines[i].split("=", 2);
+ retVal.put(parts[0].trim(), parts[1].trim());
+ }
+ return retVal;
+ }
+
+ public static void logAllStackTraces(LogLevel level) {
+ StringBuffer sb = new StringBuffer();
+ sb.append("Dumping ALL the threads!\n");
+ Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+ for (Thread t : allStacks.keySet()) {
+ sb.append(t.toString()).append('\n');
+ for (StackTraceElement ste : allStacks.get(t)) {
+ sb.append(ste.toString()).append('\n');
+ }
+ sb.append('\n');
+ }
+ log(level, sb.toString());
+ }
+
+ /**
+ * Set the filename used for logging. If the file already exists, delete it
+ * as a safe-guard against accidentally appending to an old log file.
+ */
+ public static void setLogFile(String filename) {
+ mLogFile = filename;
+ File file = new File(mLogFile);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ public static void setLogLevel(LogLevel level) {
+ mLogLevel = level;
+ }
+
+ public static void log(LogLevel level, String message) {
+ log(level, message, null);
+ }
+
+ public static void log(LogLevel level, Throwable t) {
+ log(level, null, t);
+ }
+
+ public static void log(LogLevel level, String message, Throwable t) {
+ if (mLogFile == null) {
+ throw new RuntimeException("No log file specified!");
+ }
+
+ if (level.isEnabled(mLogLevel)) {
+ PrintWriter pw = null;
+ try {
+ pw = new PrintWriter(new FileWriter(mLogFile, true));
+ if (message != null) {
+ pw.println(message);
+ }
+ if (t != null) {
+ t.printStackTrace(pw);
+ }
+ } catch (IOException ioe) {
+ Log.e("Robocop", "exception with file writer on: " + mLogFile);
+ } finally {
+ if (pw != null) {
+ pw.close();
+ }
+ }
+
+ // PrintWriter doesn't throw IOE but sets an error flag instead,
+ // so check for that
+ if (pw != null && pw.checkError()) {
+ Log.e("Robocop", "exception with file writer on: " + mLogFile);
+ }
+ }
+
+ if (level == LogLevel.INFO) {
+ Log.i("Robocop", message, t);
+ } else if (level == LogLevel.DEBUG) {
+ Log.d("Robocop", message, t);
+ } else if (level == LogLevel.WARN) {
+ Log.w("Robocop", message, t);
+ } else if (level == LogLevel.ERROR) {
+ Log.e("Robocop", message, t);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java
new file mode 100644
index 000000000..2a24344fd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java
@@ -0,0 +1,116 @@
+/* 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;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextSwitcher;
+import android.widget.TextView;
+
+public class FennecNativeElement implements Element {
+ private final Activity mActivity;
+ private final Integer mId;
+ private final String mName;
+
+ public FennecNativeElement(Integer id, Activity activity) {
+ mId = id;
+ mActivity = activity;
+ mName = activity.getResources().getResourceName(id);
+ }
+
+ @Override
+ public Integer getId() {
+ return mId;
+ }
+
+ private boolean mClickSuccess;
+
+ @Override
+ public boolean click() {
+ mClickSuccess = false;
+ RobocopUtils.runOnUiThreadSync(mActivity,
+ new Runnable() {
+ @Override
+ public void run() {
+ View view = mActivity.findViewById(mId);
+ if (view != null) {
+ if (view.performClick()) {
+ mClickSuccess = true;
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+ "Robocop called click on an element with no listener " + mId + " " + mName);
+ }
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "click: unable to find view " + mId + " " + mName);
+ }
+ }
+ });
+ return mClickSuccess;
+ }
+
+ private Object mText;
+
+ @Override
+ public String getText() {
+ mText = null;
+ RobocopUtils.runOnUiThreadSync(mActivity,
+ new Runnable() {
+ @Override
+ public void run() {
+ View v = mActivity.findViewById(mId);
+ if (v instanceof EditText) {
+ EditText et = (EditText)v;
+ mText = et.getEditableText();
+ } else if (v instanceof TextSwitcher) {
+ TextSwitcher ts = (TextSwitcher)v;
+ mText = ((TextView)ts.getCurrentView()).getText();
+ } else if (v instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup)v;
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ if (vg.getChildAt(i) instanceof TextView) {
+ mText = ((TextView)vg.getChildAt(i)).getText();
+ }
+ }
+ } else if (v instanceof TextView) {
+ mText = ((TextView)v).getText();
+ } else if (v == null) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "getText: unable to find view " + mId + " " + mName);
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "getText: unhandled type for view " + mId + " " + mName);
+ }
+ } // end of run() method definition
+ } // end of anonymous Runnable object instantiation
+ );
+ if (mText == null) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+ "getText: Text is null for view " + mId + " " + mName);
+ return null;
+ }
+ return mText.toString();
+ }
+
+ private boolean mDisplayed;
+
+ @Override
+ public boolean isDisplayed() {
+ mDisplayed = false;
+ RobocopUtils.runOnUiThreadSync(mActivity,
+ new Runnable() {
+ @Override
+ public void run() {
+ View view = mActivity.findViewById(mId);
+ if (view != null) {
+ mDisplayed = true;
+ }
+ }
+ });
+ return mDisplayed;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java
new file mode 100644
index 000000000..862f66777
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java
@@ -0,0 +1,74 @@
+/* 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;
+
+
+public class FennecTalosAssert implements Assert {
+
+ public FennecTalosAssert() { }
+
+ /**
+ * Write information to a logfile and logcat
+ */
+ public void dumpLog(String message) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, message);
+ }
+
+ /** Write information to a logfile and logcat */
+ public void dumpLog(String message, Throwable t) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, message, t);
+ }
+
+ /**
+ * Set the filename used for dumpLog.
+ */
+ public void setLogFile(String filename) {
+ FennecNativeDriver.setLogFile(filename);
+ }
+
+ public void setTestName(String testName) { }
+
+ public void endTest() { }
+
+ public void ok(boolean condition, String name, String diag) {
+ if (!condition) {
+ dumpLog("__FAIL" + name + ": " + diag + "__FAIL");
+ }
+ }
+
+ public void is(Object actual, Object expected, String name) {
+ boolean pass = (actual == null ? expected == null : actual.equals(expected));
+ ok(pass, name, "got " + actual + ", expected " + expected);
+ }
+
+ public void isnot(Object actual, Object notExpected, String name) {
+ boolean fail = (actual == null ? notExpected == null : actual.equals(notExpected));
+ ok(!fail, name, "got " + actual + ", expected not " + notExpected);
+ }
+
+ public void ispixel(int actual, int r, int g, int b, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void isnotpixel(int actual, int r, int g, int b, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void todo(boolean condition, String name, String diag) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void todo_is(Object actual, Object expected, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void todo_isnot(Object actual, Object notExpected, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void info(String name, String message) {
+ dumpLog(name + ": " + message);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java
new file mode 100644
index 000000000..208b2c7bd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java
@@ -0,0 +1,40 @@
+/* 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;
+
+import java.util.Map;
+
+import org.mozilla.gecko.tests.BaseRobocopTest;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * An Activity that extracts Robocop settings from robotium.config, launches
+ * Fennec with the Robocop testing parameters, and finishes itself.
+ * <p>
+ * This is intended to be used by local testers using |mach robocop --serve|.
+ */
+public class LaunchFennecWithConfigurationActivity extends Activity {
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final String configFile = FennecNativeDriver.getFile(BaseRobocopTest.DEFAULT_ROOT_PATH + "/robotium.config");
+ final Map<String, String> config = FennecNativeDriver.convertTextToTable(configFile);
+ final Intent intent = BaseRobocopTest.createActivityIntent(config);
+
+ intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+
+ this.finish();
+ this.startActivity(intent);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java
new file mode 100644
index 000000000..17d77b758
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java
@@ -0,0 +1,105 @@
+/* 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+
+import android.graphics.Bitmap;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+public class PaintedSurface {
+ private String mFileName;
+ private int mWidth;
+ private int mHeight;
+ private FileInputStream mPixelFile;
+ private MappedByteBuffer mPixelBuffer;
+
+ public PaintedSurface(String filename, int width, int height) {
+ mFileName = filename;
+ mWidth = width;
+ mHeight = height;
+
+ try {
+ File f = new File(filename);
+ int pixelSize = (int)f.length();
+
+ mPixelFile = new FileInputStream(filename);
+ mPixelBuffer = mPixelFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, pixelSize);
+ } catch (java.io.FileNotFoundException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ } catch (java.io.IOException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ }
+ }
+
+ public final int getWidth() {
+ return mWidth;
+ }
+
+ public final int getHeight() {
+ return mHeight;
+ }
+
+ private int pixelAtIndex(int index) {
+ int b1 = mPixelBuffer.get(index) & 0xFF;
+ int b2 = mPixelBuffer.get(index + 1) & 0xFF;
+ int b3 = mPixelBuffer.get(index + 2) & 0xFF;
+ int b4 = mPixelBuffer.get(index + 3) & 0xFF;
+ int value = (b1 << 24) + (b2 << 16) + (b3 << 8) + (b4 << 0);
+ return value;
+ }
+
+ public final int getPixelAt(int x, int y) {
+ if (mPixelBuffer == null) {
+ throw new RoboCopException("Trying to access PaintedSurface with no active PixelBuffer");
+ }
+
+ if (x >= mWidth || x < 0) {
+ throw new RoboCopException("Trying to access PaintedSurface with invalid x value");
+ }
+
+ if (y >= mHeight || y < 0) {
+ throw new RoboCopException("Trying to access PaintedSurface with invalid y value");
+ }
+
+ // The rows are reversed so row 0 is at the end and we start with the last row.
+ // This is why we do mHeight-y;
+ int index = (x + ((mHeight - y - 1) * mWidth)) * 4;
+ return pixelAtIndex(index);
+ }
+
+ public final String asDataUri() {
+ try {
+ Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
+ for (int y = 0; y < mHeight; y++) {
+ for (int x = 0; x < mWidth; x++) {
+ int index = (x + ((mHeight - y - 1) * mWidth)) * 4;
+ bm.setPixel(x, y, pixelAtIndex(index));
+ }
+ }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write("data:image/png;base64,".getBytes());
+ Base64OutputStream b64 = new Base64OutputStream(out, Base64.NO_WRAP);
+ bm.compress(Bitmap.CompressFormat.PNG, 100, b64);
+ return new String(out.toByteArray());
+ } catch (Exception e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ throw new RoboCopException("Unable to convert surface to a PNG data:uri");
+ }
+ }
+
+ public void close() {
+ try {
+ mPixelFile.close();
+ } catch (Exception e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, e);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java
new file mode 100644
index 000000000..420df818d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java
@@ -0,0 +1,24 @@
+/* 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;
+
+public class RoboCopException extends RuntimeException {
+
+ public RoboCopException() {
+ super();
+ }
+
+ public RoboCopException(String message) {
+ super(message);
+ }
+
+ public RoboCopException(Throwable cause) {
+ super(cause);
+ }
+
+ public RoboCopException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java
new file mode 100644
index 000000000..80ab3396c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java
@@ -0,0 +1,17 @@
+/* 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;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+public class RobocopShare1 extends FragmentActivity {
+ private static Bundle sArguments;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java
new file mode 100644
index 000000000..4874dffb7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java
@@ -0,0 +1,17 @@
+/* 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;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+public class RobocopShare2 extends FragmentActivity {
+ private static Bundle sArguments;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java
new file mode 100644
index 000000000..7a33abfa6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java
@@ -0,0 +1,58 @@
+/* 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;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.app.Activity;
+
+public final class RobocopUtils {
+ private static final int MAX_WAIT_MS = 20000;
+
+ private RobocopUtils() {}
+
+ public static void runOnUiThreadSync(Activity activity, final Runnable runnable) {
+ final AtomicBoolean sentinel = new AtomicBoolean(false);
+
+ // On the UI thread, run the Runnable, then set sentinel to true and wake this thread.
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+
+ synchronized (sentinel) {
+ sentinel.set(true);
+ sentinel.notifyAll();
+ }
+ }
+ }
+ );
+
+
+ // Suspend this thread, until the other thread completes its work or until a timeout is
+ // reached.
+ long startTimestamp = System.currentTimeMillis();
+
+ synchronized (sentinel) {
+ while (!sentinel.get()) {
+ try {
+ sentinel.wait(MAX_WAIT_MS);
+ } catch (InterruptedException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ }
+
+ // Abort if we woke up due to timeout (instead of spuriously).
+ if (System.currentTimeMillis() - startTimestamp >= MAX_WAIT_MS) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "time-out waiting for UI thread");
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java
new file mode 100644
index 000000000..87d5a3c25
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java
@@ -0,0 +1,188 @@
+/* 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;
+
+import java.util.HashSet;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONObject;
+
+// This implements the structured logging API described here: http://mozbase.readthedocs.org/en/latest/mozlog_structured.html
+public class StructuredLogger {
+ private final static HashSet<String> validTestStatus = new HashSet<String>(Arrays.asList("PASS", "FAIL", "TIMEOUT", "NOTRUN", "ASSERT"));
+ private final static HashSet<String> validTestEnd = new HashSet<String>(Arrays.asList("PASS", "FAIL", "OK", "ERROR", "TIMEOUT",
+ "CRASH", "ASSERT", "SKIP"));
+
+ private String mName;
+ private String mComponent;
+ private LoggerCallback mCallback;
+
+ static public interface LoggerCallback {
+ public void call(String output);
+ }
+
+ /* A default logger callback that prints the JSON output to stdout.
+ * This is not to be used in robocop as we write to a log file. */
+ static class StandardLoggerCallback implements LoggerCallback {
+ public void call(String output) {
+ System.out.println(output);
+ }
+ }
+
+ public StructuredLogger(String name, String component, LoggerCallback callback) {
+ mName = name;
+ mComponent = component;
+ mCallback = callback;
+ }
+
+ public StructuredLogger(String name, String component) {
+ this(name, component, new StandardLoggerCallback());
+ }
+
+ public StructuredLogger(String name, LoggerCallback callback) {
+ this(name, null, callback);
+ }
+
+ public StructuredLogger(String name) {
+ this(name, null, new StandardLoggerCallback());
+ }
+
+ public void suiteStart(List<String> tests, Map<String, Object> runInfo) {
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("tests", tests);
+ if (runInfo != null) {
+ data.put("run_info", runInfo);
+ }
+ this.logData("suite_start", data);
+ }
+
+ public void suiteStart(List<String> tests) {
+ this.suiteStart(tests, null);
+ }
+
+ public void suiteEnd() {
+ this.logData("suite_end");
+ }
+
+ public void testStart(String test) {
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("test", test);
+ this.logData("test_start", data);
+ }
+
+ public void testStatus(String test, String subtest, String status, String expected, String message) {
+ status = status.toUpperCase();
+ if (!StructuredLogger.validTestStatus.contains(status)) {
+ throw new IllegalArgumentException("Unrecognized status: " + status);
+ }
+
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("test", test);
+ data.put("subtest", subtest);
+ data.put("status", status);
+
+ if (message != null) {
+ data.put("message", message);
+ }
+ if (!expected.equals(status)) {
+ data.put("expected", expected);
+ }
+
+ this.logData("test_status", data);
+ }
+
+ public void testStatus(String test, String subtest, String status, String message) {
+ this.testStatus(test, subtest, status, "PASS", message);
+ }
+
+ public void testEnd(String test, String status, String expected, String message, Map<String, Object> extra) {
+ status = status.toUpperCase();
+ if (!StructuredLogger.validTestEnd.contains(status)) {
+ throw new IllegalArgumentException("Unrecognized status: " + status);
+ }
+
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("test", test);
+ data.put("status", status);
+
+ if (message != null) {
+ data.put("message", message);
+ }
+ if (extra != null) {
+ data.put("extra", extra);
+ }
+ if (!expected.equals(status) && !status.equals("SKIP")) {
+ data.put("expected", expected);
+ }
+
+ this.logData("test_end", data);
+ }
+
+ public void testEnd(String test, String status, String expected, String message) {
+ this.testEnd(test, status, expected, message, null);
+ }
+
+ public void testEnd(String test, String status, String message) {
+ this.testEnd(test, status, "OK", message, null);
+ }
+
+
+ public void debug(String message) {
+ this.log("debug", message);
+ }
+
+ public void info(String message) {
+ this.log("info", message);
+ }
+
+ public void warning(String message) {
+ this.log("warning", message);
+ }
+
+ public void error(String message) {
+ this.log("error", message);
+ }
+
+ public void critical(String message) {
+ this.log("critical", message);
+ }
+
+ private void log(String level, String message) {
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("message", message);
+ data.put("level", level);
+ this.logData("log", data);
+ }
+
+ private HashMap<String, Object> makeLogData(String action, Map<String, Object> data) {
+ HashMap<String, Object> allData = new HashMap<String, Object>();
+ allData.put("action", action);
+ allData.put("time", System.currentTimeMillis());
+ allData.put("thread", JSONObject.NULL);
+ allData.put("pid", JSONObject.NULL);
+ allData.put("source", mName);
+ if (mComponent != null) {
+ allData.put("component", mComponent);
+ }
+
+ allData.putAll(data);
+
+ return allData;
+ }
+
+ private void logData(String action, Map<String, Object> data) {
+ HashMap<String, Object> logData = this.makeLogData(action, data);
+ JSONObject jsonObject = new JSONObject(logData);
+ mCallback.call(jsonObject.toString());
+ }
+
+ private void logData(String action) {
+ this.logData(action, new HashMap<String, Object>());
+ }
+
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java
new file mode 100644
index 000000000..cadb5df93
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java
@@ -0,0 +1,252 @@
+/* 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.util.ArrayList;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.home.HomePager;
+
+import android.support.v4.view.ViewPager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TabWidget;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * This class is an extension of BaseTest that helps with interaction with about:home
+ * This class contains methods that access the different tabs from about:home, methods that get information like history and bookmarks from the database, edit and remove bookmarks and history items
+ * The purpose of this class is to collect all the logically connected methods that deal with about:home
+ * To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest
+ */
+abstract class AboutHomeTest extends PixelTest {
+ protected enum AboutHomeTabs {
+ RECENT_TABS,
+ HISTORY,
+ TOP_SITES,
+ BOOKMARKS,
+ };
+
+ private final ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{
+ add("TOP_SITES");
+ add("BOOKMARKS");
+ }};
+
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ if (aboutHomeTabs.size() < 4) {
+ // Update it for tablets vs. phones.
+ if (mDevice.type.equals("phone")) {
+ aboutHomeTabs.add(0, AboutHomeTabs.HISTORY.toString());
+ aboutHomeTabs.add(0, AboutHomeTabs.RECENT_TABS.toString());
+ } else {
+ aboutHomeTabs.add(AboutHomeTabs.HISTORY.toString());
+ aboutHomeTabs.add(AboutHomeTabs.RECENT_TABS.toString());
+ }
+ }
+ }
+
+ /**
+ * FIXME: Write new versions of these methods and update their consumers to use the new about:home pages.
+ */
+ protected ListView getHistoryList(String waitText, int expectedChildCount) {
+ return null;
+ }
+ protected ListView getHistoryList(String waitText) {
+ return null;
+ }
+
+ // Returns true if the bookmark is displayed in the bookmarks tab, false otherwise - does not check in folders
+ protected void isBookmarkDisplayed(final String url) {
+ boolean isCorrect = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View bookmark = getDisplayedBookmark(url);
+ return bookmark != null;
+ }
+ }, MAX_WAIT_MS);
+
+ mAsserter.ok(isCorrect, "Checking that " + url + " displayed as a bookmark", url + " displayed");
+ }
+
+ // Loads a bookmark by tapping on the bookmark view in the Bookmarks tab
+ protected void loadBookmark(String url) {
+ View bookmark = getDisplayedBookmark(url);
+ if (bookmark != null) {
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+ mSolo.clickOnView(bookmark);
+ contentEventExpecter.blockForEvent();
+ contentEventExpecter.unregisterListener();
+ } else {
+ mAsserter.ok(false, url + " is not one of the displayed bookmarks", "Please make sure the url provided is bookmarked");
+ }
+ }
+
+ // Opens the bookmark context menu by long-tapping on it
+ protected void openBookmarkContextMenu(String url) {
+ View bookmark = getDisplayedBookmark(url);
+ if (bookmark != null) {
+ mSolo.waitForView(bookmark);
+ mSolo.clickLongOnView(bookmark, LONG_PRESS_TIME);
+ mSolo.waitForDialogToOpen();
+ } else {
+ mAsserter.ok(false, url + " is not one of the displayed bookmarks", "Please make sure the url provided is bookmarked");
+ }
+ }
+
+ // @return the View associated with bookmark for the provided url or null if the link is not bookmarked
+ protected View getDisplayedBookmark(String url) {
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+ mSolo.hideSoftKeyboard();
+ getInstrumentation().waitForIdleSync();
+ ListView bookmarksTabList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ waitForNonEmptyListToLoad(bookmarksTabList);
+ ListAdapter adapter = bookmarksTabList.getAdapter();
+ if (adapter != null) {
+ for (int i = 0; i < adapter.getCount(); i++ ) {
+ // I am unable to click the view taken with getView for some reason so getting the child at i
+ bookmarksTabList.smoothScrollToPosition(i);
+ View bookmarkView = bookmarksTabList.getChildAt(i);
+ if (bookmarkView instanceof android.widget.LinearLayout) {
+ ViewGroup bookmarkItemView = (ViewGroup) bookmarkView;
+ for (int j = 0 ; j < bookmarkItemView.getChildCount(); j++) {
+ View bookmarkContent = bookmarkItemView.getChildAt(j);
+ if (bookmarkContent instanceof android.widget.LinearLayout) {
+ ViewGroup bookmarkItemLayout = (ViewGroup) bookmarkContent;
+ for (int k = 0 ; k < bookmarkItemLayout.getChildCount(); k++) {
+ // Both the title and url are represented as text views so we can cast the view without any issues
+ TextView bookmarkTextContent = (TextView)bookmarkItemLayout.getChildAt(k);
+ if (url.equals(bookmarkTextContent.getText().toString())) {
+ return bookmarkView;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Waits for the given ListView to have a non-empty adapter and be populated
+ * with a minimum number of items.
+ *
+ * This method will return false if the given ListView or its adapter is null,
+ * or if the ListView does not have the minimum number of items.
+ */
+ protected boolean waitForListToLoad(final ListView listView, final int minSize) {
+ Condition listWaitCondition = new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ if (listView == null) {
+ return false;
+ }
+
+ final ListAdapter adapter = listView.getAdapter();
+ if (adapter == null) {
+ return false;
+ }
+
+ return (listView.getCount() - listView.getHeaderViewsCount() >= minSize);
+ }
+ };
+ return waitForCondition(listWaitCondition, MAX_WAIT_MS);
+ }
+
+ protected boolean waitForNonEmptyListToLoad(final ListView listView) {
+ return waitForListToLoad(listView, 1);
+ }
+
+ /**
+ * Get an active ListView with the specified tag .
+ *
+ * This method uses the predefined tags in HomePager.
+ */
+ protected final ListView findListViewWithTag(String tag) {
+ for (ListView listView : mSolo.getCurrentViews(ListView.class)) {
+ final String listTag = (String) listView.getTag();
+ if (TextUtils.isEmpty(listTag)) {
+ continue;
+ }
+
+ if (TextUtils.equals(listTag, tag)) {
+ return listView;
+ }
+ }
+
+ return null;
+ }
+
+ // A wait in order for the about:home tab to be rendered after drag/tab selection
+ private void waitForAboutHomeTab(final int tabIndex) {
+ boolean correctTab = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ ViewPager pager = mSolo.getView(ViewPager.class, 0);
+ return (pager.getCurrentItem() == tabIndex);
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(correctTab, "Checking that the correct tab is displayed", "The " + aboutHomeTabs.get(tabIndex) + " tab is displayed");
+ }
+
+ private void clickAboutHomeTab(AboutHomeTabs tab) {
+ mSolo.clickOnText(tab.toString().replace("_", " "));
+ }
+
+ /**
+ * Swipes to an about:home tab.
+ * @param swipeVector swipeVector Value and direction to swipe (go left for negative, right for positive).
+ */
+ private void swipeAboutHome(int swipeVector) {
+ // Increase swipe width, which will especially impact tablets.
+ int swipeWidth = mDriver.getGeckoWidth() - 1;
+ int swipeHeight = mDriver.getGeckoHeight() / 2;
+
+ if (swipeVector >= 0) {
+ // Emulate swipe motion from right to left.
+ for (int i = 0; i < swipeVector; i++) {
+ mActions.drag(swipeWidth, 0, swipeHeight, swipeHeight);
+ mSolo.sleep(100);
+ }
+ } else {
+ // Emulate swipe motion from left to right.
+ for (int i = 0; i > swipeVector; i--) {
+ mActions.drag(0, swipeWidth, swipeHeight, swipeHeight);
+ mSolo.sleep(100);
+ }
+ }
+ }
+
+ /**
+ * This method can be used to open the different tabs of about:home.
+ */
+ protected void openAboutHomeTab(AboutHomeTabs tab) {
+ focusUrlBar();
+ ViewPager pager = mSolo.getView(ViewPager.class, 0);
+ final int currentTabIndex = pager.getCurrentItem();
+ int tabOffset;
+
+ // Handle tablets by just clicking the visible tab title.
+ if (mDevice.type.equals("tablet")) {
+ clickAboutHomeTab(tab);
+ return;
+ }
+
+ // Handle phones (non-tablets).
+ tabOffset = aboutHomeTabs.indexOf(tab.toString()) - currentTabIndex;
+ swipeAboutHome(tabOffset);
+ waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString()));
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
new file mode 100644
index 000000000..3033524e8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
@@ -0,0 +1,288 @@
+/* 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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.test.ActivityInstrumentationTestCase2;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.robotium.solo.Solo;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.FennecInstrumentationTestRunner;
+import org.mozilla.gecko.FennecMochitestAssert;
+import org.mozilla.gecko.FennecNativeActions;
+import org.mozilla.gecko.FennecNativeDriver;
+import org.mozilla.gecko.FennecTalosAssert;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.updater.UpdateServiceHelper;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+@SuppressWarnings("unchecked")
+public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
+ public static final String LOGTAG = "BaseTest";
+
+ public enum Type {
+ MOCHITEST,
+ TALOS
+ }
+
+ public static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests";
+
+ // How long to wait for a Robocop:Quit message to actually kill Fennec.
+ private static final int ROBOCOP_QUIT_WAIT_MS = 180000;
+
+ /**
+ * The Java Class instance that launches the browser.
+ * <p>
+ * This should always agree with {@link AppConstants#MOZ_ANDROID_BROWSER_INTENT_CLASS}.
+ */
+ public static final Class<? extends Activity> BROWSER_INTENT_CLASS;
+
+ // Use reflection here so we don't have to preprocess this file.
+ static {
+ Class<? extends Activity> cl;
+ try {
+ cl = (Class<? extends Activity>) Class.forName(AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+ } catch (ClassNotFoundException e) {
+ // Oh well.
+ cl = Activity.class;
+ }
+ BROWSER_INTENT_CLASS = cl;
+ }
+
+ protected Assert mAsserter;
+ protected String mLogFile;
+
+ protected String mBaseHostnameUrl;
+ protected String mBaseIpUrl;
+
+ protected Map<String, String> mConfig;
+ protected String mRootPath;
+
+ protected Solo mSolo;
+ protected Driver mDriver;
+ protected Actions mActions;
+
+ protected String mProfile;
+
+ protected StringHelper mStringHelper;
+
+ /**
+ * The browser is started at the beginning of this test. A single test is a
+ * class inheriting from <code>BaseRobocopTest</code> that contains test
+ * methods.
+ * <p>
+ * If a test should not start the browser at the beginning of a test,
+ * specify a different activity class to the one-argument constructor. To do
+ * as little as possible, specify <code>Activity.class</code>.
+ */
+ public BaseRobocopTest() {
+ this((Class<Activity>) BROWSER_INTENT_CLASS);
+ }
+
+ /**
+ * Start the given activity class at the beginning of this test.
+ * <p>
+ * <b>You should use the no-argument constructor in almost all cases.</b>
+ *
+ * @param activityClass to start before this test.
+ */
+ protected BaseRobocopTest(Class<Activity> activityClass) {
+ super(activityClass);
+ }
+
+ /**
+ * Returns the test type: mochitest or talos.
+ * <p>
+ * By default tests are mochitests, but a test can override this method in
+ * order to change its type. Most Robocop tests are mochitests.
+ */
+ protected Type getTestType() {
+ return Type.MOCHITEST;
+ }
+
+ // Member function to allow specialization.
+ protected Intent createActivityIntent() {
+ return BaseRobocopTest.createActivityIntent(mConfig);
+ }
+
+ // Static function to allow re-use.
+ public static Intent createActivityIntent(Map<String, String> config) {
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.putExtra("args", "-no-remote -profile " + config.get("profile"));
+ // Don't show the first run experience.
+ intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
+
+ final String envString = config.get("envvars");
+ if (!TextUtils.isEmpty(envString)) {
+ final String[] envStrings = envString.split(",");
+
+ for (int iter = 0; iter < envStrings.length; iter++) {
+ intent.putExtra("env" + iter, envStrings[iter]);
+ }
+ }
+
+ return intent;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ // Disable the updater.
+ UpdateServiceHelper.setEnabled(false);
+
+ // Load config file from root path (set up by Python script).
+ mRootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
+ if (mRootPath == null) {
+ Log.w("Robocop", "Did not find deviceroot in arguments; falling back to: " + DEFAULT_ROOT_PATH);
+ mRootPath = DEFAULT_ROOT_PATH;
+ }
+ String configFile = FennecNativeDriver.getFile(mRootPath + "/robotium.config");
+ mConfig = FennecNativeDriver.convertTextToTable(configFile);
+ mLogFile = mConfig.get("logfile");
+ mProfile = mConfig.get("profile");
+ mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", "");
+ mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
+
+ // Initialize the asserter.
+ if (getTestType() == Type.TALOS) {
+ mAsserter = new FennecTalosAssert();
+ } else {
+ mAsserter = new FennecMochitestAssert();
+ }
+ mAsserter.setLogFile(mLogFile);
+ mAsserter.setTestName(getClass().getName());
+
+ // Start the activity.
+ final Intent intent = createActivityIntent();
+ setActivityIntent(intent);
+
+ // Set up Robotium.solo and Driver objects
+ Activity tempActivity = getActivity();
+
+ StringHelper.initialize(tempActivity.getResources());
+ mStringHelper = StringHelper.get();
+
+ mSolo = new Solo(getInstrumentation(), tempActivity);
+ mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath);
+ mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter);
+ }
+
+ @Override
+ protected void runTest() throws Throwable {
+ try {
+ super.runTest();
+ } catch (Throwable t) {
+ // save screenshot -- written to /mnt/sdcard/Robotium-Screenshots
+ // as <filename>.jpg
+ mSolo.takeScreenshot("robocop-screenshot-"+getClass().getName());
+ if (mAsserter != null) {
+ mAsserter.dumpLog("Exception caught during test!", t);
+ mAsserter.ok(false, "Exception caught", t.toString());
+ }
+ // re-throw to continue bail-out
+ throw t;
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ try {
+ mAsserter.endTest();
+
+ // By default, we don't quit Fennec on finish, and we don't finish
+ // all opened activities. Not quiting Fennec entirely is intended to
+ // make life better for local testers, who might want to alter a
+ // test that is under development rather than Fennec itself. Not
+ // finishing activities is intended to allow local testers to
+ // manually inspect an activity's state after a test
+ // run. runtestsremote.py sets this to "1". Testers running via an
+ // IDE will not have this set at all.
+ final String quitAndFinish = FennecInstrumentationTestRunner.getFennecArguments()
+ .getString("quit_and_finish"); // null means not specified.
+ if ("1".equals(quitAndFinish)) {
+ // Request the browser force quit and wait for it to take effect.
+ Log.i(LOGTAG, "Requesting force quit.");
+ mActions.sendGeckoEvent("Robocop:Quit", null);
+ mSolo.sleep(ROBOCOP_QUIT_WAIT_MS);
+
+ // If still running, finish activities as recommended by Robotium.
+ Log.i(LOGTAG, "Finishing all opened activities.");
+ mSolo.finishOpenedActivities();
+ } else {
+ // This has the effect of keeping the activity-under-test
+ // around; if we don't set it to null, it is killed, either by
+ // finishOpenedActivities above or super.tearDown below.
+ Log.i(LOGTAG, "Not requesting force quit and trying to keep started activity alive.");
+ setActivity(null);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ super.tearDown();
+ }
+
+ /**
+ * Function to early abort if we can't reach the given HTTP server. Provides local testers
+ * with diagnostic information. Not currently available for TALOS tests, which are rarely run
+ * locally in any case.
+ */
+ public void throwIfHttpGetFails() {
+ if (getTestType() == Type.TALOS) {
+ return;
+ }
+
+ // rawURL to test fetching from. This should be a raw (IP) URL, not an alias
+ // (like mochi.test). We can't (easily) test fetching from the aliases, since
+ // those are managed by Fennec's proxy settings.
+ final String rawUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", "");
+
+ HttpURLConnection urlConnection = null;
+
+ try {
+ urlConnection = (HttpURLConnection) new URL(rawUrl).openConnection();
+
+ final int statusCode = urlConnection.getResponseCode();
+ if (200 != statusCode) {
+ throw new IllegalStateException("Status code: " + statusCode);
+ }
+ } catch (Exception e) {
+ mAsserter.ok(false, "Robocop tests on your device need network/wifi access to reach: [" + rawUrl + "].", e.toString());
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Ensure that the screen on the test device is powered on during tests.
+ */
+ public void throwIfScreenNotOn() {
+ final PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+ mAsserter.ok(pm.isScreenOn(),
+ "Robocop tests need the test device screen to be powered on.", "");
+ }
+
+ protected GeckoProfile getTestProfile() {
+ if (mProfile.startsWith("/")) {
+ return GeckoProfile.get(getActivity(), /* profileName */ null, mProfile);
+ }
+
+ return GeckoProfile.get(getActivity(), mProfile);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
new file mode 100644
index 000000000..a8dfedc4e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
@@ -0,0 +1,976 @@
+/* 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.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.RobocopUtils;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+
+import android.content.ContentValues;
+import android.content.res.AssetManager;
+import android.database.Cursor;
+import android.os.Build;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+import com.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 = 15000;
+ 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;
+
+ protected static final String URL_HTTP_PREFIX = "http://";
+
+ public Device mDevice;
+ protected DatabaseHelper mDatabaseHelper;
+ protected int mScreenMidWidth;
+ protected int mScreenMidHeight;
+ private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
+
+ protected void blockForDelayedStartup() {
+ try {
+ Actions.EventExpecter delayedStartupExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup");
+ delayedStartupExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
+ delayedStartupExpector.unregisterListener();
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in blockForDelayedStartup", e);
+ }
+ }
+
+ protected void blockForGeckoReady() {
+ try {
+ Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready");
+ if (!GeckoThread.isRunning()) {
+ geckoReadyExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
+ }
+ geckoReadyExpector.unregisterListener();
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in blockForGeckoReady", e);
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mDevice = new Device();
+ mDatabaseHelper = new DatabaseHelper(getActivity(), mAsserter);
+
+ // Ensure Robocop tests have access to network, and are run with Display powered on.
+ throwIfHttpGetFails();
+ throwIfScreenNotOn();
+ }
+
+ protected 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());
+ }
+
+ /**
+ * Click on the URL bar to focus it and enter editing mode.
+ */
+ protected final void focusUrlBar() {
+ // Click on the browser toolbar to enter editing mode
+ mSolo.waitForView(R.id.browser_toolbar);
+ final View toolbarView = mSolo.getView(R.id.browser_toolbar);
+ mSolo.clickOnView(toolbarView);
+
+ // Wait for highlighed text to gain focus
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ mSolo.waitForView(R.id.url_edit_text);
+ EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text);
+ if (urlEditText.isInputMethodTarget()) {
+ return true;
+ }
+ return false;
+ }
+ }, MAX_WAIT_ENABLED_TEXT_MS);
+
+ mAsserter.ok(success, "waiting for urlbar text to gain focus", "urlbar text gained focus");
+ }
+
+ protected final void enterUrl(String url) {
+ focusUrlBar();
+
+ final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text);
+
+ // Send the keys for the URL we want to enter
+ mSolo.clearEditText(urlEditView);
+ mSolo.typeText(urlEditView, url);
+
+ // Get the URL text from the URL bar EditText view
+ final String urlBarText = urlEditView.getText().toString();
+ mAsserter.is(url, urlBarText, "URL typed properly");
+ }
+
+ protected final Fragment getBrowserSearch() {
+ final FragmentManager fm = ((FragmentActivity) getActivity()).getSupportFragmentManager();
+ return fm.findFragmentByTag("browser_search");
+ }
+
+ protected final void hitEnterAndWait() {
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+ mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
+ // wait for screen to load
+ contentEventExpecter.blockForEvent();
+ contentEventExpecter.unregisterListener();
+ }
+
+ /**
+ * Load <code>url</code> by sending key strokes to the URL bar UI.
+ *
+ * This method waits synchronously for the <code>DOMContentLoaded</code>
+ * message from Gecko before returning.
+ *
+ * Unless you need to test text entry in the url bar, consider using loadUrl
+ * instead -- it loads pages more directly and quickly.
+ */
+ protected final void inputAndLoadUrl(String url) {
+ enterUrl(url);
+ hitEnterAndWait();
+ }
+
+ /**
+ * Load <code>url</code> using the internal
+ * <code>org.mozilla.gecko.Tabs</code> API.
+ *
+ * This method does not wait for any confirmation from Gecko before
+ * returning -- consider using verifyUrlBarTitle or a similar approach
+ * to wait for the page to load, or at least use loadUrlAndWait.
+ */
+ protected final void loadUrl(final String url) {
+ try {
+ Tabs.getInstance().loadUrl(url);
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in loadUrl", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Load <code>url</code> using the internal
+ * <code>org.mozilla.gecko.Tabs</code> API and wait for DOMContentLoaded.
+ */
+ protected final void loadUrlAndWait(final String url) {
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+ loadUrl(url);
+ contentEventExpecter.blockForEvent();
+ contentEventExpecter.unregisterListener();
+ }
+
+ protected final void closeTab(int tabId) {
+ Tabs tabs = Tabs.getInstance();
+ Tab tab = tabs.getTab(tabId);
+ tabs.closeTab(tab);
+ }
+
+ public final void verifyUrl(String url) {
+ final EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text);
+ String urlBarText = null;
+ if (urlEditText != null) {
+ // wait for a short time for the expected text, in case there is a delay
+ // in updating the view
+ waitForCondition(new VerifyTextViewText(urlEditText, url), VERIFY_URL_TIMEOUT);
+ urlBarText = urlEditText.getText().toString();
+
+ }
+ mAsserter.is(urlBarText, url, "Browser toolbar URL stayed the same");
+ }
+
+ class VerifyTextViewText implements Condition {
+ private final TextView mTextView;
+ private final String mExpected;
+ public VerifyTextViewText(TextView textView, String expected) {
+ mTextView = textView;
+ mExpected = expected;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ String textValue = mTextView.getText().toString();
+ return mExpected.equals(textValue);
+ }
+ }
+
+ class VerifyContentDescription implements Condition {
+ private final View view;
+ private final String expected;
+
+ public VerifyContentDescription(View view, String expected) {
+ this.view = view;
+ this.expected = expected;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ final CharSequence actual = view.getContentDescription();
+ return TextUtils.equals(actual, expected);
+ }
+ }
+
+ protected final String getAbsoluteUrl(String url) {
+ return mBaseHostnameUrl + "/" + url.replaceAll("(^/)", "");
+ }
+
+ protected final String getAbsoluteRawUrl(String url) {
+ return mBaseIpUrl + "/" + url.replaceAll("(^/)", "");
+ }
+
+ /*
+ * Wrapper method for mSolo.waitForCondition with additional logging.
+ */
+ protected final boolean waitForCondition(Condition condition, int timeout) {
+ boolean result = mSolo.waitForCondition(condition, timeout);
+ if (!result) {
+ // Log timeout failure for diagnostic purposes only; a failed wait may
+ // be normal and does not necessarily warrant a test assertion/failure.
+ mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms.");
+ }
+ return result;
+ }
+
+ 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 + "$";
+ final View menuView = mSolo.getView(R.id.menu);
+ mAsserter.isnot(menuView, null, "Menu view is not null");
+ mSolo.clickOnView(menuView, true);
+ mAsserter.ok(waitForEnabledText(itemName), "Waiting for menu item " + itemName, itemName + " is present and enabled");
+ mSolo.clickOnText(itemName);
+ }
+
+ public final void verifyHomePagerHidden() {
+ final View homePagerContainer = mSolo.getView(R.id.home_screen_container);
+
+ boolean rc = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return homePagerContainer.getVisibility() != View.VISIBLE;
+ }
+ }, MAX_WAIT_HOME_PAGER_HIDDEN_MS);
+
+ if (!rc) {
+ mAsserter.ok(rc, "Verify HomePager is hidden", "HomePager is hidden");
+ }
+ }
+
+ public final void verifyUrlBarTitle(String url) {
+ mAsserter.isnot(url, null, "The url argument is not null");
+
+ final String expected;
+ if (mStringHelper.ABOUT_HOME_URL.equals(url)) {
+ expected = mStringHelper.ABOUT_HOME_TITLE;
+ } else if (url.startsWith(URL_HTTP_PREFIX)) {
+ expected = url.substring(URL_HTTP_PREFIX.length());
+ } else {
+ expected = url;
+ }
+
+ final TextView urlBarTitle = (TextView) mSolo.getView(R.id.url_bar_title);
+ String pageTitle = null;
+ if (urlBarTitle != null) {
+ // Wait for the title to make sure it has been displayed in case the view
+ // does not update fast enough
+ waitForCondition(new VerifyTextViewText(urlBarTitle, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS);
+ pageTitle = urlBarTitle.getText().toString();
+ }
+ mAsserter.is(pageTitle, expected, "Page title is correct");
+ }
+
+ public final void verifyUrlInContentDescription(String url) {
+ mAsserter.isnot(url, null, "The url argument is not null");
+
+ final String expected;
+ if (mStringHelper.ABOUT_HOME_URL.equals(url)) {
+ expected = mStringHelper.ABOUT_HOME_TITLE;
+ } else if (url.startsWith(URL_HTTP_PREFIX)) {
+ expected = url.substring(URL_HTTP_PREFIX.length());
+ } else {
+ expected = url;
+ }
+
+ final View urlDisplayLayout = mSolo.getView(R.id.display_layout);
+ assertNotNull("ToolbarDisplayLayout is not null", urlDisplayLayout);
+
+ String actualUrl = null;
+
+ // Wait for the title to make sure it has been displayed in case the view
+ // does not update fast enough
+ waitForCondition(new VerifyContentDescription(urlDisplayLayout, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS);
+ if (urlDisplayLayout.getContentDescription() != null) {
+ actualUrl = urlDisplayLayout.getContentDescription().toString();
+ }
+
+ mAsserter.is(actualUrl, expected, "Url is correct");
+ }
+
+ public final void verifyTabCount(int expectedTabCount) {
+ Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter);
+ String tabCountText = tabCount.getText();
+ int tabCountInt = Integer.parseInt(tabCountText);
+ mAsserter.is(tabCountInt, expectedTabCount, "The correct number of tabs are opened");
+ }
+
+ public void verifyPinned(final boolean isPinned, final String gridItemTitle) {
+ boolean viewFound = waitForText(gridItemTitle);
+ mAsserter.ok(viewFound, "Found top site title: " + gridItemTitle, null);
+
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ // We set the left compound drawable (index 0) to the pin icon.
+ final TextView gridItemTextView = mSolo.getText(gridItemTitle);
+ return isPinned == (gridItemTextView.getCompoundDrawables()[0] != null);
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success, "Top site item was pinned: " + isPinned, null);
+ }
+
+ public void pinTopSite(String gridItemTitle) {
+ verifyPinned(false, gridItemTitle);
+ mSolo.clickLongOnText(gridItemTitle);
+ boolean dialogOpened = mSolo.waitForDialogToOpen();
+ mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null);
+ boolean pinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_PIN_SITE);
+ mAsserter.ok(pinSiteFound, "Found pin site menu item", null);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_PIN_SITE);
+ verifyPinned(true, gridItemTitle);
+ }
+
+ public void unpinTopSite(String gridItemTitle) {
+ verifyPinned(true, gridItemTitle);
+ mSolo.clickLongOnText(gridItemTitle);
+ boolean dialogOpened = mSolo.waitForDialogToOpen();
+ mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null);
+ boolean unpinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_UNPIN_SITE);
+ mAsserter.ok(unpinSiteFound, "Found unpin site menu item", null);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_UNPIN_SITE);
+ verifyPinned(false, gridItemTitle);
+ }
+
+ // Used to perform clicks on pop-up buttons without having to close the virtual keyboard
+ public void clickOnButton(String label) {
+ final Button button = mSolo.getButton(label);
+ try {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ button.performClick();
+ }
+ });
+ } catch (Throwable throwable) {
+ mAsserter.ok(false, "Unable to click the button","Was unable to click button ");
+ }
+ }
+
+ private void waitForAnimationsToFinish() {
+ // Ideally we'd actually wait for animations to finish but since we have
+ // no good way of doing that, we just wait an arbitrary unit of time.
+ mSolo.sleep(3500);
+ }
+
+ public void addTab() {
+ mSolo.clickOnView(mSolo.getView(R.id.tabs));
+ waitForAnimationsToFinish();
+
+ // 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));
+ waitForAnimationsToFinish();
+
+ // 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.
+ loadUrlAndWait(url);
+ }
+
+ public void closeAddedTabs() {
+ for(int tabID : mKnownTabIDs) {
+ closeTab(tabID);
+ }
+ }
+
+ // A temporary tabs list/grid holder while the list and grid views are being transitioned to
+ // RecyclerViews (bug 1116415 and bug 1310081).
+ private static class TabsView {
+ private AdapterView<ListAdapter> gridView;
+ private RecyclerView listView;
+
+ public TabsView(View view) {
+ if (view instanceof RecyclerView) {
+ listView = (RecyclerView) view;
+ } else {
+ gridView = (AdapterView<ListAdapter>) view;
+ }
+ }
+
+ public void bringPositionIntoView(int index) {
+ if (gridView != null) {
+ gridView.setSelection(index);
+ } else {
+ listView.scrollToPosition(index);
+ }
+ }
+
+ public View getViewAtIndex(int index) {
+ if (gridView != null) {
+ return gridView.getChildAt(index - gridView.getFirstVisiblePosition());
+ } else {
+ final RecyclerView.ViewHolder itemViewHolder = listView.findViewHolderForLayoutPosition(index);
+ return itemViewHolder == null ? null : itemViewHolder.itemView;
+ }
+ }
+
+ public void post(Runnable runnable) {
+ if (gridView != null) {
+ gridView.post(runnable);
+ } else {
+ listView.post(runnable);
+ }
+ }
+ }
+ /**
+ * Gets the AdapterView of the tabs list.
+ *
+ * @return List view in the tabs panel
+ */
+ private final TabsView getTabsLayout() {
+ Element tabs = mDriver.findElement(getActivity(), R.id.tabs);
+ tabs.click();
+ return new TabsView(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 TabsView view = getTabsLayout();
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ view.bringPositionIntoView(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() {
+ // Index is relative to all views in the list.
+ childView[0] = view.getViewAtIndex(index);
+ }
+ });
+ }
+ });
+
+ 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));
+ }
+
+ public final void runOnUiThreadSync(Runnable runnable) {
+ RobocopUtils.runOnUiThreadSync(getActivity(), runnable);
+ }
+
+ /* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */
+ public void toggleBookmark() {
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText("Settings");
+
+ // On ICS+ phones, there is no button labeled "Bookmarks"
+ // instead we have to just dig through every button on the screen
+ ArrayList<View> images = mSolo.getCurrentViews();
+ for (int i = 0; i < images.size(); i++) {
+ final View view = images.get(i);
+ boolean found = false;
+ found = "Bookmark".equals(view.getContentDescription());
+
+ // on older android versions, try looking at the button's text
+ if (!found) {
+ if (view instanceof TextView) {
+ found = "Bookmark".equals(((TextView)view).getText());
+ }
+ }
+
+ if (found) {
+ int[] xy = new int[2];
+ view.getLocationOnScreen(xy);
+
+ final int viewWidth = view.getWidth();
+ final int viewHeight = view.getHeight();
+ final float x = xy[0] + (viewWidth / 2.0f);
+ float y = xy[1] + (viewHeight / 2.0f);
+
+ mSolo.clickOnScreen(x, y);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ class Navigation {
+ private final String devType;
+ private final String osVersion;
+
+ public Navigation(Device mDevice) {
+ devType = mDevice.type;
+ osVersion = mDevice.version;
+ }
+
+ public void back() {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+
+ if (devType.equals("tablet")) {
+ Element backBtn = mDriver.findElement(getActivity(), R.id.back);
+ backBtn.click();
+ } else {
+ mSolo.goBack();
+ }
+
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+ }
+
+ public void forward() {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+
+ if (devType.equals("tablet")) {
+ mSolo.waitForView(R.id.forward);
+ mSolo.clickOnView(mSolo.getView(R.id.forward));
+ } else {
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText("^New Tab$");
+ if (!osVersion.equals("2.x")) {
+ mSolo.waitForView(R.id.forward);
+ mSolo.clickOnView(mSolo.getView(R.id.forward));
+ } else {
+ mSolo.clickOnText("^Forward$");
+ }
+ ensureMenuClosed();
+ }
+
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+ }
+
+ // DEPRECATED!
+ // Use BaseTest.toggleBookmark() in new code.
+ public void bookmark() {
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText("^New Tab$");
+ if (mSolo.searchText("^Bookmark$")) {
+ // This is the Android 2.x so the button has text
+ mSolo.clickOnText("^Bookmark$");
+ } else {
+ Element bookmarkBtn = mDriver.findElement(getActivity(), R.id.bookmark);
+ if (bookmarkBtn != null) {
+ // We are on Android 4.x so the button is an image button
+ bookmarkBtn.click();
+ }
+ }
+ ensureMenuClosed();
+ }
+
+ // On some devices, the menu may not be dismissed after clicking on an
+ // item. Close it here.
+ private void ensureMenuClosed() {
+ if (mSolo.searchText("^New Tab$")) {
+ mSolo.goBack();
+ }
+ }
+ }
+
+ /**
+ * Gets the string representation of a stack trace.
+ *
+ * @param t Throwable to get stack trace for
+ * @return Stack trace as a string
+ */
+ public static String getStackTraceString(Throwable t) {
+ StringWriter sw = new StringWriter();
+ t.printStackTrace(new PrintWriter(sw));
+ return sw.toString();
+ }
+
+ /**
+ * Condition class that waits for a view, and allows callers access it when done.
+ */
+ private class DescriptionCondition<T extends View> implements Condition {
+ public T mView;
+ private final String mDescr;
+ private final Class<T> mCls;
+
+ public DescriptionCondition(Class<T> cls, String descr) {
+ mDescr = descr;
+ mCls = cls;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ mView = findViewWithContentDescription(mCls, mDescr);
+ return (mView != null);
+ }
+ }
+
+ /**
+ * Wait for a view with the specified description .
+ */
+ public <T extends View> T waitForViewWithDescription(Class<T> cls, String description) {
+ DescriptionCondition<T> c = new DescriptionCondition<T>(cls, description);
+ waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS);
+ return c.mView;
+ }
+
+ /**
+ * Get an active view with the specified description .
+ */
+ public <T extends View> T findViewWithContentDescription(Class<T> cls, String description) {
+ for (T view : mSolo.getCurrentViews(cls)) {
+ final String descr = (String) view.getContentDescription();
+ if (TextUtils.isEmpty(descr)) {
+ continue;
+ }
+
+ if (TextUtils.equals(description, descr)) {
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Abstract class for running small test cases within a BaseTest.
+ */
+ abstract class TestCase implements Runnable {
+ /**
+ * Implement tests here. setUp and tearDown for the test case
+ * should be handled by the parent test. This is so we can avoid the
+ * overhead of starting Gecko and creating profiles.
+ */
+ protected abstract void test() throws Exception;
+
+ @Override
+ public void run() {
+ try {
+ test();
+ } catch (Exception e) {
+ mAsserter.ok(false,
+ "Test " + this.getClass().getName() + " threw exception: " + e,
+ "");
+ }
+ }
+ }
+
+ /**
+ * Set the preference and wait for it to change before proceeding with the test.
+ */
+ public void setPreferenceAndWaitForChange(final String name, final Object value) {
+ blockForGeckoReady();
+ mActions.setPref(name, value, /* flush */ false);
+
+ // Wait for confirmation of the pref change before proceeding with the test.
+ mActions.getPrefs(new String[] { name }, new Actions.PrefHandlerBase() {
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean changedValue) {
+ mAsserter.is(pref, name, "Expecting correct pref name");
+ mAsserter.ok(value instanceof Boolean, "Expecting boolean pref", "");
+ mAsserter.is(changedValue, value, "Expecting matching pref value");
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, int changedValue) {
+ mAsserter.is(pref, name, "Expecting correct pref name");
+ mAsserter.ok(value instanceof Integer, "Expecting int pref", "");
+ mAsserter.is(changedValue, value, "Expecting matching pref value");
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, String changedValue) {
+ mAsserter.is(pref, name, "Expecting correct pref name");
+ mAsserter.ok(value instanceof CharSequence, "Expecting string pref", "");
+ mAsserter.is(changedValue, value, "Expecting matching pref value");
+ }
+
+ }).waitForFinish();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
new file mode 100644
index 000000000..5a1d09f8c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
@@ -0,0 +1,135 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import android.util.DisplayMetrics;
+
+import com.robotium.solo.Condition;
+
+/**
+ * This class covers interactions with the context menu opened from web content
+ */
+abstract class ContentContextMenuTest extends PixelTest {
+ private static final int MAX_TEST_TIMEOUT = 30000; // 30 seconds (worst case)
+
+ // This method opens the context menu of any web content. It assumes that the page is already loaded
+ protected void openWebContentContextMenu(String waitText) {
+ DisplayMetrics dm = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+ // The web content we are trying to open the context menu for should be positioned at the top of the page, at least 60px high and aligned to the middle
+ float top = mDriver.getGeckoTop() + 30 * dm.density;
+ float left = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() / 2;
+
+ mAsserter.dumpLog("long-clicking at "+left+", "+top);
+ mSolo.clickLongOnScreen(left, top);
+ waitForText(waitText);
+ }
+
+ protected void verifyContextMenuItems(String[] items) {
+ // Test that the menu items are displayed
+ if (!mSolo.searchText(items[0])) {
+ openWebContentContextMenu(items[0]); // Open the context menu if it is not already
+ }
+
+ for (String option:items) {
+ mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available");
+ }
+ }
+
+ protected void openTabFromContextMenu(String contextMenuOption, int expectedTabCount) {
+ if (!mSolo.searchText(contextMenuOption)) {
+ openWebContentContextMenu(contextMenuOption); // Open the context menu if it is not already
+ }
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ mSolo.clickOnText(contextMenuOption);
+ tabEventExpecter.blockForEvent();
+ tabEventExpecter.unregisterListener();
+ verifyTabCount(expectedTabCount);
+ }
+
+ protected void verifyTabs(String[] items) {
+ if (!mSolo.searchText(items[0])) {
+ openWebContentContextMenu(items[0]);
+ }
+
+ for (String option:items) {
+ mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available");
+ }
+ }
+
+ protected void switchTabs(String tab) {
+ if (!mSolo.searchText(tab)) {
+ openWebContentContextMenu(tab);
+ }
+ mSolo.clickOnText(tab);
+ }
+
+
+ protected void verifyCopyOption(String copyOption, final String copiedText) {
+ if (!mSolo.searchText(copyOption)) {
+ openWebContentContextMenu(copyOption); // Open the context menu if it is not already
+ }
+ mSolo.clickOnText(copyOption);
+ boolean correctText = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ final String clipboardText = Clipboard.getText();
+ mAsserter.dumpLog("Clipboard text = " + clipboardText + " , expected text = " + copiedText);
+ return clipboardText.contains(copiedText);
+ }
+ }, MAX_TEST_TIMEOUT);
+ mAsserter.ok(correctText, "Checking if the text is correctly copied", "The text was correctly copied");
+ }
+
+
+
+ protected void verifyShareOption(String shareOption, String pageTitle) {
+ waitForText(pageTitle); // Even if this fails, it won't assert
+ if (!mSolo.searchText(shareOption)) {
+ openWebContentContextMenu(shareOption); // Open the context menu if it is not already
+ }
+ mSolo.clickOnText(shareOption);
+ mAsserter.ok(waitForText(shareOption), "Checking that the share pop-up is displayed", "The pop-up has been displayed");
+
+ // Close the Share Link option menu and wait for the page to be focused again
+ mSolo.goBack();
+ waitForText(pageTitle);
+ }
+
+ protected void verifyViewImageOption(String viewImageOption, final String imageUrl, String pageTitle) {
+ if (!mSolo.searchText(viewImageOption)) {
+ openWebContentContextMenu(viewImageOption);
+ }
+ mSolo.clickOnText(viewImageOption);
+
+ boolean viewedImage = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ final Element urlBarElement = mDriver.findElement(getActivity(), R.id.url_edit_text);
+ final String loadedUrl = urlBarElement.getText();
+ return loadedUrl.contentEquals(imageUrl);
+ }
+ }, MAX_TEST_TIMEOUT);
+ mAsserter.ok(viewedImage, "Checking if the image is correctly viewed", "The image was correctly viewed");
+
+ mSolo.goBack();
+ waitForText(pageTitle);
+ }
+
+ protected void verifyBookmarkLinkOption(String bookmarkOption, String link) {
+ if (!mSolo.searchText(bookmarkOption)) {
+ openWebContentContextMenu(bookmarkOption); // Open the context menu if it is not already
+ }
+ mSolo.clickOnText(bookmarkOption);
+ mAsserter.ok(waitForText("Bookmark added"), "Waiting for the Bookmark added toaster notification", "The notification has been displayed");
+ mAsserter.ok(mDatabaseHelper.isBookmark(link), "Checking if the link has been added as a bookmark", "The link has been bookmarked");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java
new file mode 100644
index 000000000..5496c97d2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java
@@ -0,0 +1,255 @@
+/* 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.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+/*
+ * ContentProviderTest provides the infrastructure to run content provider
+ * tests in an controlled/isolated environment, guaranteeing that your tests
+ * will not affect or be affected by any UI-related code. This is basically
+ * a heavily adapted port of Android's ProviderTestCase2 to work on Mozilla's
+ * infrastructure.
+ *
+ * For some tests, we need to have access to UI parts, or at least launch
+ * the activity so the assets with test data become available, which requires
+ * that we derive this test from BaseTest and consequently pull in some more
+ * UI code than we'd ideally want. Furthermore, we need to pass the
+ * Activity and not the instrumentation Context for the UI part to find some
+ * of its required resources.
+ * Similarly, we need to pass the Activity instead of the Instrumentation
+ * Context down to some of our users (still wrapped in the Delegating Provider)
+ * because they will stop working correctly if we do not. A typical problem
+ * is that databases used in the ContentProvider will be attempted to be
+ * opened twice.
+ */
+abstract class ContentProviderTest extends BaseTest {
+ protected ContentProvider mProvider;
+ protected ChangeRecordingMockContentResolver mResolver;
+ protected ArrayList<Runnable> mTests;
+ protected String mDatabaseName;
+ protected String mProviderAuthority;
+ protected IsolatedContext mProviderContext;
+
+ private class ContentProviderMockContext extends MockContext {
+ @Override
+ public Resources getResources() {
+ // We will fail to find some resources if we don't point
+ // at the original activity.
+ return ((Context)getActivity()).getResources();
+ }
+
+ @Override
+ public String getPackageName() {
+ return getInstrumentation().getContext().getPackageName();
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ return getInstrumentation().getContext().getPackageResourcePath();
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ return getInstrumentation().getContext().getDir(this.getClass().getSimpleName() + "_" + name, mode);
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ return getInstrumentation().getContext().getSharedPreferences(name, mode);
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ return getInstrumentation().getContext().getApplicationInfo();
+ }
+ }
+
+ protected class DelegatingTestContentProvider extends ContentProvider {
+ ContentProvider mTargetProvider;
+
+ public DelegatingTestContentProvider(ContentProvider targetProvider) {
+ super();
+ mTargetProvider = targetProvider;
+ }
+
+ private Uri appendTestParam(Uri uri) {
+ try {
+ return appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
+ } catch (Exception e) {}
+
+ return null;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return mTargetProvider.onCreate();
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return mTargetProvider.getType(uri);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return mTargetProvider.delete(appendTestParam(uri), selection, selectionArgs);
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return mTargetProvider.insert(appendTestParam(uri), values);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ return mTargetProvider.update(appendTestParam(uri), values,
+ selection, selectionArgs);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ return mTargetProvider.query(appendTestParam(uri), projection, selection,
+ selectionArgs, sortOrder);
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ return mTargetProvider.applyBatch(operations);
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ return mTargetProvider.bulkInsert(appendTestParam(uri), values);
+ }
+
+ public ContentProvider getTargetProvider() {
+ return mTargetProvider;
+ }
+ }
+
+ /*
+ * A MockContentResolver that records each URI that is supplied to
+ * notifyChange. Warning: the list of changed URIs is not
+ * synchronized.
+ */
+ protected class ChangeRecordingMockContentResolver extends MockContentResolver {
+ public final LinkedList<Uri> notifyChangeList = new LinkedList<Uri>();
+
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ notifyChangeList.addLast(uri);
+
+ super.notifyChange(uri, observer, syncToNetwork);
+ }
+ }
+
+ /**
+ * Factory function that makes new ContentProvider instances.
+ * <p>
+ * We want a fresh provider each test, so this should be invoked in
+ * <code>setUp</code> before each individual test.
+ */
+ protected static Callable<ContentProvider> sBrowserProviderCallable = new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new BrowserProvider();
+ }
+ };
+
+ private void setUpContentProvider(ContentProvider targetProvider) throws Exception {
+ mResolver = new ChangeRecordingMockContentResolver();
+
+ final String filenamePrefix = this.getClass().getSimpleName() + ".";
+ RenamingDelegatingContext targetContextWrapper =
+ new RenamingDelegatingContext(
+ new ContentProviderMockContext(),
+ (Context)getActivity(),
+ filenamePrefix);
+
+ mProviderContext = new IsolatedContext(mResolver, targetContextWrapper);
+
+ targetProvider.attachInfo(mProviderContext, null);
+
+ mProvider = new DelegatingTestContentProvider(targetProvider);
+ mProvider.attachInfo(mProviderContext, null);
+
+ mResolver.addProvider(mProviderAuthority, mProvider);
+ }
+
+ public static Uri appendUriParam(Uri uri, String param, String value) {
+ return uri.buildUpon().appendQueryParameter(param, value).build();
+ }
+
+ public void setTestName(String testName) {
+ mAsserter.setTestName(this.getClass().getName() + " - " + testName);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ throw new UnsupportedOperationException("You should call setUp(authority, databaseName) instead");
+ }
+
+ public void setUp(Callable<ContentProvider> contentProviderFactory, String authority, String databaseName) throws Exception {
+ super.setUp();
+
+ mTests = new ArrayList<Runnable>();
+ mDatabaseName = databaseName;
+
+ mProviderAuthority = authority;
+
+ setUpContentProvider(contentProviderFactory.call());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (Build.VERSION.SDK_INT >= 11) {
+ mProvider.shutdown();
+ }
+
+ if (mDatabaseName != null) {
+ mProviderContext.deleteDatabase(mDatabaseName);
+ }
+
+ super.tearDown();
+ }
+
+ public AssetManager getAssetManager() {
+ return getInstrumentation().getContext().getAssets();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java
new file mode 100644
index 000000000..c87dc2432
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java
@@ -0,0 +1,170 @@
+/* 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/. */
+
+/* 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.util.ArrayList;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.BrowserDB;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+class DatabaseHelper {
+ protected enum BrowserDataType {BOOKMARKS, HISTORY};
+ private final Activity mActivity;
+ private final Assert mAsserter;
+
+ public DatabaseHelper(Activity activity, Assert asserter) {
+ mActivity = activity;
+ mAsserter = asserter;
+ }
+ /**
+ * This method can be used to check if an URL is present in the bookmarks database
+ */
+ protected boolean isBookmark(String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ return getProfileDB().isBookmark(resolver, url);
+ }
+
+ protected Uri buildUri(BrowserDataType dataType) {
+ Uri uri = null;
+ if (dataType == BrowserDataType.BOOKMARKS || dataType == BrowserDataType.HISTORY) {
+ uri = Uri.parse("content://" + AppConstants.ANDROID_PACKAGE_NAME + ".db.browser/" + dataType.toString().toLowerCase());
+ } else {
+ mAsserter.ok(false, "The wrong data type has been provided = " + dataType.toString(), "Please provide the correct data type");
+ }
+ uri = uri.buildUpon().appendQueryParameter("profile", GeckoProfile.DEFAULT_PROFILE)
+ .appendQueryParameter("sync", "true").build();
+ return uri;
+ }
+
+ /**
+ * Adds a bookmark.
+ */
+ protected void addMobileBookmark(String title, String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ getProfileDB().addBookmark(resolver, title, url);
+ mAsserter.ok(true, "Inserting a new bookmark", "Inserting the bookmark with the title = " + title + " and the url = " + url);
+ }
+
+ /**
+ * Updates the title and keyword of a bookmark with the given URL.
+ *
+ * Warning: This method assumes that there's only one bookmark with the given URL.
+ */
+ protected void updateBookmark(String url, String title, String keyword) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ // Get the id for the bookmark with the given URL.
+ Cursor c = null;
+ try {
+ c = getProfileDB().getBookmarkForUrl(resolver, url);
+ if (!c.moveToFirst()) {
+ mAsserter.ok(false, "Getting bookmark with url", "Couldn't find bookmark with url = " + url);
+ return;
+ }
+
+ int id = c.getInt(c.getColumnIndexOrThrow("_id"));
+ getProfileDB().updateBookmark(resolver, id, url, title, keyword);
+
+ mAsserter.ok(true, "Updating bookmark", "Updating bookmark with url = " + url);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ protected void deleteBookmark(String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ getProfileDB().removeBookmarksWithURL(resolver, url);
+ }
+
+ protected void deleteHistoryItem(String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ getProfileDB().removeHistoryEntry(resolver, url);
+ }
+
+ // About the same implementation as getFolderIdFromGuid from LocalBrowserDB because it is declared private and we can't use reflections to access it
+ protected long getFolderIdFromGuid(String guid) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ long folderId = -1L;
+ final Uri bookmarksUri = buildUri(BrowserDataType.BOOKMARKS);
+
+ Cursor c = null;
+ try {
+ c = resolver.query(bookmarksUri,
+ new String[] { "_id" },
+ "guid = ?",
+ new String[] { guid },
+ null);
+ if (c.moveToFirst()) {
+ folderId = c.getLong(c.getColumnIndexOrThrow("_id"));
+ }
+
+ if (folderId == -1) {
+ mAsserter.ok(false, "Trying to get the folder id" ,"We did not get the correct folder id");
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return folderId;
+ }
+
+ /**
+ * Returns all of the bookmarks or history entries in a database.
+ *
+ * @return an ArrayList of the urls in the Firefox for Android Bookmarks or History databases.
+ */
+ protected ArrayList<String> getBrowserDBUrls(BrowserDataType dataType) {
+ final ArrayList<String> browserData = new ArrayList<String>();
+ final ContentResolver resolver = mActivity.getContentResolver();
+
+ Cursor cursor = null;
+ final BrowserDB db = getProfileDB();
+ if (dataType == BrowserDataType.HISTORY) {
+ cursor = db.getAllVisitedHistory(resolver);
+ } else if (dataType == BrowserDataType.BOOKMARKS) {
+ cursor = db.getBookmarksInFolder(resolver, getFolderIdFromGuid("mobile"));
+ }
+
+ if (cursor == null) {
+ mAsserter.ok(false, "We could not retrieve any data from the database", "The cursor was null");
+ return browserData;
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ // Nothing here, but that's OK -- maybe there are zero results. The calling test will fail.
+ return browserData;
+ }
+
+ do {
+ // The URL field may be null for folders in the structure of the Bookmarks table for Firefox. Eliminate those.
+ if (cursor.getString(cursor.getColumnIndex("url")) != null) {
+ browserData.add(cursor.getString(cursor.getColumnIndex("url")));
+ }
+ } while (cursor.moveToNext());
+
+ return browserData;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ protected BrowserDB getProfileDB() {
+ return BrowserDB.from(mActivity);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java
new file mode 100644
index 000000000..a71f8fd49
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java
@@ -0,0 +1,107 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.JavascriptBridge;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * Extended to write tests using JavascriptBridge, which allows Java and JS to communicate back-and-forth.
+ * If you don't need back-and-forth communication, consider {@link JavascriptTest}.
+ *
+ * To write a test:
+ * * Extend this class
+ * * Add your javascript file to the base robocop directory (see where `testJavascriptBridge.js` is located)
+ * * In the main test method, call {@link #blockForReadyAndLoadJS(String)} with your javascript file name
+ * (don't include the path) or if you're loading a non-harness url, be sure to call {@link GeckoHelper#blockForReady()}
+ * * You can access js calls via the {@link #getJS()} method
+ * - Read {@link JavascriptBridge} javadoc for more information about using the API.
+ */
+public class JavascriptBridgeTest extends UITest {
+
+ private static final long WAIT_GET_FROM_JS_MILLIS = 20000;
+
+ private JavascriptBridge js;
+
+ // Feel free to implement additional return types.
+ private boolean isAsyncValueSet;
+ private String asyncValueStr;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ js = new JavascriptBridge(this);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ js.disconnect();
+ super.tearDown();
+ }
+
+ public JavascriptBridge getJS() {
+ return js;
+ }
+
+ protected void blockForReadyAndLoadJS(final String jsFilename) {
+ NavigationHelper.enterAndLoadUrl(mStringHelper.getHarnessUrlForJavascript(jsFilename));
+ }
+
+ /**
+ * Used to retrieve values from js when it's required to call async methods (e.g. promises).
+ * This method will block until the value is retrieved else timeout.
+ *
+ * This method is not thread-safe.
+ *
+ * Ideally, we could just have Javascript call Java when the callback completes but Java won't
+ * listen for messages unless we call into JS again (bug 1253467).
+ *
+ * To use this method:
+ * * Call this method with a name argument, henceforth known as `varName`. Note that it will be capitalized
+ * in all function names.
+ * * Create a js function, `"getAsync" + varName` (e.g. if `varName == "clientId`, the function is
+ * `getAsyncClientId`) of no args. This function should call the async get method and assign a global variable to
+ * the return value.
+ * * Create a js function, `"pollGetAsync" + varName` (e.g. `pollGetAsyncClientId`) of no args. It should call
+ * `java.asyncCall('blockingFromJsResponseString', ...` with two args: a boolean if the async value has been set yet
+ * and a String with the global return value (`null` or `undefined` are acceptable if the value has not been set).
+ */
+ public String getBlockingFromJsString(final String varName) {
+ isAsyncValueSet = false;
+ final String fnSuffix = capitalize(varName);
+ getJS().syncCall("getAsync" + fnSuffix); // Initiate async callback
+
+ final long timeoutMillis = System.currentTimeMillis() + WAIT_GET_FROM_JS_MILLIS;
+ do {
+ // Avoid sleeping! The async callback may have already completed so
+ // we test for completion here, rather than in the loop predicate.
+ getJS().syncCall("pollGetAsync" + fnSuffix);
+ if (isAsyncValueSet) {
+ break;
+ }
+
+ if (System.currentTimeMillis() > timeoutMillis) {
+ fFail("Retrieving " + varName + " from JS has timed out");
+ }
+ try {
+ Thread.sleep(500, 0); // Give time for JS to complete its operation. (emulator one core?)
+ } catch (final InterruptedException e) { }
+ } while (true);
+
+ return asyncValueStr;
+ }
+
+ public void blockingFromJsResponseString(final boolean isValueSet, final String value) {
+ this.isAsyncValueSet = isValueSet;
+ this.asyncValueStr = value;
+ }
+
+ private String capitalize(final String str) {
+ return str.substring(0, 1).toUpperCase() + str.substring(1);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java
new file mode 100644
index 000000000..52893510d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java
@@ -0,0 +1,87 @@
+/* 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 org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.tests.helpers.JavascriptBridge;
+import org.mozilla.gecko.tests.helpers.JavascriptMessageParser;
+
+import android.util.Log;
+
+/**
+ * Extended to test stand-alone Javascript in automation. If you're looking to test JS interactions
+ * with Java, see {@link JavascriptBridgeTest}.
+ *
+ * There are also other tests that run stand-alone javascript but are more difficult for the mobile
+ * team to run (e.g. xpcshell).
+ */
+public class JavascriptTest extends BaseTest {
+ private static final String LOGTAG = "JavascriptTest";
+ private static final String EVENT_TYPE = JavascriptBridge.EVENT_TYPE;
+
+ // Calculate these once, at initialization. isLoggable is too expensive to
+ // have in-line in each log call.
+ private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
+ private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+
+ private final String javascriptUrl;
+
+ public JavascriptTest(String javascriptUrl) {
+ super();
+ this.javascriptUrl = javascriptUrl;
+ }
+
+ public void testJavascript() throws Exception {
+ blockForGeckoReady();
+
+ doTestJavascript();
+ }
+
+ protected void doTestJavascript() throws Exception {
+ // We want to be waiting for Robocop messages before the page is loaded
+ // because the test harness runs each test in the suite (and possibly
+ // completes testing) before the page load event is fired.
+ final Actions.EventExpecter expecter = mActions.expectGeckoEvent(EVENT_TYPE);
+ mAsserter.dumpLog("Registered listener for " + EVENT_TYPE);
+
+ final String url = getAbsoluteUrl(mStringHelper.getHarnessUrlForJavascript(javascriptUrl));
+ mAsserter.dumpLog("Loading JavaScript test from " + url);
+ loadUrl(url);
+
+ final JavascriptMessageParser testMessageParser =
+ new JavascriptMessageParser(mAsserter, false);
+ try {
+ while (!testMessageParser.isTestFinished()) {
+ if (logVerbose) {
+ Log.v(LOGTAG, "Waiting for " + EVENT_TYPE);
+ }
+ String data = expecter.blockForEventData();
+ if (logVerbose) {
+ Log.v(LOGTAG, "Got event with data '" + data + "'");
+ }
+
+ JSONObject o = new JSONObject(data);
+ String innerType = o.getString("innerType");
+ if (!"progress".equals(innerType)) {
+ throw new Exception("Unexpected event innerType " + innerType);
+ }
+
+ String message = o.getString("message");
+ if (message == null) {
+ throw new Exception("Progress message must not be null");
+ }
+ testMessageParser.logMessage(message);
+ }
+
+ if (logDebug) {
+ Log.d(LOGTAG, "Got test finished message");
+ }
+ } finally {
+ expecter.unregisterListener();
+ mAsserter.dumpLog("Unregistered listener for " + EVENT_TYPE);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java
new file mode 100644
index 000000000..5b8254e99
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java
@@ -0,0 +1,210 @@
+/* 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 org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.PrefsHelper;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+class MotionEventHelper {
+ private static final String LOGTAG = "RobocopMotionEventHelper";
+
+ private static final long DRAG_EVENTS_PER_SECOND = 20; // 20 move events per second when doing a drag
+
+ private final Instrumentation mInstrumentation;
+ private final int mSurfaceOffsetX;
+ private final int mSurfaceOffsetY;
+ private final LayerView layerView;
+ private boolean mApzEnabled;
+ private float mTouchStartTolerance;
+ private final int mDpi;
+
+ public MotionEventHelper(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY) {
+ mInstrumentation = inst;
+ mSurfaceOffsetX = surfaceOffsetX;
+ mSurfaceOffsetY = surfaceOffsetY;
+ layerView = GeckoAppShell.getLayerView();
+ mApzEnabled = false;
+ mTouchStartTolerance = 0.0f;
+ mDpi = GeckoAppShell.getDpi();
+ Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
+ PrefsHelper.getPref("layers.async-pan-zoom.enabled", new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, boolean value) {
+ mApzEnabled = value;
+ }
+ });
+ PrefsHelper.getPref("apz.touch_start_tolerance", new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, String value) {
+ mTouchStartTolerance = Float.parseFloat(value);
+ }
+ });
+ }
+
+ public long down(float x, float y) {
+ Log.d(LOGTAG, "Triggering down at (" + x + "," + y + ")");
+ long downTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
+ try {
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+ return downTime;
+ }
+
+ public long move(long downTime, float x, float y) {
+ return move(downTime, SystemClock.uptimeMillis(), x, y);
+ }
+
+ public long move(long downTime, long moveTime, float x, float y) {
+ Log.d(LOGTAG, "Triggering move to (" + x + "," + y + ")");
+ MotionEvent event = MotionEvent.obtain(downTime, moveTime, MotionEvent.ACTION_MOVE, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
+ try {
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+ return downTime;
+ }
+
+ public long up(long downTime, float x, float y) {
+ return up(downTime, SystemClock.uptimeMillis(), x, y);
+ }
+
+ public long up(long downTime, long upTime, float x, float y) {
+ Log.d(LOGTAG, "Triggering up at (" + x + "," + y + ")");
+ MotionEvent event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
+ try {
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+ return -1L;
+ }
+
+ private long movePastTouchStartTolerance(float startX, float startY, float endX, float endY) {
+ long downTime = 0;
+ float eventDx = (endX - startX);
+ float eventDy = (endY - startY);
+ if (mApzEnabled && (mTouchStartTolerance > 0.0f) && (eventDx != 0 || eventDy !=0)) {
+ final float dragLength = (float)Math.sqrt((eventDx * eventDx) + (eventDy * eventDy));
+ final float extraDragLength = (float)Math.ceil(mTouchStartTolerance * mDpi);
+ final float extraDx = (eventDx / dragLength) * extraDragLength * (eventDx > 0.0f ? -1.0f : 1.0f);
+ final float extraDy = (eventDy / dragLength) * extraDragLength * (eventDy > 0.0f ? -1.0f : 1.0f);
+ downTime = down(startX + extraDx, startY + extraDy);
+ downTime = move(downTime, startX + extraDx, startY + extraDy);
+ try {
+ Thread.sleep(1000L / DRAG_EVENTS_PER_SECOND);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ } else {
+ downTime = down(startX, startY);
+ }
+ return downTime;
+ }
+
+ public Thread dragAsync(final float startX, final float startY, final float endX, final float endY, final long durationMillis) {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ layerView.setIsLongpressEnabled(false);
+
+ int numEvents = (int)(durationMillis * DRAG_EVENTS_PER_SECOND / 1000);
+ float eventDx = (endX - startX) / numEvents;
+ float eventDy = (endY - startY) / numEvents;
+ long downTime = movePastTouchStartTolerance(startX, startY, endX, endY);
+ for (int i = 0; i < numEvents - 1; i++) {
+ downTime = move(downTime, startX + (eventDx * i), startY + (eventDy * i));
+ try {
+ Thread.sleep(1000L / DRAG_EVENTS_PER_SECOND);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+ // sleep a bit before sending the last move so that the calculated
+ // fling velocity is low and we don't end up doing a fling afterwards.
+ try {
+ Thread.sleep(1000L);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ // do the last one using endX/endY directly to avoid rounding errors
+ downTime = move(downTime, endX, endY);
+ downTime = up(downTime, endX, endY);
+
+ layerView.setIsLongpressEnabled(true);
+ }
+ };
+ t.start();
+ return t;
+ }
+
+ public void dragSync(float startX, float startY, float endX, float endY, long durationMillis) {
+ try {
+ dragAsync(startX, startY, endX, endY, durationMillis).join();
+ mInstrumentation.waitForIdleSync();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+
+ public void dragSync(float startX, float startY, float endX, float endY) {
+ dragSync(startX, startY, endX, endY, 1000);
+ }
+
+ public Thread flingAsync(final float startX, final float startY, final float endX, final float endY, final float velocity) {
+ // note that the first move after the touch-down is used to get over the panning threshold, and
+ // is basically cancelled out. this means we need to generate (at least) two move events, with
+ // the last move event hitting the target velocity. to do this we just slice the total distance
+ // in half, assuming the first half will get us over the panning threshold and the second half
+ // will trigger the fling.
+ final float dx = (endX - startX) / 2;
+ final float dy = (endY - startY) / 2;
+ float distance = (float) Math.sqrt((dx * dx) + (dy * dy));
+ final long time = (long)(distance / velocity);
+ if (time <= 0) {
+ throw new IllegalArgumentException( "Fling parameters require too small a time period" );
+ }
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ long downTime = down(startX, startY);
+ downTime = move(downTime, downTime + time, startX + dx, startY + dy);
+ downTime = move(downTime, downTime + time + time, endX, endY);
+ downTime = up(downTime, downTime + time + time + time, endX, endY);
+ }
+ };
+ t.start();
+ return t;
+ }
+
+ public void flingSync(float startX, float startY, float endX, float endY, float velocity) {
+ try {
+ flingAsync(startX, startY, endX, endY, velocity).join();
+ mInstrumentation.waitForIdleSync();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+
+ public void tap(float x, float y) {
+ long downTime = down(x, y);
+ downTime = up(downTime, x, y);
+ }
+
+ public void doubleTap(float x, float y) {
+ tap(x, y);
+ tap(x, y);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java
new file mode 100644
index 000000000..508c6b197
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java
@@ -0,0 +1,224 @@
+/* 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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.app.Instrumentation;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+class MotionEventReplayer {
+ private static final String LOGTAG = "RobocopMotionEventReplayer";
+
+ // the inner dimensions of the window on which the motion event capture was taken from
+ private static final int CAPTURE_WINDOW_WIDTH = 720;
+ private static final int CAPTURE_WINDOW_HEIGHT = 1038;
+
+ private final Instrumentation mInstrumentation;
+ private final int mSurfaceOffsetX;
+ private final int mSurfaceOffsetY;
+ private final int mSurfaceWidth;
+ private final int mSurfaceHeight;
+ private final Map<String, Integer> mActionTypes;
+ private Method mObtainNanoMethod;
+
+ public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) {
+ mInstrumentation = inst;
+ mSurfaceOffsetX = surfaceOffsetX;
+ mSurfaceOffsetY = surfaceOffsetY;
+ mSurfaceWidth = surfaceWidth;
+ mSurfaceHeight = surfaceHeight;
+ Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
+
+ mActionTypes = new HashMap<String, Integer>();
+ mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL);
+ mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN);
+ mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE);
+ mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN);
+ mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP);
+ mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP);
+ }
+
+ private int parseAction(String action) {
+ int index = 0;
+
+ // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by
+ // pointer index in parentheses, like ACTION_POINTER_UP(1)
+ int beginParen = action.indexOf("(");
+ if (beginParen >= 0) {
+ int endParen = action.indexOf(")", beginParen + 1);
+ index = Integer.parseInt(action.substring(beginParen + 1, endParen));
+ action = action.substring(0, beginParen);
+ }
+
+ return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ }
+
+ private int parseInt(String value) {
+ if (value == null) {
+ return 0;
+ }
+ if (value.startsWith("0x")) {
+ return Integer.parseInt(value.substring(2), 16);
+ }
+ return Integer.parseInt(value);
+ }
+
+ private float scaleX(float value) {
+ return value * mSurfaceWidth / CAPTURE_WINDOW_WIDTH;
+ }
+
+ private float scaleY(float value) {
+ return value * mSurfaceHeight / CAPTURE_WINDOW_HEIGHT;
+ }
+
+ public void replayEvents(InputStream eventDescriptions)
+ throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
+ {
+ // As an example, a line in the input stream might look like:
+ //
+ // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412,
+ // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0,
+ // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329,
+ // downTime=21972329, deviceId=6, source=0x1002 }
+ //
+ // These can be generated by printing out event.toString() in LayerView's
+ // onTouchEvent function on a phone running Ice Cream Sandwich. Different
+ // Android versions have different serializations of the motion event, and this
+ // code could probably be modified to parse other serializations if needed.
+ Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}");
+ Map<String, String> eventProperties = new HashMap<String, String>();
+
+ boolean firstEvent = true;
+ long timeDelta = 0L;
+ long lastEventTime = 0L;
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions));
+ try {
+ for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) {
+ Matcher m = p.matcher(eventStr);
+ if (! m.find()) {
+ // this line doesn't have any MotionEvent data, skip it
+ continue;
+ }
+
+ // extract the key-value pairs from the description and store them
+ // in the eventProperties table
+ StringTokenizer keyValues = new StringTokenizer(m.group(1), ",");
+ while (keyValues.hasMoreTokens()) {
+ String keyValue = keyValues.nextToken();
+ String key = keyValue.substring(0, keyValue.indexOf('=')).trim();
+ String value = keyValue.substring(keyValue.indexOf('=') + 1).trim();
+ eventProperties.put(key, value);
+ }
+
+ // set up the values we need to build the MotionEvent
+ long downTime = Long.parseLong(eventProperties.get("downTime"));
+ long eventTime = Long.parseLong(eventProperties.get("eventTime"));
+ int action = parseAction(eventProperties.get("action"));
+ float pressure = 1.0f;
+ float size = 1.0f;
+ int metaState = parseInt(eventProperties.get("metaState"));
+ float xPrecision = 1.0f;
+ float yPrecision = 1.0f;
+ int deviceId = 0;
+ int edgeFlags = parseInt(eventProperties.get("edgeFlags"));
+ int source = parseInt(eventProperties.get("source"));
+ int flags = parseInt(eventProperties.get("flags"));
+
+ int pointerCount = parseInt(eventProperties.get("pointerCount"));
+ int[] pointerIds = new int[pointerCount];
+ Object pointerData;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+ for (int i = 0; i < pointerCount; i++) {
+ pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
+ pointerCoords[i] = new MotionEvent.PointerCoords();
+ pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
+ pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
+ }
+ pointerData = pointerCoords;
+ } else {
+ // pre-gingerbread we have to use a hidden API to create the motion event, and we have
+ // to create a flattened list of floats rather than an array of PointerCoords
+ final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA
+ final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X
+ final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y
+ float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA];
+ for (int i = 0; i < pointerCount; i++) {
+ pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
+ sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] =
+ mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
+ sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] =
+ mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
+ }
+ pointerData = sampleData;
+ }
+
+ // we want to adjust the timestamps on all the generated events so that they line up with
+ // the time that this function is executing on-device.
+ long now = SystemClock.uptimeMillis();
+ if (firstEvent) {
+ timeDelta = now - eventTime;
+ firstEvent = false;
+ }
+ downTime += timeDelta;
+ eventTime += timeDelta;
+
+ // we also generate the events in "real-time" (i.e. have delays between events that
+ // correspond to the delays in the event timestamps).
+ if (now < eventTime) {
+ try {
+ Thread.sleep(eventTime - now);
+ } catch (InterruptedException ie) {
+ }
+ }
+
+ // and finally we dispatch the event
+ MotionEvent event;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
+ pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
+ xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
+ } else {
+ // pre-gingerbread we have to use a hidden API to accomplish this
+ if (mObtainNanoMethod == null) {
+ mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class,
+ long.class, long.class, int.class, int.class, pointerIds.getClass(),
+ pointerData.getClass(), int.class, float.class, float.class,
+ int.class, int.class);
+ }
+ event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime,
+ eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData,
+ metaState, xPrecision, yPrecision, deviceId, edgeFlags);
+ }
+ try {
+ Log.v(LOGTAG, "Injecting " + event.toString());
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+
+ eventProperties.clear();
+ }
+ } finally {
+ br.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java
new file mode 100644
index 000000000..a33ecf241
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java
@@ -0,0 +1,117 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+abstract class PixelTest extends BaseTest {
+ private static final long PAINT_CLEAR_DELAY = 10000; // milliseconds
+
+ protected final PaintedSurface loadAndGetPainted(String url) {
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ loadUrlAndWait(url);
+ verifyHomePagerHidden();
+ paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY);
+ paintExpecter.unregisterListener();
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ protected final void loadAndPaint(String url) {
+ PaintedSurface painted = loadAndGetPainted(url);
+ painted.close();
+ }
+
+ protected final PaintedSurface reloadAndGetPainted() {
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText(mStringHelper.RELOAD_LABEL);
+ mSolo.clickOnText(mStringHelper.RELOAD_LABEL);
+
+ paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY);
+ paintExpecter.unregisterListener();
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ protected final void reloadAndPaint() {
+ PaintedSurface painted = reloadAndGetPainted();
+ painted.close();
+ }
+
+ protected final PaintedSurface waitForPaint(Actions.RepeatedEventExpecter expecter) {
+ expecter.blockUntilClear(PAINT_CLEAR_DELAY);
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ protected final PaintedSurface waitWithNoPaint(Actions.RepeatedEventExpecter expecter) {
+ try {
+ Thread.sleep(PAINT_CLEAR_DELAY);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ mAsserter.is(expecter.eventReceived(), false, "Checking gecko didn't draw unnecessarily");
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ // this matches the algorithm in robocop_boxes.html
+ protected final int[] getBoxColorAt(int x, int y) {
+ int r = ((int)Math.floor(x / 3) % 256);
+ r = r & 0xF8;
+ int g = (x + y) % 256;
+ g = g & 0xFC;
+ int b = ((int)Math.floor(y / 3) % 256);
+ b = b & 0xF8;
+ return new int[] { r, g, b };
+ }
+
+ /**
+ * Checks the top-left corner of the visible area of the page is at (x,y) of robocop_boxes.html.
+ */
+ protected final void checkScrollWithBoxes(PaintedSurface painted, int x, int y) {
+ int[] color = getBoxColorAt(x, y);
+ mAsserter.ispixel(painted.getPixelAt(0, 0), color[0], color[1], color[2], "Pixel at 0, 0");
+ color = getBoxColorAt(x + 100, y);
+ mAsserter.ispixel(painted.getPixelAt(100, 0), color[0], color[1], color[2], "Pixel at 100, 0");
+ color = getBoxColorAt(x, y + 100);
+ mAsserter.ispixel(painted.getPixelAt(0, 100), color[0], color[1], color[2], "Pixel at 0, 100");
+ color = getBoxColorAt(x + 100, y + 100);
+ mAsserter.ispixel(painted.getPixelAt(100, 100), color[0], color[1], color[2], "Pixel at 100, 100");
+ }
+
+ /**
+ * Loads the robocop_boxes.html file and verifies that we are positioned at (0,0) on it.
+ * @param url URL of the robocop_boxes.html file.
+ * @return The painted surface after rendering the file.
+ */
+ protected final void loadAndVerifyBoxes(String url) {
+ PaintedSurface painted = loadAndGetPainted(url);
+ try {
+ checkScrollWithBoxes(painted, 0, 0);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java
new file mode 100644
index 000000000..eb808b542
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java
@@ -0,0 +1,56 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import android.util.Log;
+
+import org.json.JSONObject;
+
+/**
+ * A base test class for selection handler tests.
+ */
+abstract class SelectionHandlerTest extends UITest {
+ private static final String geckoEventString = "Robocop:testSelectionHandler";
+ private final String url;
+
+ public SelectionHandlerTest(String url) {
+ this.url = url;
+ }
+
+ public void testSelection() {
+ GeckoHelper.blockForReady();
+
+ Actions.EventExpecter robocopTestExpecter = getActions().expectGeckoEvent(geckoEventString);
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ while (!test(robocopTestExpecter)) {
+ // do nothing
+ }
+
+ robocopTestExpecter.unregisterListener();
+ }
+
+ protected boolean test(Actions.EventExpecter expecter) {
+ final JSONObject eventData;
+ try {
+ eventData = new JSONObject(expecter.blockForEventData());
+ } catch(Exception ex) {
+ // Log and ignore
+ getAsserter().ok(false, "JS Test", "Error decoding data " + ex);
+ return false;
+ }
+
+ if (eventData.has("result")) {
+ getAsserter().ok(eventData.optBoolean("result"), "JS Test", eventData.optString("msg"));
+ } else if (eventData.has("todo")) {
+ getAsserter().todo(eventData.optBoolean("todo"), "JS TODO", eventData.optString("msg"));
+ }
+
+ EventDispatcher.sendResponse(eventData, new JSONObject());
+ return eventData.optBoolean("done", false);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java
new file mode 100644
index 000000000..e07a9750c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java
@@ -0,0 +1,407 @@
+/* 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.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.FennecMochitestAssert;
+
+public abstract class SessionTest extends BaseTest {
+ protected Navigation mNavigation;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mNavigation = new Navigation(mDevice);
+ }
+
+ /**
+ * A generic session object representing a collection of items that has a
+ * selected index.
+ */
+ protected abstract class SessionObject<T> {
+ private final int mIndex;
+ private final T[] mItems;
+
+ public SessionObject(int index, T... items) {
+ mIndex = index;
+ mItems = items;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public T[] getItems() {
+ return mItems;
+ }
+ }
+
+ protected class PageInfo {
+ private final String url;
+ private final String title;
+
+ public PageInfo(String key) {
+ if (key.startsWith("about:")) {
+ url = key;
+ } else {
+ url = getPage(key);
+ }
+ title = key;
+ }
+ }
+
+ protected class SessionTab extends SessionObject<PageInfo> {
+ public SessionTab(int index, PageInfo... items) {
+ super(index, items);
+ }
+ }
+
+ protected class Session extends SessionObject<SessionTab> {
+ public Session(int index, SessionTab... items) {
+ super(index, items);
+ }
+ }
+
+ /**
+ * Walker for visiting items in a browser-like navigation order.
+ */
+ protected abstract class NavigationWalker<T> {
+ private final T[] mItems;
+ private final int mIndex;
+
+ public NavigationWalker(SessionObject<T> obj) {
+ mItems = obj.getItems();
+ mIndex = obj.getIndex();
+ }
+
+ /**
+ * Walks over the list of items, calling the onItem() callback for each.
+ *
+ * The selected item is the first item visited. Each item after the
+ * selected item is then visited in ascending index order. Finally, the
+ * list is iterated in reverse, and each item before the selected item
+ * is visited in descending index order.
+ */
+ public void walk() {
+ onItem(mItems[mIndex], mIndex);
+ for (int i = mIndex + 1; i < mItems.length; i++) {
+ goForward();
+ onItem(mItems[i], i);
+ }
+ if (mIndex > 0) {
+ for (int i = mItems.length - 2; i >= 0; i--) {
+ goBack();
+ if (i < mIndex) {
+ onItem(mItems[i], i);
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback when an item is visited during a walk.
+ *
+ * Only one callback is executed per item.
+ */
+ public abstract void onItem(T item, int currentIndex);
+
+ /**
+ * Callback executed for each back step of the walk.
+ */
+ public void goBack() {}
+
+ /**
+ * Callback executed for each forward step of the walk.
+ */
+ public void goForward() {}
+ }
+
+ /**
+ * Loads a set of tabs in the browser specified by the given session.
+ *
+ * @param session Session to load
+ */
+ protected void loadSessionTabs(Session session) {
+ // Verify initial about:home tab
+ verifyTabCount(1);
+ verifyUrl(mStringHelper.ABOUT_HOME_URL);
+
+ SessionTab[] tabs = session.getItems();
+ for (int i = 0; i < tabs.length; i++) {
+ final SessionTab tab = tabs[i];
+ final PageInfo[] pages = tab.getItems();
+
+ // New tabs always start with about:home, so make sure about:home
+ // is always the first entry.
+ mAsserter.is(pages[0].url, mStringHelper.ABOUT_HOME_URL, "first page in tab is " +
+ mStringHelper.ABOUT_HOME_URL);
+
+ // If this is the first tab, the tab already exists, so no need to
+ // create a new one. Otherwise, create a new tab if we're loading
+ // the first the first page in the set.
+ if (i > 0) {
+ addTab();
+ }
+
+ for (int j = 1; j < pages.length; j++) {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+
+ loadUrl(pages[j].url);
+
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+ }
+
+ final int index = tab.getIndex();
+ for (int j = pages.length - 1; j > index; j--) {
+ mNavigation.back();
+ }
+ }
+
+ selectTabAt(session.getIndex());
+ }
+
+ /**
+ * Verifies that the set of open tabs matches the given session.
+ *
+ * @param session Session to verify
+ */
+ protected void verifySessionTabs(Session session) {
+ verifyTabCount(session.getItems().length);
+
+ (new NavigationWalker<SessionTab>(session) {
+ boolean mFirstTabVisited;
+
+ @Override
+ public void onItem(SessionTab tab, int currentIndex) {
+ // The first tab to check should already be selected at startup
+ if (mFirstTabVisited) {
+ selectTabAt(currentIndex);
+ } else {
+ mFirstTabVisited = true;
+ }
+
+ (new NavigationWalker<PageInfo>(tab) {
+ @Override
+ public void onItem(PageInfo page, int currentIndex) {
+ final String text;
+ if (mStringHelper.ABOUT_HOME_URL.equals(page.url)) {
+ text = mStringHelper.TITLE_PLACE_HOLDER;
+ } else if (page.url.startsWith(URL_HTTP_PREFIX)) {
+ text = page.url.substring(URL_HTTP_PREFIX.length());
+ } else {
+ text = page.url;
+ }
+ waitForText(text);
+
+ verifyUrlBarTitle(page.url);
+ }
+
+ @Override
+ public void goBack() {
+ mNavigation.back();
+ }
+
+ @Override
+ public void goForward() {
+ mNavigation.forward();
+ }
+ }).walk();
+ }
+ }).walk();
+ }
+
+ /**
+ * Gets session restore JSON corresponding to the open session.
+ *
+ * The JSON format follows the format used in Gecko for session restore and
+ * should be interchangeable with the Gecko's generated sessionstore.js.
+ *
+ * @param session Session to serialize
+ * @return JSON string of session
+ */
+ protected String buildSessionJSON(Session session) {
+ final SessionTab[] sessionTabs = session.getItems();
+ String sessionString = null;
+
+ try {
+ final JSONArray tabs = new JSONArray();
+
+ for (int i = 0; i < sessionTabs.length; i++) {
+ final JSONObject tab = new JSONObject();
+ final JSONArray entries = new JSONArray();
+ final SessionTab sessionTab = sessionTabs[i];
+ final PageInfo[] pages = sessionTab.getItems();
+
+ for (int j = 0; j < pages.length; j++) {
+ final PageInfo page = pages[j];
+ final JSONObject entry = new JSONObject();
+ entry.put("url", page.url);
+ entry.put("title", page.title);
+ entries.put(entry);
+ }
+
+ tab.put("entries", entries);
+ tab.put("index", sessionTab.getIndex() + 1);
+ tabs.put(tab);
+ }
+
+ JSONObject window = new JSONObject();
+ window.put("tabs", tabs);
+ window.put("selected", session.getIndex() + 1);
+ sessionString = new JSONObject().put("windows", new JSONArray().put(window)).toString();
+ } catch (JSONException e) {
+ mAsserter.ok(false, "JSON exception", getStackTraceString(e));
+ }
+
+ return sessionString;
+ }
+
+ /**
+ * @see SessionTest#verifySessionJSON(Session, String, Assert)
+ */
+ protected void verifySessionJSON(Session session, String sessionString) {
+ verifySessionJSON(session, sessionString, mAsserter);
+ }
+
+ /**
+ * Verifies a session JSON string against the given session.
+ *
+ * @param session Session to verify against
+ * @param sessionString JSON string to verify
+ * @param asserter Assert class to use during verification
+ */
+ protected void verifySessionJSON(Session session, String sessionString, Assert asserter) {
+ final SessionTab[] sessionTabs = session.getItems();
+
+ try {
+ final JSONObject window = new JSONObject(sessionString).getJSONArray("windows").getJSONObject(0);
+ final JSONArray tabs = window.getJSONArray("tabs");
+ final int optSelected = window.optInt("selected", -1);
+
+ asserter.is(optSelected, session.getIndex() + 1, "selected tab matches");
+
+ for (int i = 0; i < tabs.length(); i++) {
+ final JSONObject tab = tabs.getJSONObject(i);
+ final int index = tab.getInt("index");
+ final JSONArray entries = tab.getJSONArray("entries");
+ final SessionTab sessionTab = sessionTabs[i];
+ final PageInfo[] pages = sessionTab.getItems();
+
+ asserter.is(index, sessionTab.getIndex() + 1, "selected page index matches");
+
+ for (int j = 0; j < entries.length(); j++) {
+ final JSONObject entry = entries.getJSONObject(j);
+ final String url = entry.getString("url");
+ final String title = entry.optString("title");
+ final PageInfo page = pages[j];
+
+ asserter.is(url, page.url, "URL in JSON matches session URL");
+ if (!page.url.startsWith("about:")) {
+ asserter.is(title, page.title, "title in JSON matches session title");
+ }
+ }
+ }
+ } catch (JSONException e) {
+ asserter.ok(false, "JSON exception", getStackTraceString(e));
+ }
+ }
+
+ /**
+ * Exception thrown by NonFatalAsserter for assertion failures.
+ */
+ public static class AssertException extends RuntimeException {
+ public AssertException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Asserter that throws an AssertException on failure instead of aborting
+ * the test.
+ *
+ * This can be used in methods called via waitForCondition() where an assertion
+ * might not immediately succeed.
+ */
+ public class NonFatalAsserter extends FennecMochitestAssert {
+ @Override
+ public void ok(boolean condition, String name, String diag) {
+ if (!condition) {
+ String details = (diag == null ? "" : " | " + diag);
+ throw new AssertException("Assertion failed: " + name + details);
+ }
+ mAsserter.ok(condition, name, diag);
+ }
+ }
+
+ /**
+ * Gets a URL for a dynamically-generated page.
+ *
+ * The page will have a URL unique to the given ID, and the page's title
+ * will match the given ID.
+ *
+ * @param id ID used to generate page URL
+ * @return URL of the page
+ */
+ protected String getPage(String id) {
+ return getAbsoluteUrl("/robocop/robocop_dynamic.sjs?id=" + id);
+ }
+
+ protected String readProfileFile(String filename) {
+ try {
+ return readFile(new File(mProfile, filename));
+ } catch (IOException e) {
+ mAsserter.ok(false, "Error reading" + filename, getStackTraceString(e));
+ }
+ return null;
+ }
+
+ protected void writeProfileFile(String filename, String data) {
+ try {
+ writeFile(new File(mProfile, filename), data);
+ } catch (IOException e) {
+ mAsserter.ok(false, "Error writing to " + filename, getStackTraceString(e));
+ }
+ }
+
+ private String readFile(File target) throws IOException {
+ if (!target.exists()) {
+ return null;
+ }
+
+ FileReader fr = new FileReader(target);
+ try {
+ StringBuffer sb = new StringBuffer();
+ char[] buf = new char[8192];
+ int read = fr.read(buf);
+ while (read >= 0) {
+ sb.append(buf, 0, read);
+ read = fr.read(buf);
+ }
+ return sb.toString();
+ } finally {
+ fr.close();
+ }
+ }
+
+ private void writeFile(File target, String data) throws IOException {
+ FileWriter writer = new FileWriter(target);
+ try {
+ writer.write(data);
+ } finally {
+ writer.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java
new file mode 100644
index 000000000..6f5db560d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java
@@ -0,0 +1,401 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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 android.content.res.Resources;
+
+import org.mozilla.gecko.R;
+
+public class StringHelper {
+ private static StringHelper instance;
+
+ // This needs to be accessed statically, before an instance of StringHelper can be created.
+ public static String STATIC_ABOUT_HOME_URL = "about:home";
+
+ public final String OK;
+ public final String CANCEL;
+ public final String CLEAR;
+
+ // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length
+ public final String[] DEFAULT_BOOKMARKS_TITLES;
+ public final String[] DEFAULT_BOOKMARKS_URLS;
+ public final int DEFAULT_BOOKMARKS_COUNT;
+
+ // About pages
+ public final String ABOUT_BLANK_URL = "about:blank";
+ public final String ABOUT_FIREFOX_URL;
+ public final String ABOUT_HOME_URL = "about:home";
+ public final String ABOUT_ADDONS_URL = "about:addons";
+ public final String ABOUT_SCHEME = "about:";
+
+ // About pages' titles
+ public final String ABOUT_HOME_TITLE = "";
+
+ // Context Menu item strings
+ public final String CONTEXT_MENU_BOOKMARK_LINK = "Bookmark Link";
+ public final String CONTEXT_MENU_OPEN_LINK_IN_NEW_TAB = "Open Link in New Tab";
+ public final String CONTEXT_MENU_OPEN_IN_NEW_TAB;
+ public final String CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB = "Open Link in Private Tab";
+ public final String CONTEXT_MENU_OPEN_IN_PRIVATE_TAB;
+ public final String CONTEXT_MENU_COPY_LINK = "Copy Link";
+ public final String CONTEXT_MENU_SHARE_LINK = "Share Link";
+ public final String CONTEXT_MENU_EDIT;
+ public final String CONTEXT_MENU_SHARE;
+ public final String CONTEXT_MENU_REMOVE;
+ public final String CONTEXT_MENU_COPY_ADDRESS;
+ public final String CONTEXT_MENU_EDIT_SITE_SETTINGS;
+ public final String CONTEXT_MENU_SITE_SETTINGS_SAVE_PASSWORD = "Save Password";
+ public final String CONTEXT_MENU_ADD_TO_HOME_SCREEN;
+ public final String CONTEXT_MENU_PIN_SITE;
+ public final String CONTEXT_MENU_UNPIN_SITE;
+
+ // Context Menu menu items
+ public final String[] CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB;
+
+ public final String[] CONTEXT_MENU_ITEMS_IN_NORMAL_TAB;
+
+ public final String[] BOOKMARK_CONTEXT_MENU_ITEMS;
+
+ public final String[] CONTEXT_MENU_ITEMS_IN_URL_BAR;
+
+ public final String TITLE_PLACE_HOLDER;
+
+ // Robocop page urls
+ // Note: please use getAbsoluteUrl(String url) on each robocop url to get the correct url
+ public final String ROBOCOP_BIG_LINK_URL = "/robocop/robocop_big_link.html";
+ public final String ROBOCOP_BIG_MAILTO_URL = "/robocop/robocop_big_mailto.html";
+ public final String ROBOCOP_BLANK_PAGE_01_URL = "/robocop/robocop_blank_01.html";
+ public final String ROBOCOP_BLANK_PAGE_02_URL = "/robocop/robocop_blank_02.html";
+ public final String ROBOCOP_BLANK_PAGE_03_URL = "/robocop/robocop_blank_03.html";
+ public final String ROBOCOP_BLANK_PAGE_04_URL = "/robocop/robocop_blank_04.html";
+ public final String ROBOCOP_BLANK_PAGE_05_URL = "/robocop/robocop_blank_05.html";
+ public final String ROBOCOP_BOXES_URL = "/robocop/robocop_boxes.html";
+ public final String ROBOCOP_GEOLOCATION_URL = "/robocop/robocop_geolocation.html";
+ public final String ROBOCOP_LOGIN_01_URL= "/robocop/robocop_login_01.html";
+ public final String ROBOCOP_LOGIN_02_URL= "/robocop/robocop_login_02.html";
+ public final String ROBOCOP_POPUP_URL = "/robocop/robocop_popup.html";
+ public final String ROBOCOP_OFFLINE_STORAGE_URL = "/robocop/robocop_offline_storage.html";
+ public final String ROBOCOP_PICTURE_LINK_URL = "/robocop/robocop_picture_link.html";
+ public final String ROBOCOP_SEARCH_URL = "/robocop/robocop_search.html";
+ public final String ROBOCOP_TEXT_PAGE_URL = "/robocop/robocop_text_page.html";
+ public final String ROBOCOP_ADOBE_FLASH_URL = "/robocop/robocop_adobe_flash.html";
+ public final String ROBOCOP_INPUT_URL = "/robocop/robocop_input.html";
+ public final String ROBOCOP_READER_MODE_BASIC_ARTICLE = "/robocop/reader_mode_pages/basic_article.html";
+ public final String ROBOCOP_LINK_TO_SLOW_LOADING = "/robocop/robocop_link_to_slow_loading.html";
+
+ private final String ROBOCOP_JS_HARNESS_URL = "/robocop/robocop_javascript.html";
+
+ // Robocop page images
+ public final String ROBOCOP_PICTURE_URL = "/robocop/Firefox.jpg";
+
+ // Robocop page titles
+ public final String ROBOCOP_BIG_LINK_TITLE = "Big Link";
+ public final String ROBOCOP_BIG_MAILTO_TITLE = "Big Mailto";
+ public final String ROBOCOP_BLANK_PAGE_01_TITLE = "Browser Blank Page 01";
+ public final String ROBOCOP_BLANK_PAGE_02_TITLE = "Browser Blank Page 02";
+ public final String ROBOCOP_GEOLOCATION_TITLE = "Geolocation Test Page";
+ public final String ROBOCOP_PICTURE_LINK_TITLE = "Picture Link";
+ public final String ROBOCOP_SEARCH_TITLE = "Robocop Search Engine";
+
+ // Distribution tile labels
+ public final String DISTRIBUTION1_LABEL = "Distribution 1";
+ public final String DISTRIBUTION2_LABEL = "Distribution 2";
+
+ // Settings menu strings
+ public final String PRIVACY_SECTION_LABEL;
+ public final String MOZILLA_SECTION_LABEL;
+
+ // Mozilla
+ public final String BRAND_NAME = "(Fennec|Nightly|Firefox Aurora|Firefox Beta|Firefox)";
+ public final String ABOUT_LABEL = "About " + BRAND_NAME ;
+ public final String LOCATION_SERVICES_LABEL = "Mozilla Location Service";
+
+ // Labels for the about:home tabs
+ public final String HISTORY_LABEL;
+ public final String TOP_SITES_LABEL;
+ public final String BOOKMARKS_LABEL;
+ public final String TODAY_LABEL;
+
+ // Desktop default bookmarks folders
+ public final String BOOKMARKS_UP_TO;
+ public final String BOOKMARKS_ROOT_LABEL;
+ public final String DESKTOP_FOLDER_LABEL;
+ public final String TOOLBAR_FOLDER_LABEL;
+ public final String BOOKMARKS_MENU_FOLDER_LABEL;
+ public final String UNSORTED_FOLDER_LABEL;
+
+ // Menu items - some of the items are found only on android 2.3 and lower and some only on android 3.0+
+ public final String NEW_TAB_LABEL;
+ public final String NEW_PRIVATE_TAB_LABEL;
+ public final String SHARE_LABEL;
+ public final String FIND_IN_PAGE_LABEL;
+ public final String DESKTOP_SITE_LABEL;
+ public final String PDF_LABEL;
+ public final String DOWNLOADS_LABEL;
+ public final String ADDONS_LABEL;
+ public final String LOGINS_LABEL;
+ public final String SETTINGS_LABEL;
+ public final String GUEST_MODE_LABEL;
+ public final String TAB_QUEUE_LABEL;
+ public final String TAB_QUEUE_SUMMARY;
+
+ // Android 3.0+
+ public final String TOOLS_LABEL;
+ public final String PAGE_LABEL;
+
+ // Android 2.3 and lower only
+ public final String MORE_LABEL = "More";
+ public final String RELOAD_LABEL;
+ public final String FORWARD_LABEL;
+ public final String BOOKMARK_LABEL;
+
+ // Bookmark Toast Notification
+ public final String BOOKMARK_ADDED_LABEL;
+ public final String BOOKMARK_REMOVED_LABEL;
+ public final String BOOKMARK_UPDATED_LABEL;
+ public final String BOOKMARK_OPTIONS_LABEL;
+
+ // Edit Bookmark screen
+ public final String EDIT_BOOKMARK;
+
+ // Strings used in doorhanger messages and buttons
+ public final String GEO_MESSAGE = "Share your location with";
+ public final String GEO_ALLOW;
+ public final String GEO_DENY = "Don't share";
+
+ public final String OFFLINE_MESSAGE = "to store data on your device for offline use";
+ public final String OFFLINE_ALLOW = "Allow";
+ public final String OFFLINE_DENY = "Don't allow";
+
+ public final String LOGIN_MESSAGE = "Would you like " + BRAND_NAME + " to remember this login?";
+ public final String LOGIN_ALLOW = "Remember";
+ public final String LOGIN_DENY = "Never";
+
+ public final String POPUP_MESSAGE = "prevented this site from opening";
+ public final String POPUP_ALLOW;
+ public final String POPUP_DENY = "Don't show";
+
+ // Strings used as content description, e.g. for ImageButtons
+ public final String CONTENT_DESCRIPTION_READER_MODE_BUTTON = "Enter Reader View";
+
+ // Home Panel Settings
+ public final String CUSTOMIZE_HOME;
+ public final String ENABLED;
+ public final String HISTORY;
+ public final String PANELS;
+
+ // Search Settings
+ public final String SEARCH_TITLE;
+ public final String SEARCH_SUGGESTIONS;
+ public final String SEARCH_INSTALLED;
+
+ // Advanced Settings
+ public final String ADVANCED;
+ public final String DONT_SHOW_MENU;
+ public final String SHOW_MENU;
+ public final String DISABLED;
+ public final String TAP_TO_PLAY;
+ public final String HIDE_TITLE_BAR;
+
+ // Update Settings
+ public final String AUTOMATIC_UPDATES;
+ public final String OVER_WIFI_OPTION;
+ public final String DOWNLOAD_UPDATES_AUTO;
+ public final String ALWAYS;
+ public final String NEVER;
+
+ // Restore Tabs Settings
+ public final String DONT_RESTORE_TABS;
+ public final String ALWAYS_RESTORE_TABS;
+ public final String DONT_RESTORE_QUIT;
+
+ private StringHelper(final Resources res) {
+
+ OK = res.getString(R.string.button_ok);
+ CANCEL = res.getString(R.string.button_cancel);
+ CLEAR = res.getString(R.string.button_clear);
+
+ // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length
+ DEFAULT_BOOKMARKS_TITLES = new String[] {
+ res.getString(R.string.bookmarkdefaults_title_aboutfirefox),
+ res.getString(R.string.bookmarkdefaults_title_support),
+ res.getString(R.string.bookmarkdefaults_title_addons)
+ };
+ DEFAULT_BOOKMARKS_URLS = new String[] {
+ res.getString(R.string.bookmarkdefaults_url_aboutfirefox),
+ res.getString(R.string.bookmarkdefaults_url_support),
+ res.getString(R.string.bookmarkdefaults_url_addons)
+ };
+ DEFAULT_BOOKMARKS_COUNT = DEFAULT_BOOKMARKS_TITLES.length;
+
+ // About pages
+ ABOUT_FIREFOX_URL = res.getString(R.string.bookmarkdefaults_url_aboutfirefox);
+
+ // Context Menu item strings
+ CONTEXT_MENU_OPEN_IN_NEW_TAB = res.getString(R.string.contextmenu_open_new_tab);
+ CONTEXT_MENU_OPEN_IN_PRIVATE_TAB = res.getString(R.string.contextmenu_open_private_tab);
+ CONTEXT_MENU_EDIT = res.getString(R.string.contextmenu_top_sites_edit);
+ CONTEXT_MENU_SHARE = res.getString(R.string.contextmenu_share);
+ CONTEXT_MENU_REMOVE = res.getString(R.string.contextmenu_remove);
+ CONTEXT_MENU_COPY_ADDRESS = res.getString(R.string.contextmenu_copyurl);
+ CONTEXT_MENU_EDIT_SITE_SETTINGS = res.getString(R.string.contextmenu_site_settings);
+ CONTEXT_MENU_ADD_TO_HOME_SCREEN = res.getString(R.string.contextmenu_add_to_launcher);
+ CONTEXT_MENU_PIN_SITE = res.getString(R.string.contextmenu_top_sites_pin);
+ CONTEXT_MENU_UNPIN_SITE = res.getString(R.string.contextmenu_top_sites_unpin);
+
+ // Context Menu menu items
+ CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB = new String[] {
+ CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB,
+ CONTEXT_MENU_COPY_LINK,
+ CONTEXT_MENU_SHARE_LINK,
+ CONTEXT_MENU_BOOKMARK_LINK
+ };
+
+ CONTEXT_MENU_ITEMS_IN_NORMAL_TAB = new String[] {
+ CONTEXT_MENU_OPEN_LINK_IN_NEW_TAB,
+ CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB,
+ CONTEXT_MENU_COPY_LINK,
+ CONTEXT_MENU_SHARE_LINK,
+ CONTEXT_MENU_BOOKMARK_LINK
+ };
+
+ BOOKMARK_CONTEXT_MENU_ITEMS = new String[] {
+ CONTEXT_MENU_OPEN_IN_NEW_TAB,
+ CONTEXT_MENU_OPEN_IN_PRIVATE_TAB,
+ CONTEXT_MENU_COPY_ADDRESS,
+ CONTEXT_MENU_SHARE,
+ CONTEXT_MENU_EDIT,
+ CONTEXT_MENU_REMOVE,
+ CONTEXT_MENU_ADD_TO_HOME_SCREEN
+ };
+
+ CONTEXT_MENU_ITEMS_IN_URL_BAR = new String[] {
+ CONTEXT_MENU_SHARE,
+ CONTEXT_MENU_COPY_ADDRESS,
+ CONTEXT_MENU_EDIT_SITE_SETTINGS,
+ CONTEXT_MENU_ADD_TO_HOME_SCREEN
+ };
+
+ TITLE_PLACE_HOLDER = res.getString(R.string.url_bar_default_text);
+
+ // Settings menu strings
+ PRIVACY_SECTION_LABEL = res.getString(R.string.pref_category_privacy_short);
+ MOZILLA_SECTION_LABEL = res.getString(R.string.pref_category_vendor);
+
+ // Labels for the about:home tabs
+ HISTORY_LABEL = res.getString(R.string.home_history_title);
+ TOP_SITES_LABEL = res.getString(R.string.home_top_sites_title);
+ BOOKMARKS_LABEL = res.getString(R.string.bookmarks_title);
+ TODAY_LABEL = res.getString(R.string.history_today_section);
+
+ BOOKMARKS_UP_TO = res.getString(R.string.home_move_back_to_filter);
+ BOOKMARKS_ROOT_LABEL = res.getString(R.string.bookmarks_title);
+ DESKTOP_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_desktop);
+ TOOLBAR_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_toolbar);
+ BOOKMARKS_MENU_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_menu);
+ UNSORTED_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_unfiled);
+
+ // Menu items - some of the items are found only on android 2.3 and lower and some only on android 3.0+
+ NEW_TAB_LABEL = res.getString(R.string.new_tab);
+ NEW_PRIVATE_TAB_LABEL = res.getString(R.string.new_private_tab);
+ SHARE_LABEL = res.getString(R.string.share);
+ FIND_IN_PAGE_LABEL = res.getString(R.string.find_in_page);
+ DESKTOP_SITE_LABEL = res.getString(R.string.desktop_mode);
+ PDF_LABEL = res.getString(R.string.save_as_pdf);
+ DOWNLOADS_LABEL = res.getString(R.string.downloads);
+ ADDONS_LABEL = res.getString(R.string.addons);
+ LOGINS_LABEL = res.getString(R.string.logins);
+ SETTINGS_LABEL = res.getString(R.string.settings);
+ GUEST_MODE_LABEL = res.getString(R.string.new_guest_session);
+ TAB_QUEUE_LABEL = res.getString(R.string.pref_tab_queue_title);
+ TAB_QUEUE_SUMMARY = res.getString(R.string.pref_tab_queue_summary);
+
+ // Android 3.0+
+ TOOLS_LABEL = res.getString(R.string.tools);
+ PAGE_LABEL = res.getString(R.string.page);
+
+ // Android 2.3 and lower only
+ RELOAD_LABEL = res.getString(R.string.reload);
+ FORWARD_LABEL = res.getString(R.string.forward);
+ BOOKMARK_LABEL = res.getString(R.string.bookmark);
+
+ // Bookmark Toast Notification
+ BOOKMARK_ADDED_LABEL = res.getString(R.string.bookmark_added);
+ BOOKMARK_REMOVED_LABEL = res.getString(R.string.bookmark_removed);
+ BOOKMARK_UPDATED_LABEL = res.getString(R.string.bookmark_updated);
+ BOOKMARK_OPTIONS_LABEL = res.getString(R.string.bookmark_options);
+
+ // Edit Bookmark screen
+ EDIT_BOOKMARK = res.getString(R.string.bookmark_edit_title);
+
+ // Strings used in doorhanger messages and buttons
+ GEO_ALLOW = res.getString(R.string.share);
+
+ POPUP_ALLOW = res.getString(R.string.pref_panels_show);
+
+ // Home Settings
+ PANELS = res.getString(R.string.pref_category_home_panels);
+ CUSTOMIZE_HOME = res.getString(R.string.pref_category_home);
+ ENABLED = res.getString(R.string.pref_home_updates_enabled);
+ HISTORY = res.getString(R.string.home_history_title);
+
+ // Search Settings
+ SEARCH_TITLE = res.getString(R.string.search);
+ SEARCH_SUGGESTIONS = res.getString(R.string.pref_search_suggestions);
+ SEARCH_INSTALLED = res.getString(R.string.pref_category_installed_search_engines);
+
+ // Advanced Settings
+ ADVANCED = res.getString(R.string.pref_category_advanced);
+ DONT_SHOW_MENU = res.getString(R.string.pref_char_encoding_off);
+ SHOW_MENU = res.getString(R.string.pref_char_encoding_on);
+ DISABLED = res.getString(R.string.pref_plugins_disabled );
+ TAP_TO_PLAY = res.getString(R.string.pref_plugins_tap_to_play);
+ HIDE_TITLE_BAR = res.getString(R.string.pref_scroll_title_bar_summary );
+
+ // Update Settings
+ AUTOMATIC_UPDATES = res.getString(R.string.pref_home_updates);
+ OVER_WIFI_OPTION = res.getString(R.string.pref_update_autodownload_wifi);
+ DOWNLOAD_UPDATES_AUTO = res.getString(R.string.pref_update_autodownload);
+ ALWAYS = res.getString(R.string.pref_update_autodownload_enabled);
+ NEVER = res.getString(R.string.pref_update_autodownload_disabled);
+
+ // Restore Tabs Settings
+ DONT_RESTORE_TABS = res.getString(R.string.pref_restore_quit);
+ ALWAYS_RESTORE_TABS = res.getString(R.string.pref_restore_always);
+ DONT_RESTORE_QUIT = res.getString(R.string.pref_restore_quit);
+ }
+
+ public static void initialize(Resources res) {
+ if (instance != null) {
+ throw new IllegalStateException(StringHelper.class.getSimpleName() + " already Initialized");
+ }
+ instance = new StringHelper(res);
+ }
+
+ public static StringHelper get() {
+ if (instance == null) {
+ throw new IllegalStateException(StringHelper.class.getSimpleName() + " instance is not yet initialized. Use StringHelper.initialize(Resources) first.");
+ }
+ return instance;
+ }
+
+ /**
+ * Build a URL for loading a Javascript file in the Robocop Javascript
+ * harness.
+ * <p>
+ * We append a random slug to avoid caching: see
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache">https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache</a>.
+ *
+ * @param javascriptUrl to load.
+ * @return URL with harness wrapper.
+ */
+ public String getHarnessUrlForJavascript(String javascriptUrl) {
+ // We include a slug to make sure we never cache the harness.
+ return ROBOCOP_JS_HARNESS_URL +
+ "?slug=" + System.currentTimeMillis() +
+ "&path=" + javascriptUrl;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java
new file mode 100644
index 000000000..da952b5cb
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java
@@ -0,0 +1,203 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.tests.components.AboutHomeComponent;
+import org.mozilla.gecko.tests.components.AppMenuComponent;
+import org.mozilla.gecko.tests.components.BaseComponent;
+import org.mozilla.gecko.tests.components.GeckoViewComponent;
+import org.mozilla.gecko.tests.components.TabStripComponent;
+import org.mozilla.gecko.tests.components.ToolbarComponent;
+import org.mozilla.gecko.tests.helpers.HelperInitializer;
+
+import com.robotium.solo.Solo;
+
+/**
+ * A base test class for Robocop (UI-centric) tests. This and the related classes attempt to
+ * provide a framework to improve upon the issues discovered with the previous BaseTest
+ * implementation by providing simple test authorship and framework extension, consistency,
+ * and reliability.
+ *
+ * For documentation on writing tests and extending the framework, see
+ * https://wiki.mozilla.org/Mobile/Fennec/Android/UITest
+ */
+abstract class UITest extends BaseRobocopTest
+ implements UITestContext {
+
+ private static final String JUNIT_FAILURE_MSG = "A JUnit method was called. Make sure " +
+ "you are using AssertionHelper to make assertions. Try `fAssert*(...);`";
+
+ protected AboutHomeComponent mAboutHome;
+ protected AppMenuComponent mAppMenu;
+ protected GeckoViewComponent mGeckoView;
+ protected TabStripComponent mTabStrip;
+ protected ToolbarComponent mToolbar;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // Helpers depend on components so initialize them first.
+ initComponents();
+ initHelpers();
+
+ // Ensure Robocop tests have access to network, and are run with Display powered on.
+ throwIfHttpGetFails();
+ throwIfScreenNotOn();
+ }
+
+ private void initComponents() {
+ mAboutHome = new AboutHomeComponent(this);
+ mAppMenu = new AppMenuComponent(this);
+ mGeckoView = new GeckoViewComponent(this);
+ mTabStrip = new TabStripComponent(this);
+ mToolbar = new ToolbarComponent(this);
+ }
+
+ private void initHelpers() {
+ HelperInitializer.init(this);
+ }
+
+ @Override
+ public Solo getSolo() {
+ return mSolo;
+ }
+
+ @Override
+ public Assert getAsserter() {
+ return mAsserter;
+ }
+
+ @Override
+ public Driver getDriver() {
+ return mDriver;
+ }
+
+ @Override
+ public Actions getActions() {
+ return mActions;
+ }
+
+ @Override
+ public StringHelper getStringHelper() {
+ return mStringHelper;
+ }
+
+ @Override
+ public void dumpLog(final String logtag, final String message) {
+ mAsserter.dumpLog(logtag + ": " + message);
+ }
+
+ @Override
+ public void dumpLog(final String logtag, final String message, final Throwable t) {
+ mAsserter.dumpLog(logtag + ": " + message, t);
+ }
+
+ @Override
+ public BaseComponent getComponent(final ComponentType type) {
+ switch (type) {
+ case ABOUTHOME:
+ return mAboutHome;
+
+ case APPMENU:
+ return mAppMenu;
+
+ case GECKOVIEW:
+ return mGeckoView;
+
+ case TOOLBAR:
+ return mToolbar;
+
+ default:
+ fail("Unknown component type, " + type + ".");
+ return null; // Should not reach this statement but required by javac.
+ }
+ }
+
+ /**
+ * Returns the test type. By default this returns MOCHITEST, but tests can override this
+ * method in order to change the type of the test.
+ */
+ @Override
+ protected Type getTestType() {
+ return Type.MOCHITEST;
+ }
+
+ @Override
+ public String getAbsoluteHostnameUrl(final String url) {
+ return getAbsoluteUrl(mBaseHostnameUrl, url);
+ }
+
+ @Override
+ public String getAbsoluteIpUrl(final String url) {
+ return getAbsoluteUrl(mBaseIpUrl, url);
+ }
+
+ private String getAbsoluteUrl(final String baseUrl, final String url) {
+ return baseUrl + "/" + url.replaceAll("(^/)", "");
+ }
+
+ /**
+ * Throws an Exception. Called from overridden JUnit methods to ensure JUnit assertions
+ * are not accidentally used over AssertionHelper assertions (the latter of which contains
+ * additional logging facilities for use in our test harnesses).
+ */
+ private static void junit() {
+ throw new UnsupportedOperationException(JUNIT_FAILURE_MSG);
+ }
+
+ // Note: inexplicably, javac does not think we're overriding these methods,
+ // so we can't use the @Override annotation.
+ public static void assertEquals(short e, short a) { junit(); }
+ public static void assertEquals(String m, int e, int a) { junit(); }
+ public static void assertEquals(String m, short e, short a) { junit(); }
+ public static void assertEquals(char e, char a) { junit(); }
+ public static void assertEquals(String m, String e, String a) { junit(); }
+ public static void assertEquals(int e, int a) { junit(); }
+ public static void assertEquals(String m, double e, double a, double delta) { junit(); }
+ public static void assertEquals(String m, long e, long a) { junit(); }
+ public static void assertEquals(byte e, byte a) { junit(); }
+ public static void assertEquals(Object e, Object a) { junit(); }
+ public static void assertEquals(boolean e, boolean a) { junit(); }
+ public static void assertEquals(String m, float e, float a, float delta) { junit(); }
+ public static void assertEquals(String m, boolean e, boolean a) { junit(); }
+ public static void assertEquals(String e, String a) { junit(); }
+ public static void assertEquals(float e, float a, float delta) { junit(); }
+ public static void assertEquals(String m, byte e, byte a) { junit(); }
+ public static void assertEquals(double e, double a, double delta) { junit(); }
+ public static void assertEquals(String m, char e, char a) { junit(); }
+ public static void assertEquals(String m, Object e, Object a) { junit(); }
+ public static void assertEquals(long e, long a) { junit(); }
+
+ public static void assertFalse(String m, boolean c) { junit(); }
+ public static void assertFalse(boolean c) { junit(); }
+
+ public static void assertNotNull(String m, Object o) { junit(); }
+ public static void assertNotNull(Object o) { junit(); }
+
+ public static void assertNotSame(Object e, Object a) { junit(); }
+ public static void assertNotSame(String m, Object e, Object a) { junit(); }
+
+ public static void assertNull(Object o) { junit(); }
+ public static void assertNull(String m, Object o) { junit(); }
+
+ public static void assertSame(Object e, Object a) { junit(); }
+ public static void assertSame(String m, Object e, Object a) { junit(); }
+
+ public static void assertTrue(String m, boolean c) { junit(); }
+ public static void assertTrue(boolean c) { junit(); }
+
+ public static void fail(String m) { junit(); }
+ public static void fail() { junit(); }
+
+ public static void failNotEquals(String m, Object e, Object a) { junit(); }
+ public static void failNotSame(String m, Object e, Object a) { junit(); }
+ public static void failSame(String m) { junit(); }
+
+ public static String format(String m, Object e, Object a) { junit(); return null; }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java
new file mode 100644
index 000000000..c825a20a4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java
@@ -0,0 +1,51 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.tests.components.BaseComponent;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Interface to the global information about a UITest environment.
+ */
+public interface UITestContext {
+
+ public static enum ComponentType {
+ ABOUTHOME,
+ APPMENU,
+ GECKOVIEW,
+ TOOLBAR
+ }
+
+ public Activity getActivity();
+ public Solo getSolo();
+ public Assert getAsserter();
+ public Driver getDriver();
+ public Actions getActions();
+ public Instrumentation getInstrumentation();
+ public StringHelper getStringHelper();
+
+ public void dumpLog(final String logtag, final String message);
+ public void dumpLog(final String logtag, final String message, final Throwable t);
+
+ /**
+ * Returns the absolute version of the given URL using the host's hostname.
+ */
+ public String getAbsoluteHostnameUrl(final String url);
+
+ /**
+ * Returns the absolute version of the given URL using the host's IP address.
+ */
+ public String getAbsoluteIpUrl(final String url);
+
+ public BaseComponent getComponent(final ComponentType type);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
new file mode 100644
index 000000000..b12e0d23e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
@@ -0,0 +1,193 @@
+/* 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.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+import android.os.Build;
+import android.support.v4.view.ViewPager;
+import android.view.View;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Solo;
+
+/**
+ * A class representing any interactions that take place on the Awesomescreen.
+ */
+public class AboutHomeComponent extends BaseComponent {
+ private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
+
+ private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
+ PanelType.TOP_SITES,
+ PanelType.BOOKMARKS,
+ PanelType.COMBINED_HISTORY
+ );
+
+ // The percentage of the panel to swipe between 0 and 1. This value was set through
+ // testing: 0.55f was tested on try and fails on armv6 devices.
+ private static final float SWIPE_PERCENTAGE = 0.70f;
+
+ public AboutHomeComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ private View getHomePagerContainer() {
+ return mSolo.getView(R.id.home_screen_container);
+ }
+
+ private ViewPager getHomePagerView() {
+ return (ViewPager) mSolo.getView(R.id.home_pager);
+ }
+
+ private View getHomeBannerView() {
+ if (mSolo.waitForView(R.id.home_banner)) {
+ return mSolo.getView(R.id.home_banner);
+ }
+ return null;
+ }
+
+ public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
+ assertVisible();
+
+ final int expectedPanelIndex = PANEL_ORDERING.indexOf(expectedPanel);
+ fAssertEquals("The current HomePager panel is " + expectedPanel,
+ expectedPanelIndex, getHomePagerView().getCurrentItem());
+ return this;
+ }
+
+ public AboutHomeComponent assertNotVisible() {
+ fAssertTrue("The HomePager is not visible",
+ getHomePagerContainer().getVisibility() != View.VISIBLE ||
+ getHomePagerView().getVisibility() != View.VISIBLE);
+ return this;
+ }
+
+ public AboutHomeComponent assertVisible() {
+ fAssertTrue("The HomePager is visible",
+ getHomePagerContainer().getVisibility() == View.VISIBLE &&
+ getHomePagerView().getVisibility() == View.VISIBLE);
+ return this;
+ }
+
+ public AboutHomeComponent assertBannerNotVisible() {
+ View banner = getHomeBannerView();
+ if (Build.VERSION.SDK_INT >= 11) {
+ fAssertTrue("The HomeBanner is not visible",
+ getHomePagerContainer().getVisibility() != View.VISIBLE ||
+ banner == null ||
+ banner.getVisibility() != View.VISIBLE ||
+ banner.getTranslationY() == banner.getHeight());
+ } else {
+ // getTranslationY is not available before api 11.
+ // This check is a little less specific.
+ fAssertTrue("The HomeBanner is not visible",
+ getHomePagerContainer().getVisibility() != View.VISIBLE ||
+ banner == null ||
+ banner.isShown() == false);
+ }
+ return this;
+ }
+
+ public AboutHomeComponent assertBannerVisible() {
+ fAssertTrue("The HomeBanner is visible",
+ getHomePagerContainer().getVisibility() == View.VISIBLE &&
+ getHomeBannerView().getVisibility() == View.VISIBLE);
+ return this;
+ }
+
+ public AboutHomeComponent assertBannerText(String text) {
+ assertBannerVisible();
+
+ final TextView textView = (TextView) getHomeBannerView().findViewById(R.id.text);
+ fAssertEquals("The correct HomeBanner text is shown",
+ text, textView.getText().toString());
+ return this;
+ }
+
+ public AboutHomeComponent clickOnBanner() {
+ assertBannerVisible();
+
+ mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner.");
+ mSolo.clickOnView(getHomeBannerView());
+ return this;
+ }
+
+ public AboutHomeComponent dismissBanner() {
+ assertBannerVisible();
+
+ mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner close button.");
+ mSolo.clickOnView(getHomeBannerView().findViewById(R.id.close));
+ return this;
+ }
+
+ public AboutHomeComponent swipeToPanelOnRight() {
+ mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the right.");
+ swipeToPanel(Solo.RIGHT);
+ return this;
+ }
+
+ public AboutHomeComponent swipeToPanelOnLeft() {
+ mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the left.");
+ swipeToPanel(Solo.LEFT);
+ return this;
+ }
+
+ private void swipeToPanel(final int panelDirection) {
+ fAssertTrue("Swiping in a valid direction",
+ panelDirection == Solo.LEFT || panelDirection == Solo.RIGHT);
+ assertVisible();
+
+ final int panelIndex = getHomePagerView().getCurrentItem();
+
+ mSolo.scrollViewToSide(getHomePagerView(), panelDirection, SWIPE_PERCENTAGE);
+
+ // The panel on the left is a lower index and vice versa.
+ final int unboundedPanelIndex = panelIndex + (panelDirection == Solo.LEFT ? -1 : 1);
+ final int maxPanelIndex = PANEL_ORDERING.size() - 1;
+ final int expectedPanelIndex = Math.min(Math.max(0, unboundedPanelIndex), maxPanelIndex);
+
+ waitForPanelIndex(expectedPanelIndex);
+ }
+
+ private void waitForPanelIndex(final int expectedIndex) {
+ final String panelName = PANEL_ORDERING.get(expectedIndex).name();
+
+ WaitHelper.waitFor("HomePager " + panelName + " panel", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (getHomePagerView().getCurrentItem() == expectedIndex);
+ }
+ });
+ }
+
+ /**
+ * Navigate directly to a built-in panel by its panel type.
+ * <p>
+ * If the panel type is not part of the active Home Panel configuration, the
+ * default about:home panel is displayed. If the panel type is not a
+ * built-in panel, an IllegalArgumentException is thrown.
+ *
+ * @param panelType to navigate to.
+ * @return self, for chaining.
+ */
+ public AboutHomeComponent navigateToBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
+ Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(panelType));
+ final int expectedPanelIndex = PANEL_ORDERING.indexOf(panelType);
+ waitForPanelIndex(expectedPanelIndex);
+ return this;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java
new file mode 100644
index 000000000..278cc7564
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java
@@ -0,0 +1,295 @@
+/* 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.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.menu.MenuItemActionBar;
+import org.mozilla.gecko.menu.MenuItemDefault;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.RobotiumHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.RelativeLayout;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.RobotiumUtils;
+import com.robotium.solo.Solo;
+
+/**
+ * A class representing any interactions that take place on the app menu.
+ */
+public class AppMenuComponent extends BaseComponent {
+ private static final int MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS = 7500;
+
+ public enum MenuItem {
+ FORWARD(R.string.forward),
+ NEW_TAB(R.string.new_tab),
+ PAGE(R.string.page),
+ RELOAD(R.string.reload);
+
+ private final int resourceID;
+ private String stringResource;
+
+ MenuItem(final int resourceID) {
+ this.resourceID = resourceID;
+ }
+
+ public String getString(final Solo solo) {
+ if (stringResource == null) {
+ stringResource = solo.getString(resourceID);
+ }
+
+ return stringResource;
+ }
+ };
+
+ public enum PageMenuItem {
+ SAVE_AS_PDF(R.string.save_as_pdf);
+
+ private static final MenuItem PARENT_MENU = MenuItem.PAGE;
+
+ private final int resourceID;
+ private String stringResource;
+
+ PageMenuItem(final int resourceID) {
+ this.resourceID = resourceID;
+ }
+
+ public String getString(final Solo solo) {
+ if (stringResource == null) {
+ stringResource = solo.getString(resourceID);
+ }
+
+ return stringResource;
+ }
+ };
+
+ public AppMenuComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ public void assertMenuIsOpen() {
+ fAssertTrue("Menu is open", isMenuOpen());
+ }
+
+ public void assertMenuIsNotOpen() {
+ fAssertFalse("Menu is not open", isMenuOpen());
+ }
+
+ public void assertMenuItemIsDisabledAndVisible(PageMenuItem pageMenuItem) {
+ openAppMenu();
+
+ // Non-legacy devices have hierarchical menu, check for parent menu item "page".
+ final View parentMenuItemView = findAppMenuItemView(MenuItem.PAGE.getString(mSolo));
+ if (parentMenuItemView.isEnabled()) {
+ fAssertTrue("The parent 'page' menu item is enabled", parentMenuItemView.isEnabled());
+ fAssertEquals("The parent 'page' menu item is visible", View.VISIBLE,
+ parentMenuItemView.getVisibility());
+
+ // Parent menu "page" is enabled, open page menu and check for menu item represented by pageMenuItem.
+ pressMenuItem(MenuItem.PAGE.getString(mSolo));
+
+ final View pageMenuItemView = findAppMenuItemView(pageMenuItem.getString(mSolo));
+ fAssertNotNull("The page menu item is not null", pageMenuItemView);
+ fAssertFalse("The page menu item is not enabled", pageMenuItemView.isEnabled());
+ fAssertEquals("The page menu item is visible", View.VISIBLE, pageMenuItemView.getVisibility());
+ } else {
+ fAssertFalse("The parent 'page' menu item is not enabled", parentMenuItemView.isEnabled());
+ fAssertEquals("The parent 'page' menu item is visible", View.VISIBLE, parentMenuItemView.getVisibility());
+ }
+ // Close the App Menu.
+ mSolo.goBack();
+ }
+
+ private View getOverflowMenuButtonView() {
+ return mSolo.getView(R.id.menu);
+ }
+
+ /**
+ * Try to find a MenuItemActionBar/MenuItemDefault with the given text set as contentDescription / text.
+ *
+ * When using legacy menus, make sure the menu has been opened to the appropriate level
+ * (i.e. base menu or "More" menu) to ensure the appropriate menu views are in memory.
+ * TODO: ^ Maybe we just need to have opened the "More" menu and the current one doesn't matter.
+ *
+ * This method is dependent on not having two views with equivalent contentDescription / text.
+ */
+ private View findAppMenuItemView(final String text) {
+ return WaitHelper.waitFor(String.format("menu item view '%s'", text), new Callable<View>() {
+ @Override
+ public View call() throws Exception {
+ final List<View> views = mSolo.getViews();
+
+ final List<MenuItemActionBar> menuItemActionBarList = RobotiumUtils.filterViews(MenuItemActionBar.class, views);
+ for (MenuItemActionBar menuItem : menuItemActionBarList) {
+ if (TextUtils.equals(menuItem.getContentDescription(), text)) {
+ return menuItem;
+ }
+ }
+
+ final List<MenuItemDefault> menuItemDefaultList = RobotiumUtils.filterViews(MenuItemDefault.class, views);
+ for (MenuItemDefault menuItem : menuItemDefaultList) {
+ if (TextUtils.equals(menuItem.getText(), text)) {
+ return menuItem;
+ }
+ }
+
+ // On Android 2.3, menu items may be instances of
+ // com.android.internal.view.menu.ListMenuItemView, each with a child
+ // android.widget.RelativeLayout which in turn has a child
+ // TextView with the appropriate text.
+ final List<TextView> textViewList = RobotiumUtils.filterViews(TextView.class, views);
+ for (TextView textView : textViewList) {
+ if (TextUtils.equals(textView.getText(), text)) {
+ View relativeLayout = (View) textView.getParent();
+ if (relativeLayout instanceof RelativeLayout) {
+ View listMenuItemView = (View)relativeLayout.getParent();
+ return listMenuItemView;
+ }
+ }
+ }
+ return null;
+ }
+ }, MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS);
+ }
+
+ /**
+ * Helper function to let Robotium locate and click menu item from legacy Android menu (devices with Android 2.x).
+ *
+ * Robotium will also try to open the menu if there are no open dialog.
+ *
+ * @param menuItemTitle, The title of menu item to open.
+ */
+ private void pressLegacyMenuItem(final String menuItemTitle) {
+ mSolo.clickOnMenuItem(menuItemTitle, true);
+ }
+
+ private void pressMenuItem(final String menuItemTitle) {
+ // Wait for the menu item view to be enabled. This improves reliability on Android 2.3.
+ WaitHelper.waitFor(String.format("menu item %s to be enabled", menuItemTitle), new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View v = findAppMenuItemView(menuItemTitle);
+ return (v != null) && v.isEnabled();
+ }
+ });
+
+ final View menuItemView = findAppMenuItemView(menuItemTitle);
+ fAssertTrue("Menu is open", isMenuOpen(menuItemView));
+
+ fAssertTrue(String.format("The menu item %s is enabled", menuItemTitle), menuItemView.isEnabled());
+ fAssertEquals(String.format("The menu item %s is visible", menuItemTitle), View.VISIBLE,
+ menuItemView.getVisibility());
+
+ mSolo.clickOnView(menuItemView);
+ }
+
+ private void pressSubMenuItem(final String parentMenuItemTitle, final String childMenuItemTitle) {
+ openAppMenu();
+
+ pressMenuItem(parentMenuItemTitle);
+
+ // Child menu item is not pressed yet, Click on it.
+ pressMenuItem(childMenuItemTitle);
+ }
+
+ public void pressMenuItem(MenuItem menuItem) {
+ openAppMenu();
+ pressMenuItem(menuItem.getString(mSolo));
+ }
+
+ public void pressMenuItem(final PageMenuItem pageMenuItem) {
+ pressSubMenuItem(PageMenuItem.PARENT_MENU.getString(mSolo), pageMenuItem.getString(mSolo));
+ }
+
+ private void openAppMenu() {
+ assertMenuIsNotOpen();
+
+ // This is a hack needed for tablets where the OverflowMenuButton is always in the GONE state,
+ // so we press the menu key instead.
+ if (DeviceHelper.isTablet()) {
+ mSolo.sendKey(Solo.MENU);
+ } else {
+ pressOverflowMenuButton();
+ }
+
+ waitForMenuOpen();
+ }
+
+ private void pressOverflowMenuButton() {
+ final View overflowMenuButton = getOverflowMenuButtonView();
+
+ fAssertTrue("The overflow menu button is enabled", overflowMenuButton.isEnabled());
+ fAssertEquals("The overflow menu button is visible", View.VISIBLE, overflowMenuButton.getVisibility());
+
+ mSolo.clickOnView(overflowMenuButton, true);
+ }
+
+ /**
+ * Determines whether the app menu is open by searching for items in the menu.
+ *
+ * @return true if app menu is open.
+ */
+ private boolean isMenuOpen() {
+ // We choose these options because New Tab is near the top of the menu and Page is near the middle/bottom.
+ // Intermittently, the menu doesn't scroll to top so we can't just use the first item in the list.
+ return isMenuOpen(MenuItem.NEW_TAB.getString(mSolo)) || isMenuOpen(MenuItem.PAGE.getString(mSolo));
+ }
+
+ /**
+ * Determines whether the app menu is open by searching for the text in menuItemTitle.
+ *
+ * @param menuItemTitle, The contentDescription of menu item to search.
+ *
+ * @return true if app menu is open.
+ */
+ private boolean isMenuOpen(String menuItemTitle) {
+ final View menuItemView = findAppMenuItemView(menuItemTitle);
+ return isMenuOpen(menuItemView) ? true : RobotiumHelper.searchExactText(menuItemTitle, true);
+ }
+
+ /**
+ * If a ListMenuItemView with menuItemTitle is visible then the app menu is open .
+ *
+ * @param menuItemView, must be a ListMenuItemView with menuItemTitle.
+ * You must use findAppMenuItemView(menuItemTitle) to obtain it.
+ *
+ * @return true if app menu is open.
+ */
+ private boolean isMenuOpen(View menuItemView) {
+ return (menuItemView != null) && (menuItemView.getVisibility() == View.VISIBLE);
+ }
+
+ public void waitForMenuOpen() {
+ WaitHelper.waitFor("menu to open", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return isMenuOpen();
+ }
+ });
+ }
+
+ public void waitForMenuClose() {
+ WaitHelper.waitFor("menu to close", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !isMenuOpen();
+ }
+ });
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java
new file mode 100644
index 000000000..eadaaa173
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java
@@ -0,0 +1,36 @@
+/* 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.components;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.tests.StringHelper;
+import org.mozilla.gecko.tests.UITestContext;
+
+import android.app.Activity;
+
+import com.robotium.solo.Solo;
+
+/**
+ * A base class for constructing components - an abstraction over small bits of Firefox
+ * functionality. For example, the Toolbar or the about:home screen could be considered a
+ * component. Components should not need to know about each others existences and should be
+ * combined via helpers. Helpers can also handle a series of actions taken on one component
+ * (e.g. clicking the toolbar, entering a url, and waiting for page load).
+ */
+public abstract class BaseComponent {
+ protected final UITestContext mTestContext;
+ protected final Activity mActivity;
+ protected final Solo mSolo;
+ protected final Actions mActions;
+ protected final StringHelper mStringHelper;
+
+ public BaseComponent(final UITestContext testContext) {
+ mTestContext = testContext;
+ mActivity = mTestContext.getActivity();
+ mSolo = mTestContext.getSolo();
+ mActions = mTestContext.getActions();
+ mStringHelper = mTestContext.getStringHelper();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java
new file mode 100644
index 000000000..3beab3169
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java
@@ -0,0 +1,343 @@
+/* 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.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotSame;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertSame;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.FrameworkHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+import com.robotium.solo.Condition;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * A class representing any interactions that take place on GeckoView.
+ */
+public class GeckoViewComponent extends BaseComponent {
+
+ public final TextInput mTextInput;
+
+ public GeckoViewComponent(final UITestContext testContext) {
+ super(testContext);
+ mTextInput = new TextInput();
+ }
+
+ /**
+ * Returns the GeckoView.
+ */
+ private View getView() {
+ // Solo.getView asserts returning a valid View
+ return mSolo.getView(R.id.layer_view);
+ }
+
+ private void setContext(final Context newContext) {
+ final View geckoView = getView();
+ // Switch to a no-InputMethodManager context to avoid interference
+ mTestContext.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ FrameworkHelper.setViewContext(geckoView, newContext);
+ }
+ });
+ }
+
+ public static abstract class InputConnectionTest {
+ protected Handler inputConnectionHandler;
+
+ /**
+ * Processes pending events on the input connection thread before returning.
+ * Must be called on the input connection thread during a test.
+ */
+ protected void processInputConnectionEvents() {
+ fAssertSame("Should be called on input connection thread",
+ Looper.myLooper(), inputConnectionHandler.getLooper());
+
+ // Adapted from GeckoThread.pumpMessageLoop.
+ MessageQueue queue = Looper.myQueue();
+ queue.addIdleHandler(new MessageQueue.IdleHandler() {
+ @Override
+ public boolean queueIdle() {
+ final Message msg = Message.obtain(inputConnectionHandler);
+ msg.obj = inputConnectionHandler;
+ inputConnectionHandler.sendMessageAtFrontOfQueue(msg);
+ return false; // Remove this idle handler.
+ }
+ });
+
+ final Method getNextMessage;
+ try {
+ getNextMessage = queue.getClass().getDeclaredMethod("next");
+ } catch (final NoSuchMethodException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ getNextMessage.setAccessible(true);
+
+ while (true) {
+ final Message msg;
+ try {
+ msg = (Message) getNextMessage.invoke(queue);
+ } catch (final IllegalAccessException | InvocationTargetException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ if (msg.obj == inputConnectionHandler &&
+ msg.getTarget() == inputConnectionHandler) {
+ // Our idle signal
+ break;
+ } else if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ break;
+ }
+ msg.getTarget().dispatchMessage(msg);
+ }
+ }
+
+ /**
+ * Processes pending events on the Gecko thread before returning.
+ * Must be called on the input connection thread during a test.
+ */
+ protected void processGeckoEvents() {
+ fAssertSame("Should be called on input connection thread",
+ Looper.myLooper(), inputConnectionHandler.getLooper());
+
+ GeckoThread.waitOnGecko();
+ }
+
+ private static ExtractedText getExtractedText(final InputConnection ic) {
+ final ExtractedTextRequest req = new ExtractedTextRequest();
+ return ic.getExtractedText(req, 0);
+ }
+
+ protected String getText(final InputConnection ic) {
+ return getExtractedText(ic).text.toString();
+ }
+
+ private static void assertText(final String message,
+ final String expected,
+ final String actual) {
+ // In an HTML editor, Gecko may insert an additional element that show up as a
+ // return character at the end. Deal with that here.
+ int end = actual.length();
+ if (end > 0 && actual.charAt(end - 1) == '\n') {
+ end--;
+ }
+ fAssertEquals(message, expected, actual.substring(0, end));
+ }
+
+ protected void assertText(final String message,
+ final InputConnection ic,
+ final String text) {
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ assertText(message, text, getText(ic));
+ }
+
+ protected void assertSelection(final String message,
+ final InputConnection ic,
+ final int start,
+ final int end) {
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ final ExtractedText extract = getExtractedText(ic);
+ fAssertEquals(message, start, extract.selectionStart);
+ fAssertEquals(message, end, extract.selectionEnd);
+ }
+
+ protected void assertSelectionAt(final String message,
+ final InputConnection ic,
+ final int value) {
+ assertSelection(message, ic, value, value);
+ }
+
+ protected void assertTextAndSelection(final String message,
+ final InputConnection ic,
+ final String text,
+ final int start,
+ final int end) {
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ final ExtractedText extract = getExtractedText(ic);
+ assertText(message, text, extract.text.toString());
+ fAssertEquals(message, start, extract.selectionStart);
+ fAssertEquals(message, end, extract.selectionEnd);
+ }
+
+ protected void assertTextAndSelectionAt(final String message,
+ final InputConnection ic,
+ final String text,
+ final int selection) {
+ assertTextAndSelection(message, ic, text, selection, selection);
+ }
+
+ public abstract void test(InputConnection ic, EditorInfo info);
+ }
+
+ public class TextInput {
+ private TextInput() {
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ final InputMethodManager imm = (InputMethodManager)
+ mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ fAssertNotNull("Must have an InputMethodManager", imm);
+ return imm;
+ }
+
+ /**
+ * Returns whether text input is being directed to the GeckoView.
+ */
+ private boolean isActive() {
+ return getInputMethodManager().isActive(getView());
+ }
+
+ public TextInput assertActive() {
+ fAssertTrue("Current view should be the active input view", isActive());
+ return this;
+ }
+
+ public TextInput waitForActive() {
+ WaitHelper.waitFor("current view to become the active input view", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return isActive();
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Returns whether an InputConnection is available.
+ * An InputConnection is available when text input is being directed to the
+ * GeckoView, and a text field (input, textarea, contentEditable, etc.) is
+ * currently focused inside the GeckoView.
+ */
+ private boolean hasInputConnection() {
+ final InputMethodManager imm = getInputMethodManager();
+ return imm.isActive(getView()) && imm.isAcceptingText();
+ }
+
+ public TextInput assertInputConnection() {
+ fAssertTrue("Current view should have an active InputConnection", hasInputConnection());
+ return this;
+ }
+
+ public TextInput waitForInputConnection() {
+ WaitHelper.waitFor("current view to have an active InputConnection", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return hasInputConnection();
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Starts an InputConnectionTest. An InputConnectionTest must run on the
+ * InputConnection thread which may or may not be the main UI thread. Also,
+ * during an InputConnectionTest, the system InputMethodManager service must
+ * be temporarily disabled to prevent the system IME from interfering with our
+ * tests. We disable the service by override the GeckoView's context with one
+ * that returns a null InputMethodManager service.
+ *
+ * @param test Test to run
+ */
+ public TextInput testInputConnection(final InputConnectionTest test) {
+
+ fAssertNotNull("Test must not be null", test);
+ assertInputConnection();
+
+ // GeckoInputConnection can run on another thread than the main thread,
+ // so we need to be testing it on that same thread it's running on
+ final View geckoView = getView();
+ final Handler inputConnectionHandler = geckoView.getHandler();
+ final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView);
+
+ setContext(new ContextWrapper(oldGeckoViewContext) {
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.INPUT_METHOD_SERVICE.equals(name)) {
+ return null;
+ }
+ return super.getSystemService(name);
+ }
+ });
+
+ (new InputConnectionTestRunner(test, inputConnectionHandler)).launch();
+
+ setContext(oldGeckoViewContext);
+ return this;
+ }
+
+ private class InputConnectionTestRunner implements Runnable {
+ private final InputConnectionTest mTest;
+ private boolean mDone;
+
+ public InputConnectionTestRunner(final InputConnectionTest test,
+ final Handler handler) {
+ test.inputConnectionHandler = handler;
+ mTest = test;
+ }
+
+ public synchronized void launch() {
+ // Below, we are blocking the instrumentation thread to wait on the
+ // InputConnection thread. Therefore, the InputConnection thread must not be
+ // the same as the instrumentation thread to avoid a deadlock. This should
+ // always be the case and we perform a sanity check to make sure.
+ fAssertNotSame("InputConnection should not be running on instrumentation thread",
+ Looper.myLooper(), mTest.inputConnectionHandler.getLooper());
+
+ mDone = false;
+ mTest.inputConnectionHandler.post(this);
+ do {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // Ignore interrupts
+ }
+ } while (!mDone);
+ }
+
+ @Override
+ public void run() {
+ final EditorInfo info = new EditorInfo();
+ final InputConnection ic = getView().onCreateInputConnection(info);
+ fAssertNotNull("Must have an InputConnection", ic);
+ // Restore the IC to a clean state
+ ic.clearMetaKeyStates(-1);
+ ic.finishComposingText();
+ mTest.test(ic, info);
+ synchronized (this) {
+ // Test finished; return from launch().
+ mDone = true;
+ notify();
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
new file mode 100644
index 000000000..e8a90b351
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
@@ -0,0 +1,56 @@
+package org.mozilla.gecko.tests.components;
+
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.widget.TwoWayView;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+/**
+ * A class representing any interactions that take place on the tablet tab strip.
+ */
+public class TabStripComponent extends BaseComponent {
+ // Using a text id because the layout and therefore the id might be stripped from the (non-tablet) build
+ private static final String TAB_STRIP_ID = "tab_strip";
+
+ public TabStripComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ public void switchToTab(int index) {
+ // The tab strip is only available on tablets
+ DeviceHelper.assertIsTablet();
+
+ View tabView = waitForTabView(index);
+ fAssertNotNull(String.format("Tab at index %d is not null", index), tabView);
+
+ mSolo.clickOnView(tabView);
+ }
+
+ private View waitForTabView(final int index) {
+ final TwoWayView tabStrip = getTabStripView();
+ final View[] tabView = new View[1];
+
+ WaitHelper.waitFor(String.format("Tab at index %d to be visible", index), new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (tabView[0] = tabStrip.getChildAt(index)) != null;
+ }
+ });
+
+ return tabView[0];
+ }
+
+ private TwoWayView getTabStripView() {
+ TwoWayView tabStrip = (TwoWayView) mSolo.getView("tab_strip");
+
+ fAssertNotNull("Tab strip is not null", tabStrip);
+
+ return tabStrip;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java
new file mode 100644
index 000000000..25101a395
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java
@@ -0,0 +1,326 @@
+/* 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.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.toolbar.PageActionLayout;
+
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Solo;
+
+/**
+ * A class representing any interactions that take place on the Toolbar.
+ */
+public class ToolbarComponent extends BaseComponent {
+
+ private static final String URL_HTTP_PREFIX = "http://";
+
+ // We are waiting up to 30 seconds instead of the default waiting time because reader mode
+ // parsing can take quite some time on slower devices (Bug 1142699)
+ private static final int READER_MODE_WAIT_MS = 30000;
+
+ public ToolbarComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ public ToolbarComponent assertIsEditing() {
+ fAssertTrue("The toolbar is in the editing state", isEditing());
+ return this;
+ }
+
+ public ToolbarComponent assertIsNotEditing() {
+ fAssertFalse("The toolbar is not in the editing state", isEditing());
+ return this;
+ }
+
+ public ToolbarComponent assertTitle(final String url) {
+ fAssertNotNull("The url argument is not null", url);
+
+ final String expected;
+ final String absoluteURL = NavigationHelper.adjustUrl(url);
+
+ if (mStringHelper.ABOUT_HOME_URL.equals(absoluteURL)) {
+ expected = mStringHelper.ABOUT_HOME_TITLE;
+ } else if (absoluteURL.startsWith(URL_HTTP_PREFIX)) {
+ expected = absoluteURL.substring(URL_HTTP_PREFIX.length());
+ } else {
+ expected = absoluteURL;
+ }
+
+ // Since we only display a shortened "base domain" (See bug 1236431) we use the content
+ // description to obtain the full URL.
+ fAssertEquals("The Toolbar title is " + expected, expected, getUrlFromContentDescription());
+ return this;
+ }
+
+ public ToolbarComponent assertUrl(final String expected) {
+ assertIsEditing();
+ fAssertEquals("The Toolbar url is " + expected, expected, getUrlEditText().getText());
+ return this;
+ }
+
+ public ToolbarComponent assertIsUrlEditTextSelected() {
+ fAssertTrue("The edit text is selected", isUrlEditTextSelected());
+ return this;
+ }
+
+ public ToolbarComponent assertIsUrlEditTextNotSelected() {
+ fAssertFalse("The edit text is not selected", isUrlEditTextSelected());
+ return this;
+ }
+
+ public ToolbarComponent assertBackButtonIsNotEnabled() {
+ fAssertFalse("The back button is not enabled", isBackButtonEnabled());
+ return this;
+ }
+
+ /**
+ * Returns the root View for the browser toolbar.
+ */
+ private View getToolbarView() {
+ mSolo.waitForView(R.id.browser_toolbar);
+ return mSolo.getView(R.id.browser_toolbar);
+ }
+
+ private EditText getUrlEditText() {
+ return (EditText) getToolbarView().findViewById(R.id.url_edit_text);
+ }
+
+ private View getUrlDisplayLayout() {
+ return getToolbarView().findViewById(R.id.display_layout);
+ }
+
+ private TextView getUrlTitleText() {
+ return (TextView) getToolbarView().findViewById(R.id.url_bar_title);
+ }
+
+ private ImageButton getBackButton() {
+ DeviceHelper.assertIsTablet();
+ return (ImageButton) getToolbarView().findViewById(R.id.back);
+ }
+
+ private ImageButton getForwardButton() {
+ DeviceHelper.assertIsTablet();
+ return (ImageButton) getToolbarView().findViewById(R.id.forward);
+ }
+
+ private ImageButton getReloadButton() {
+ DeviceHelper.assertIsTablet();
+ return (ImageButton) getToolbarView().findViewById(R.id.reload);
+ }
+
+ private PageActionLayout getPageActionLayout() {
+ return (PageActionLayout) getToolbarView().findViewById(R.id.page_action_layout);
+ }
+
+ private ImageButton getReaderModeButton() {
+ final PageActionLayout pageActionLayout = getPageActionLayout();
+ final int count = pageActionLayout.getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ final View view = pageActionLayout.getChildAt(i);
+ if (mStringHelper.CONTENT_DESCRIPTION_READER_MODE_BUTTON.equals(view.getContentDescription())) {
+ return (ImageButton) view;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the View for the edit cancel button in the browser toolbar.
+ */
+ private View getEditCancelButton() {
+ return getToolbarView().findViewById(R.id.edit_cancel);
+ }
+
+ private String getUrlFromContentDescription() {
+ assertIsNotEditing();
+
+ final CharSequence contentDescription = getUrlDisplayLayout().getContentDescription();
+ if (contentDescription == null) {
+ return "";
+ } else {
+ return contentDescription.toString();
+ }
+ }
+
+ /**
+ * Returns the title of the page. Note that this makes no assertions to Toolbar state and
+ * may return a value that may never be visible to the user. Callers likely want to use
+ * {@link assertTitle} instead.
+ */
+ public String getPotentiallyInconsistentTitle() {
+ return getTitleHelper(false);
+ }
+
+ private String getTitleHelper(final boolean shouldAssertNotEditing) {
+ if (shouldAssertNotEditing) {
+ assertIsNotEditing();
+ }
+
+ return getUrlTitleText().getText().toString();
+ }
+
+ private boolean isEditing() {
+ return getUrlDisplayLayout().getVisibility() != View.VISIBLE &&
+ getUrlEditText().getVisibility() == View.VISIBLE;
+ }
+
+ public ToolbarComponent enterEditingMode() {
+ assertIsNotEditing();
+
+ mSolo.clickOnView(getUrlTitleText(), true);
+
+ waitForEditing();
+ WaitHelper.waitFor("UrlEditText to be input method target", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return getUrlEditText().isInputMethodTarget();
+ }
+ });
+
+ return this;
+ }
+
+ public ToolbarComponent commitEditingMode() {
+ assertIsEditing();
+
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ mSolo.sendKey(Solo.ENTER);
+ }
+ });
+ waitForNotEditing();
+
+ return this;
+ }
+
+ public ToolbarComponent dismissEditingMode() {
+ assertIsEditing();
+
+ if (DeviceHelper.isTablet()) {
+ final EditText urlEditText = getUrlEditText();
+ if (urlEditText.isFocused()) {
+ mSolo.goBack();
+ }
+ mSolo.goBack();
+ } else {
+ mSolo.clickOnView(getEditCancelButton());
+ }
+
+ waitForNotEditing();
+
+ return this;
+ }
+
+ public ToolbarComponent enterUrl(final String url) {
+ fAssertNotNull("url is not null", url);
+
+ assertIsEditing();
+
+ final EditText urlEditText = getUrlEditText();
+ fAssertTrue("The UrlEditText is the input method target",
+ urlEditText.isInputMethodTarget());
+
+ mSolo.clearEditText(urlEditText);
+ mSolo.typeText(urlEditText, url);
+
+ return this;
+ }
+
+ public ToolbarComponent pressBackButton() {
+ final ImageButton backButton = getBackButton();
+ return pressButton(backButton, "back");
+ }
+
+ public ToolbarComponent pressForwardButton() {
+ final ImageButton forwardButton = getForwardButton();
+ return pressButton(forwardButton, "forward");
+ }
+
+ public ToolbarComponent pressReloadButton() {
+ final ImageButton reloadButton = getReloadButton();
+ return pressButton(reloadButton, "reload");
+ }
+
+ public ToolbarComponent pressReaderModeButton() {
+ final ImageButton readerModeButton = waitForReaderModeButton();
+ pressButton(readerModeButton, "reader mode");
+
+ return this;
+ }
+
+ private ToolbarComponent pressButton(final View view, final String buttonName) {
+ fAssertNotNull("The " + buttonName + " button View is not null", view);
+ fAssertTrue("The " + buttonName + " button is enabled", view.isEnabled());
+ fAssertEquals("The " + buttonName + " button is visible",
+ View.VISIBLE, view.getVisibility());
+ assertIsNotEditing();
+
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ mSolo.clickOnView(view);
+ }
+ });
+
+ return this;
+ }
+
+ private void waitForEditing() {
+ WaitHelper.waitFor("Toolbar to enter editing mode", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return isEditing();
+ }
+ });
+ }
+
+ private void waitForNotEditing() {
+ WaitHelper.waitFor("Toolbar to exit editing mode", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !isEditing();
+ }
+ });
+ }
+
+ private ImageButton waitForReaderModeButton() {
+ final ImageButton[] readerModeButton = new ImageButton[1];
+
+ WaitHelper.waitFor("the Reader mode button to be visible", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (readerModeButton[0] = getReaderModeButton()) != null;
+ }
+ }, READER_MODE_WAIT_MS);
+
+ return readerModeButton[0];
+ }
+
+ private boolean isUrlEditTextSelected() {
+ return getUrlEditText().isSelected();
+ }
+
+ private boolean isBackButtonEnabled() {
+ return getBackButton().isEnabled();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java
new file mode 100644
index 000000000..894d134d1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java
@@ -0,0 +1,112 @@
+/* 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.helpers;
+
+import java.util.Arrays;
+
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * Provides assertions in a JUnit-like API that wraps the robocop Assert interface.
+ */
+public final class AssertionHelper {
+ // Assert.ok has a "diag" ("diagnostic") parameter that has no useful purpose.
+ private static final String DIAG_STRING = "";
+
+ private static Assert sAsserter;
+
+ private AssertionHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sAsserter = context.getAsserter();
+ }
+
+ public static void fAssertArrayEquals(final String message, final byte[] expecteds, final byte[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final char[] expecteds, final char[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final short[] expecteds, final short[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final int[] expecteds, final int[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final long[] expecteds, final long[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final Object[] expecteds, final Object[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertEquals(final String message, final double expected, final double actual, final double delta) {
+ if (Double.compare(expected, actual) != 0) {
+ sAsserter.ok(Math.abs(expected - actual) <= delta, message, DIAG_STRING);
+ }
+ }
+
+ public static void fAssertEquals(final String message, final long expected, final long actual) {
+ sAsserter.is(actual, expected, message);
+ }
+
+ public static void fAssertEquals(final String message, final Object expected, final Object actual) {
+ sAsserter.is(actual, expected, message);
+ }
+
+ public static void fAssertNotEquals(final String message, final double unexpected, final double actual, final double delta) {
+ sAsserter.ok(Math.abs(unexpected - actual) > delta, message, DIAG_STRING);
+ }
+
+ public static void fAssertNotEquals(final String message, final long unexpected, final long actual) {
+ sAsserter.isnot(actual, unexpected, message);
+ }
+
+ public static void fAssertNotEquals(final String message, final Object unexpected, final Object actual) {
+ sAsserter.isnot(actual, unexpected, message);
+ }
+
+ public static void fAssertFalse(final String message, final boolean actual) {
+ sAsserter.ok(!actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertNotNull(final String message, final Object actual) {
+ sAsserter.isnot(actual, null, message);
+ }
+
+ public static void fAssertNotSame(final String message, final Object unexpected, final Object actual) {
+ sAsserter.ok(unexpected != actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertNull(final String message, final Object actual) {
+ sAsserter.is(actual, null, message);
+ }
+
+ public static void fAssertSame(final String message, final Object expected, final Object actual) {
+ sAsserter.ok(expected == actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertTrue(final String message, final boolean actual) {
+ sAsserter.ok(actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertIsPixel(final String message, final int actual, final int r, final int g, final int b) {
+ sAsserter.ispixel(actual, r, g, b, message);
+ }
+
+ public static void fAssertIsNotPixel(final String message, final int actual, final int r, final int g, final int b) {
+ sAsserter.isnotpixel(actual, r, g, b, message);
+ }
+
+ public static void fFail(final String message) {
+ sAsserter.ok(false, message, DIAG_STRING);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java
new file mode 100644
index 000000000..476bd34dd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java
@@ -0,0 +1,108 @@
+/* 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.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.tests.UITestContext;
+
+import android.app.Activity;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Provides general hardware (ex: configuration) and software (ex: version) information
+ * about the current test device and allows changing its configuration.
+ */
+public final class DeviceHelper {
+ public enum Type {
+ PHONE,
+ TABLET
+ }
+
+ public enum AndroidVersion {
+ v2x,
+ v3x,
+ v4x
+ }
+
+ private static Activity sActivity;
+ private static Solo sSolo;
+
+ private static Type sDeviceType;
+ private static AndroidVersion sAndroidVersion;
+
+ private static int sScreenHeight;
+ private static int sScreenWidth;
+
+ private DeviceHelper() { /* To disallow instantiation. */ }
+
+ public static void assertIsTablet() {
+ fAssertTrue("The device is a tablet", isTablet());
+ }
+
+ protected static void init(final UITestContext context) {
+ sActivity = context.getActivity();
+ sSolo = context.getSolo();
+
+ setAndroidVersion();
+ setScreenDimensions();
+ setDeviceType();
+ }
+
+ private static void setAndroidVersion() {
+ int sdk = Build.VERSION.SDK_INT;
+ if (sdk < Build.VERSION_CODES.HONEYCOMB) {
+ sAndroidVersion = AndroidVersion.v2x;
+ } else if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) {
+ sAndroidVersion = AndroidVersion.v4x;
+ } else {
+ sAndroidVersion = AndroidVersion.v3x;
+ }
+ }
+
+ private static void setScreenDimensions() {
+ final DisplayMetrics dm = new DisplayMetrics();
+ sActivity.getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+ sScreenHeight = dm.heightPixels;
+ sScreenWidth = dm.widthPixels;
+ }
+
+ private static void setDeviceType() {
+ sDeviceType = (GeckoAppShell.isTablet() ? Type.TABLET : Type.PHONE);
+ }
+
+ public static int getScreenHeight() {
+ return sScreenHeight;
+ }
+
+ public static int getScreenWidth() {
+ return sScreenWidth;
+ }
+
+ public static AndroidVersion getAndroidVersion() {
+ return sAndroidVersion;
+ }
+
+ public static boolean isPhone() {
+ return (sDeviceType == Type.PHONE);
+ }
+
+ public static boolean isTablet() {
+ return (sDeviceType == Type.TABLET);
+ }
+
+ public static void setLandscapeRotation() {
+ sSolo.setActivityOrientation(Solo.LANDSCAPE);
+ }
+
+ public static void setPortraitOrientation() {
+ sSolo.setActivityOrientation(Solo.LANDSCAPE);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java
new file mode 100644
index 000000000..d3c4d6390
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java
@@ -0,0 +1,94 @@
+/* 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.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import java.lang.reflect.Field;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * Provides helper functions for accessing Android framework features
+ *
+ * This class uses reflection to access framework functionalities that are
+ * unavailable through the regular Android API. Using reflection in this
+ * case is okay because it does not touch Gecko classes that go through
+ * ProGuard.
+ */
+public final class FrameworkHelper {
+
+ private FrameworkHelper() { /* To disallow instantiation. */ }
+
+ private static Field getClassField(final Class<?> clazz, final String fieldName)
+ throws NoSuchFieldException {
+ Class<?> cls = clazz;
+ do {
+ try {
+ return cls.getDeclaredField(fieldName);
+ } catch (final Exception e) {
+ // NoSuchFieldException is a documented exception of getDeclaredField
+ // and is frequently observed here. No other exceptions are documented
+ // for getDeclaredField. However, on Android 2.3, NoSuchMethodException
+ // is also observed, when called on some classes. This appears to be
+ // an Android bug reportedly fixed in Honeycomb. Since NoSuchMethodException
+ // is not declared, it cannot be caught, so we catch all Exceptions.
+ cls = cls.getSuperclass();
+ }
+ } while (cls != null);
+ // We tried getDeclaredField before; now try getField instead.
+ // getField behaves differently in that getField traverses the inheritance
+ // list, but it only works on public fields. While getField won't get us
+ // anything new, it makes code cleaner by throwing an exception for us.
+ return clazz.getField(fieldName);
+ }
+
+ private static Object getField(final Object obj, final String fieldName) {
+ try {
+ final Field field = getClassField(obj.getClass(), fieldName);
+ final boolean accessible = field.isAccessible();
+ field.setAccessible(true);
+ final Object ret = field.get(obj);
+ field.setAccessible(accessible);
+ return ret;
+ } catch (final NoSuchFieldException e) {
+ // We expect a valid field name; if it's not valid,
+ // the caller is doing something wrong and should be fixed.
+ fFail("Argument field should be a valid field name: " + e.toString());
+ } catch (final IllegalAccessException e) {
+ // This should not happen. If it does, setAccessible above is not working.
+ fFail("Field should be accessible: " + e.toString());
+ }
+ throw new IllegalStateException("Should not continue from previous failures");
+ }
+
+ private static void setField(final Object obj, final String fieldName, final Object value) {
+ try {
+ final Field field = getClassField(obj.getClass(), fieldName);
+ final boolean accessible = field.isAccessible();
+ field.setAccessible(true);
+ field.set(obj, value);
+ field.setAccessible(accessible);
+ return;
+ } catch (final NoSuchFieldException e) {
+ // We expect a valid field name; if it's not valid,
+ // the caller is doing something wrong and should be fixed.
+ fFail("Argument field should be a valid field name: " + e.toString());
+ } catch (final IllegalAccessException e) {
+ // This should not happen. If it does, setAccessible above is not working.
+ fFail("Field should be accessible: " + e.toString());
+ }
+ throw new IllegalStateException("Cannot continue from previous failures");
+ }
+
+ public static Context getViewContext(final View v) {
+ return (Context) getField(v, "mContext");
+ }
+
+ public static void setViewContext(final View v, final Context c) {
+ setField(v, "mContext", c);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java
new file mode 100644
index 000000000..b8d1ef0ce
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java
@@ -0,0 +1,50 @@
+package org.mozilla.gecko.tests.helpers;
+
+import android.app.Activity;
+import android.util.DisplayMetrics;
+
+import com.robotium.solo.Solo;
+
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.tests.StringHelper;
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * Provides helper functions for clicking elements rendered by the Gecko engine.
+ */
+public class GeckoClickHelper {
+ private static Solo sSolo;
+ private static Activity sActivity;
+ private static Driver sDriver;
+
+ protected static void init(final UITestContext context) {
+ sSolo = context.getSolo();
+ sActivity = context.getActivity();
+ sDriver = context.getDriver();
+ }
+
+ private GeckoClickHelper() { /* To disallow instantiation. */ }
+
+ /**
+ * Long press the link and select "Open Link in New Tab" from the context menu.
+ *
+ * The link should be positioned at the top of the page, at least 60px high and
+ * aligned to the middle.
+ */
+ public static void openCentralizedLinkInNewTab() {
+ openLinkContextMenu();
+
+ // Click on "Open Link in New Tab"
+ sSolo.clickOnText(StringHelper.get().CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]);
+ }
+
+ private static void openLinkContextMenu() {
+ DisplayMetrics dm = new DisplayMetrics();
+ sActivity.getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+ sSolo.clickLongOnScreen(
+ sDriver.getGeckoLeft() + sDriver.getGeckoWidth() / 2,
+ sDriver.getGeckoTop() + 30 * dm.density
+ );
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
new file mode 100644
index 000000000..cd75b7255
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
@@ -0,0 +1,49 @@
+/* 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.helpers;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Actions.EventExpecter;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.tests.UITestContext;
+
+import android.app.Activity;
+
+/**
+ * Provides helper functions for accessing the underlying Gecko engine.
+ */
+public final class GeckoHelper {
+ private static Activity sActivity;
+ private static Actions sActions;
+
+ private GeckoHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sActivity = context.getActivity();
+ sActions = context.getActions();
+ }
+
+ public static void blockForReady() {
+ blockForEvent("Gecko:Ready");
+ }
+
+ /**
+ * Blocks for the "Gecko:DelayedStartup" event, which occurs after "Gecko:Ready" and the
+ * first page load.
+ */
+ public static void blockForDelayedStartup() {
+ blockForEvent("Gecko:DelayedStartup");
+ }
+
+ private static void blockForEvent(final String eventName) {
+ final EventExpecter eventExpecter = sActions.expectGeckoEvent(eventName);
+
+ if (!GeckoThread.isRunning()) {
+ eventExpecter.blockForEvent();
+ }
+
+ eventExpecter.unregisterListener();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java
new file mode 100644
index 000000000..229dc1062
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java
@@ -0,0 +1,30 @@
+/* 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.helpers;
+
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * AssertionHelper is statically imported in many places. Thus we want to hide
+ * its init method outside of this package. We initialize the remaining helper
+ * classes from here so that all the init methods are package protected.
+ */
+public final class HelperInitializer {
+
+ private HelperInitializer() { /* To disallow instantiation. */ }
+
+ public static void init(final UITestContext context) {
+ // Other helpers make assertions so init AssertionHelper first.
+ AssertionHelper.init(context);
+
+ DeviceHelper.init(context);
+ GeckoClickHelper.init(context);
+ GeckoHelper.init(context);
+ JavascriptBridge.init(context);
+ NavigationHelper.init(context);
+ RobotiumHelper.init(context);
+ WaitHelper.init(context);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java
new file mode 100644
index 000000000..1b0ece1cd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java
@@ -0,0 +1,394 @@
+/* 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.helpers;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import junit.framework.AssertionFailedError;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Actions.EventExpecter;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * Javascript bridge allows calls to and from JavaScript.
+ *
+ * To establish communication, create an instance of JavascriptBridge in Java and pass in
+ * an object that will receive calls from JavaScript. For example:
+ *
+ * {@code final JavascriptBridge js = new JavascriptBridge(javaObj);}
+ *
+ * Next, create an instance of JavaBridge in JavaScript and pass in another object
+ * that will receive calls from Java. For example:
+ *
+ * {@code let java = new JavaBridge(jsObj);}
+ *
+ * Once a link is established, calls can be made using the methods syncCall and asyncCall.
+ * syncCall waits for the call to finish before returning. For example:
+ *
+ * {@code js.syncCall("abc", 1, 2, 3);} will synchronously call the JavaScript method
+ * jsObj.abc and pass in arguments 1, 2, and 3.
+ *
+ * {@code java.asyncCall("def", 4, 5, 6);} will asynchronously call the Java method
+ * javaObj.def and pass in arguments 4, 5, and 6.
+ *
+ * Supported argument types include int, double, boolean, String, and JSONObject. Note
+ * that only implicit conversion is done, meaning if a floating point argument is passed
+ * from JavaScript to Java, the call will fail if the Java method has an int argument.
+ *
+ * Because JavascriptBridge and JavaBridge use one underlying communication channel,
+ * creating multiple instances of them will not create independent links.
+ *
+ * Note also that because Robocop tests finish as soon as the Java test method returns,
+ * the last call to JavaScript from Java must be a synchronous call. Otherwise, the test
+ * will finish before the JavaScript method is run. Calls to Java from JavaScript do not
+ * have this requirement. Because of these considerations, calls from Java to JavaScript
+ * are usually synchronous and calls from JavaScript to Java are usually asynchronous.
+ * See testJavascriptBridge.java for examples.
+ */
+public final class JavascriptBridge {
+
+ private static enum MessageStatus {
+ QUEUE_EMPTY, // Did not process a message; queue was empty.
+ PROCESSED, // A message other than sync was processed.
+ REPLIED, // A sync message was processed.
+ SAVED, // An async message was saved; see processMessage().
+ };
+
+ @SuppressWarnings("serial")
+ public static class CallException extends RuntimeException {
+ public CallException() {
+ super();
+ }
+
+ public CallException(final String msg) {
+ super(msg);
+ }
+
+ public CallException(final String msg, final Throwable e) {
+ super(msg, e);
+ }
+
+ public CallException(final Throwable e) {
+ super(e);
+ }
+ }
+
+ public static final String EVENT_TYPE = "Robocop:JS";
+
+ private static Actions sActions;
+ private static Assert sAsserter;
+
+ // Target of JS-to-Java calls
+ private final Object mTarget;
+ // List of public methods in subclass
+ private final Method[] mMethods;
+ // Parser for handling xpcshell assertions
+ private final JavascriptMessageParser mLogParser;
+ // Expecter of our internal Robocop event
+ private final EventExpecter mExpecter;
+ // Saved async message; see processMessage() for its purpose.
+ private JSONObject mSavedAsyncMessage;
+ // Number of levels in the synchronous call stack
+ private int mCallStackDepth;
+ // If JavaBridge has been loaded
+ private boolean mJavaBridgeLoaded;
+
+ /* package */ static void init(final UITestContext context) {
+ sActions = context.getActions();
+ sAsserter = context.getAsserter();
+ }
+
+ public JavascriptBridge(final Object target) {
+ mTarget = target;
+ mMethods = target.getClass().getMethods();
+ mExpecter = sActions.expectGeckoEvent(EVENT_TYPE);
+ // The JS here is unrelated to a test harness, so we
+ // have our message parser end on assertion failure.
+ mLogParser = new JavascriptMessageParser(sAsserter, true);
+ }
+
+ /**
+ * Synchronously calls a method in Javascript.
+ *
+ * @param method Name of the method to call
+ * @param args Arguments to pass to the Javascript method; must be a list of
+ * values allowed by JSONObject.
+ */
+ public void syncCall(final String method, final Object... args) {
+ mCallStackDepth++;
+
+ sendMessage("sync-call", method, args);
+ try {
+ while (processPendingMessage() != MessageStatus.REPLIED) {
+ }
+ } catch (final AssertionFailedError e) {
+ // Most likely an event expecter time out
+ throw new CallException("Cannot call " + method, e);
+ }
+
+ // If syncCall was called reentrantly from processPendingMessage(), mCallStackDepth
+ // will be greater than 1 here. In that case we don't have to wait for pending calls
+ // because the outermost syncCall will do it for us.
+ if (mCallStackDepth == 1) {
+ // We want to wait for all asynchronous calls to finish,
+ // because the test may end immediately after this method returns.
+ finishPendingCalls();
+ }
+ mCallStackDepth--;
+ }
+
+ /**
+ * Asynchronously calls a method in Javascript.
+ *
+ * @param method Name of the method to call
+ * @param args Arguments to pass to the Javascript method; must be a list of
+ * values allowed by JSONObject.
+ */
+ public void asyncCall(final String method, final Object... args) {
+ sendMessage("async-call", method, args);
+ }
+
+ /**
+ * Disconnect the bridge.
+ */
+ public void disconnect() {
+ mExpecter.unregisterListener();
+ }
+
+ /**
+ * Process a new message; wait for new message if necessary.
+ *
+ * @return MessageStatus value to indicate result of processing the message
+ */
+ private MessageStatus processPendingMessage() {
+ // We're on the test thread.
+ // We clear mSavedAsyncMessage in maybeProcessPendingMessage() but not here,
+ // because we always have a new message for processing here, so we never
+ // get a chance to clear mSavedAsyncMessage.
+ try {
+ final String message = mExpecter.blockForEventData();
+ return processMessage(new JSONObject(message));
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Invalid message", e);
+ }
+ }
+
+ /**
+ * Process a message if a new or saved message is available.
+ *
+ * @return MessageStatus value to indicate result of processing the message
+ */
+ private MessageStatus maybeProcessPendingMessage() {
+ // We're on the test thread.
+ final String message = mExpecter.blockForEventDataWithTimeout(0);
+ if (message != null) {
+ try {
+ return processMessage(new JSONObject(message));
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Invalid message", e);
+ }
+ }
+ if (mSavedAsyncMessage != null) {
+ // processMessage clears mSavedAsyncMessage.
+ return processMessage(mSavedAsyncMessage);
+ }
+ return MessageStatus.QUEUE_EMPTY;
+ }
+
+ /**
+ * Wait for all asynchronous messages from Javascript to be processed.
+ */
+ private void finishPendingCalls() {
+ MessageStatus result;
+ do {
+ result = maybeProcessPendingMessage();
+ if (result == MessageStatus.REPLIED) {
+ throw new IllegalStateException("Sync reply was unexpected");
+ }
+ } while (result != MessageStatus.QUEUE_EMPTY);
+ }
+
+ private void ensureJavaBridgeLoaded() {
+ while (!mJavaBridgeLoaded) {
+ processPendingMessage();
+ }
+ }
+
+ private void sendMessage(final String innerType, final String method, final Object[] args) {
+ ensureJavaBridgeLoaded();
+
+ // Call from Java to Javascript
+ final JSONObject message = new JSONObject();
+ final JSONArray jsonArgs = new JSONArray();
+ try {
+ if (args != null) {
+ for (final Object arg : args) {
+ jsonArgs.put(convertToJSONValue(arg));
+ }
+ }
+ message.put("type", EVENT_TYPE)
+ .put("innerType", innerType)
+ .put("method", method)
+ .put("args", jsonArgs);
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Unable to create JSON message", e);
+ }
+ sActions.sendGeckoEvent(EVENT_TYPE, message.toString());
+ }
+
+ private MessageStatus processMessage(JSONObject message) {
+ final String type;
+ final String methodName;
+ final JSONArray argsArray;
+ final Object[] args;
+ try {
+ if (!EVENT_TYPE.equals(message.getString("type"))) {
+ throw new IllegalStateException("Message type is not " + EVENT_TYPE);
+ }
+ type = message.getString("innerType");
+
+ switch (type) {
+ case "progress":
+ // Javascript harness message
+ mLogParser.logMessage(message.getString("message"));
+ return MessageStatus.PROCESSED;
+
+ case "notify-loaded":
+ mJavaBridgeLoaded = true;
+ return MessageStatus.PROCESSED;
+
+ case "sync-reply":
+ // Reply to Java-to-Javascript sync call
+ return MessageStatus.REPLIED;
+
+ case "sync-call":
+ case "async-call":
+
+ if ("async-call".equals(type)) {
+ // Save this async message until another async message arrives, then we
+ // process the saved message and save the new one. This is done as a
+ // form of tail call optimization, by making sync-replies come before
+ // async-calls. On the other hand, if (message == mSavedAsyncMessage),
+ // it means we're currently processing the saved message and should clear
+ // mSavedAsyncMessage.
+ final JSONObject newSavedMessage =
+ (message != mSavedAsyncMessage ? message : null);
+ message = mSavedAsyncMessage;
+ mSavedAsyncMessage = newSavedMessage;
+ if (message == null) {
+ // Saved current message and there wasn't an already saved one.
+ return MessageStatus.SAVED;
+ }
+ }
+
+ methodName = message.getString("method");
+ argsArray = message.getJSONArray("args");
+ args = new Object[argsArray.length()];
+ for (int i = 0; i < args.length; i++) {
+ args[i] = convertFromJSONValue(argsArray.get(i));
+ }
+ invokeMethod(methodName, args);
+
+ if ("sync-call".equals(type)) {
+ // Reply for sync messages
+ sendMessage("sync-reply", methodName, null);
+ }
+ return MessageStatus.PROCESSED;
+ }
+
+ throw new IllegalStateException("Message type is unexpected");
+
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Unable to retrieve JSON message", e);
+ }
+ }
+
+ /**
+ * Given a method name and a list of arguments,
+ * call the most suitable method in the subclass.
+ */
+ private Object invokeMethod(final String methodName, final Object[] args) {
+ final Class<?>[] argTypes = new Class<?>[args.length];
+ for (int i = 0; i < argTypes.length; i++) {
+ if (args[i] == null) {
+ argTypes[i] = Object.class;
+ } else {
+ argTypes[i] = args[i].getClass();
+ }
+ }
+
+ // Try using argument types directly without casting.
+ try {
+ return invokeMethod(mTarget.getClass().getMethod(methodName, argTypes), args);
+ } catch (final NoSuchMethodException e) {
+ // getMethod() failed; try fallback below.
+ }
+
+ // One scenario for getMethod() to fail above is that we don't have the exact
+ // argument types in argTypes (e.g. JS gave us an int but we're using a double,
+ // or JS gave us a null and we don't know its intended type), or the number of
+ // arguments is incorrect. Now we find all the methods with the given name and
+ // try calling them one-by-one. If one call fails, we move to the next call.
+ // Java will try to convert our arguments to the right types.
+ Throwable lastException = null;
+ for (final Method method : mMethods) {
+ if (!method.getName().equals(methodName)) {
+ continue;
+ }
+ try {
+ return invokeMethod(method, args);
+ } catch (final IllegalArgumentException e) {
+ lastException = e;
+ // Try the next method
+ } catch (final UnsupportedOperationException e) {
+ // "Cannot access method" exception below, see if there are other public methods
+ lastException = e;
+ // Try the next method
+ }
+ }
+ // Now we're out of options
+ throw new UnsupportedOperationException(
+ "Cannot call method " + methodName + " (not public? wrong argument types?)",
+ lastException);
+ }
+
+ private Object invokeMethod(final Method method, final Object[] args) {
+ try {
+ return method.invoke(mTarget, args);
+ } catch (final IllegalAccessException e) {
+ throw new UnsupportedOperationException(
+ "Cannot access method " + method.getName(), e);
+ } catch (final InvocationTargetException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof CallException) {
+ // Don't wrap CallExceptions; this can happen if a call is nested on top
+ // of existing sync calls, and the nested call throws a CallException
+ throw (CallException) cause;
+ }
+ throw new CallException("Failed to invoke " + method.getName(), cause);
+ }
+ }
+
+ private Object convertFromJSONValue(final Object value) {
+ if (value == JSONObject.NULL) {
+ return null;
+ }
+ return value;
+ }
+
+ private Object convertToJSONValue(final Object value) {
+ if (value == null) {
+ return JSONObject.NULL;
+ }
+ return value;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java
new file mode 100644
index 000000000..6237f1adc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java
@@ -0,0 +1,100 @@
+/* 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.helpers;
+
+import org.mozilla.gecko.Assert;
+
+import junit.framework.AssertionFailedError;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Route messages from Javascript's head.js test framework into Java's
+ * Mochitest framework.
+ */
+public final class JavascriptMessageParser {
+
+ /**
+ * The Javascript test harness sends test events to Java.
+ * Each such test event is wrapped in a Robocop:JS event.
+ */
+ public static final String EVENT_TYPE = "Robocop:JS";
+
+ // Messages matching this pattern are handled specially. Messages not
+ // matching this pattern are still printed. This pattern should be able
+ // to handle having multiple lines in a message.
+ private static final Pattern testMessagePattern =
+ Pattern.compile("TEST-([A-Z\\-]+) \\| (.*?) \\| (.*)", Pattern.DOTALL);
+
+ private final Assert asserter;
+ // Used to help print stack traces neatly.
+ private String lastTestName = "";
+ // Have we seen a message saying the test is finished?
+ private boolean testFinishedMessageSeen = false;
+ private final boolean endOnAssertionFailure;
+
+ /**
+ * Constructs a message parser for test result messages sent from JavaScript. When seeing an
+ * assertion failure, the message parser can use the given {@link org.mozilla.gecko.Assert}
+ * instance to immediately end the test (typically if the underlying JS framework is not able
+ * to end the test itself) or to swallow the Errors - this functionality is determined by the
+ * <code>endOnAssertionFailure</code> parameter.
+ *
+ * @param asserter The Assert instance to which test results should be passed.
+ * @param endOnAssertionFailure
+ * true if the test should end if we see a JS assertion failure, false otherwise.
+ */
+ public JavascriptMessageParser(final Assert asserter, final boolean endOnAssertionFailure) {
+ this.asserter = asserter;
+ this.endOnAssertionFailure = endOnAssertionFailure;
+ }
+
+ public boolean isTestFinished() {
+ return testFinishedMessageSeen;
+ }
+
+ public void logMessage(final String str) {
+ final Matcher m = testMessagePattern.matcher(str.trim());
+
+ if (m.matches()) {
+ final String type = m.group(1);
+ final String name = m.group(2);
+ final String message = m.group(3);
+
+ if ("INFO".equals(type)) {
+ asserter.info(name, message);
+ testFinishedMessageSeen = testFinishedMessageSeen ||
+ "exiting test".equals(message);
+ } else if ("PASS".equals(type)) {
+ asserter.ok(true, name, message);
+ } else if ("UNEXPECTED-FAIL".equals(type)) {
+ try {
+ asserter.ok(false, name, message);
+ } catch (AssertionFailedError e) {
+ // Above, we call the assert, allowing it to log.
+ // Now we can end the test, if applicable.
+ if (this.endOnAssertionFailure) {
+ throw e;
+ }
+ // Otherwise, swallow the Error. The JS framework we're
+ // logging messages from is likely capable of ending tests
+ // when it needs to, and we want to see all of its failures,
+ // not just the first one!
+ }
+ } else if ("KNOWN-FAIL".equals(type)) {
+ asserter.todo(false, name, message);
+ } else if ("UNEXPECTED-PASS".equals(type)) {
+ asserter.todo(true, name, message);
+ }
+
+ lastTestName = name;
+ } else {
+ // Generally, these extra lines are stack traces from failures,
+ // so we print them with the name of the last test seen.
+ asserter.info(lastTestName, str.trim());
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java
new file mode 100644
index 000000000..e3ccc8236
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java
@@ -0,0 +1,104 @@
+/* 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.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.UITestContext.ComponentType;
+import org.mozilla.gecko.tests.components.AppMenuComponent;
+import org.mozilla.gecko.tests.components.ToolbarComponent;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Provides helper functionality for navigating around the Firefox UI. These functions will often
+ * combine actions taken on multiple components to perform larger interactions.
+ */
+final public class NavigationHelper {
+ private static UITestContext sContext;
+ private static Solo sSolo;
+
+ private static AppMenuComponent sAppMenu;
+ private static ToolbarComponent sToolbar;
+
+ protected static void init(final UITestContext context) {
+ sContext = context;
+ sSolo = context.getSolo();
+
+ sAppMenu = (AppMenuComponent) context.getComponent(ComponentType.APPMENU);
+ sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR);
+ }
+
+ public static void enterAndLoadUrl(String url) {
+ fAssertNotNull("url is not null", url);
+
+ url = adjustUrl(url);
+ sToolbar.enterEditingMode()
+ .enterUrl(url)
+ .commitEditingMode();
+ }
+
+ /**
+ * Returns a new URL with the docshell HTTP server host prefix.
+ */
+ public static String adjustUrl(final String url) {
+ fAssertNotNull("url is not null", url);
+
+ if (url.startsWith("about:") || url.startsWith("chrome:")) {
+ return url;
+ }
+
+ return sContext.getAbsoluteHostnameUrl(url);
+ }
+
+ public static void goBack() {
+ if (DeviceHelper.isTablet()) {
+ sToolbar.pressBackButton(); // Waits for page load & asserts isNotEditing.
+ return;
+ }
+
+ sToolbar.assertIsNotEditing();
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ // TODO: Lower soft keyboard first if applicable. Note that
+ // Solo.hideSoftKeyboard() does not clear focus (which might be fine since
+ // Gecko would be the element focused).
+ sSolo.goBack();
+ }
+ });
+ }
+
+ public static void goForward() {
+ if (DeviceHelper.isTablet()) {
+ sToolbar.pressForwardButton(); // Waits for page load & asserts isNotEditing.
+ return;
+ }
+
+ sToolbar.assertIsNotEditing();
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.FORWARD);
+ }
+ });
+ }
+
+ public static void reload() {
+ if (DeviceHelper.isTablet()) {
+ sToolbar.pressReloadButton(); // Waits for page load & asserts isNotEditing.
+ return;
+ }
+
+ sToolbar.assertIsNotEditing();
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.RELOAD);
+ }
+ });
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java
new file mode 100644
index 000000000..2536eb9db
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java
@@ -0,0 +1,43 @@
+/* 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.helpers;
+
+import com.robotium.solo.Solo;
+
+import org.mozilla.gecko.tests.UITestContext;
+
+import java.util.regex.Pattern;
+
+/**
+ * Provides helper functions for using Robotium.
+ */
+public final class RobotiumHelper {
+ private static Solo sSolo;
+
+ private RobotiumHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sSolo = context.getSolo();
+ }
+
+ /**
+ * Same as Solo.waitForText(), but matching against full text, without regular expressions.
+ */
+ public static boolean waitForExactText(final String text,
+ final int minimumNumberOfMatches,
+ final long timeout) {
+ String matchText = "^" + Pattern.quote(text) + "$";
+ return sSolo.waitForText(matchText, minimumNumberOfMatches, timeout);
+ }
+
+ /**
+ * Same as Solo.searchText(), but matching against full text, without regular expressions.
+ */
+ public static boolean searchExactText(final String text,
+ final boolean onlyVisible) {
+ String matchText = "^" + Pattern.quote(text) + "$";
+ return sSolo.searchText(matchText, onlyVisible);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java
new file mode 100644
index 000000000..f6e616652
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java
@@ -0,0 +1,215 @@
+/* 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.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import android.os.SystemClock;
+
+import java.util.concurrent.Callable;
+import java.util.regex.Pattern;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Actions.EventExpecter;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.UITestContext.ComponentType;
+import org.mozilla.gecko.tests.components.ToolbarComponent;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Solo;
+
+/**
+ * Provides functionality related to waiting on certain events to happen.
+ */
+public final class WaitHelper {
+ // TODO: Make public for when Solo.waitForCondition is used directly (i.e. do not want
+ // assertion from waitFor)?
+ // DEFAULT_MAX_WAIT_MS of 5000 was intermittently insufficient during
+ // initialization on Android 2.3 emulator -- bug 1114655
+ private static final int DEFAULT_MAX_WAIT_MS = 15000;
+ private static final int PAGE_LOAD_WAIT_MS = 10000;
+ private static final int CHANGE_WAIT_MS = 15000;
+
+ // TODO: via lucasr - Add ThrobberVisibilityChangeVerifier?
+ private static final ChangeVerifier[] PAGE_LOAD_VERIFIERS = new ChangeVerifier[] {
+ new ToolbarTitleTextChangeVerifier()
+ };
+
+ private static UITestContext sContext;
+ private static Solo sSolo;
+ private static Actions sActions;
+
+ private static ToolbarComponent sToolbar;
+
+ private WaitHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sContext = context;
+ sSolo = context.getSolo();
+ sActions = context.getActions();
+
+ sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR);
+ }
+
+ /**
+ * Waits for the given {@link solo.Condition} using the default wait duration; will throw an
+ * AssertionError if the duration is elapsed and the condition is not satisfied.
+ */
+ public static void waitFor(String message, final Condition condition) {
+ message = "Waiting for " + message + ".";
+ fAssertTrue(message, sSolo.waitForCondition(condition, DEFAULT_MAX_WAIT_MS));
+ }
+
+ /**
+ * Waits for the given {@link solo.Condition} using the given wait duration; will throw an
+ * AssertionError if the duration is elapsed and the condition is not satisfied.
+ */
+ public static void waitFor(String message, final Condition condition, final int waitMillis) {
+ message = "Waiting for " + message + " with timeout " + waitMillis + ".";
+ fAssertTrue(message, sSolo.waitForCondition(condition, waitMillis));
+ }
+
+ /**
+ * Waits for the given Callable to return something that is not null, using the given wait
+ * duration; will throw an AssertionError if the duration is elapsed and the callable has not
+ * returned a non-null object.
+ *
+ * @return the value returned by the Callable. Or null if the duration has elapsed.
+ */
+ public static <V> V waitFor(String message, final Callable<V> callable, int waitMillis) {
+ sContext.dumpLog("WaitHelper", "Waiting for " + message + " with timeout " + waitMillis + ".");
+
+ final Object[] value = new Object[1];
+
+ Condition condition = new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ try {
+ V result = callable.call();
+ value[0] = result;
+ return result != null;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ };
+
+ sSolo.waitForCondition(condition, waitMillis);
+
+ return (V) value[0];
+ }
+
+ /**
+ * Waits for the Gecko event declaring the page has loaded. Takes in and runs a Runnable
+ * that will perform the action that will cause the page to load.
+ */
+ public static void waitForPageLoad(final Runnable initiatingAction) {
+ fAssertNotNull("initiatingAction is not null", initiatingAction);
+
+ // Some changes to the UI occur in response to the same event we listen to for when
+ // the page has finished loading (e.g. a page title update). As such, we ensure this
+ // UI state has changed before returning from this method; here we store the initial
+ // state.
+ final ChangeVerifier[] pageLoadVerifiers = PAGE_LOAD_VERIFIERS;
+ for (final ChangeVerifier verifier : pageLoadVerifiers) {
+ verifier.storeState();
+ }
+
+ // Wait for the page load and title changed event.
+ final EventExpecter[] eventExpecters = new EventExpecter[] {
+ sActions.expectGeckoEvent("DOMContentLoaded"),
+ sActions.expectGeckoEvent("DOMTitleChanged")
+ };
+
+ initiatingAction.run();
+
+ // PAGE_LOAD_WAIT_MS is the total time we wait for all events to finish.
+ final long expecterStartMillis = SystemClock.uptimeMillis();
+ for (final EventExpecter expecter : eventExpecters) {
+ final int eventWaitTimeMillis = PAGE_LOAD_WAIT_MS - (int)(SystemClock.uptimeMillis() - expecterStartMillis);
+ expecter.blockForEventDataWithTimeout(eventWaitTimeMillis);
+ expecter.unregisterListener();
+ }
+
+ // The timeout wait time should be the aggregate time for all ChangeVerifiers.
+ final long verifierStartMillis = SystemClock.uptimeMillis();
+
+ // Verify remaining state has changed.
+ for (final ChangeVerifier verifier : pageLoadVerifiers) {
+ // If we timeout, either the state is set to the same value (which is fine), or
+ // the state has not yet changed. Since we can't be sure it will ever change, move
+ // on and let the assertions fail if applicable.
+ final int verifierWaitMillis = CHANGE_WAIT_MS - (int)(SystemClock.uptimeMillis() - verifierStartMillis);
+ final boolean hasTimedOut = !sSolo.waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return verifier.hasStateChanged();
+ }
+ }, verifierWaitMillis);
+
+ sContext.dumpLog(verifier.getLogTag(),
+ (hasTimedOut ? "timed out." : "was satisfied."));
+ }
+ }
+
+ /**
+ * Implementations of this interface verify that the state of the test has changed from
+ * the invocation of storeState to the invocation of hasStateChanged. A boolean will be
+ * returned from hasStateChanged, indicating this change of status.
+ */
+ private interface ChangeVerifier {
+ String getLogTag();
+
+ /**
+ * Stores the initial state of the system. This system state is used to diff against
+ * the end state to determine if the system has changed. Since this is just a diff
+ * (with a timeout), this method could potentially store state inconsistent with
+ * what is visible to the user.
+ */
+ void storeState();
+ boolean hasStateChanged();
+ }
+
+ private static class ToolbarTitleTextChangeVerifier implements ChangeVerifier {
+ private static final String LOGTAG = ToolbarTitleTextChangeVerifier.class.getSimpleName();
+
+ // A regex that matches the page title that shows up while the page is loading.
+ private static final Pattern LOADING_PREFIX = Pattern.compile("[A-Za-z]{3,9}://");
+
+ private CharSequence mOldTitleText;
+
+ @Override
+ public String getLogTag() {
+ return LOGTAG;
+ }
+
+ @Override
+ public void storeState() {
+ mOldTitleText = sToolbar.getPotentiallyInconsistentTitle();
+ sContext.dumpLog(LOGTAG, "stored title, \"" + mOldTitleText + "\".");
+ }
+
+ @Override
+ public boolean hasStateChanged() {
+ // TODO: Additionally, consider Solo.waitForText.
+ // TODO: Robocop sleeps .5 sec between calls. Cache title view?
+ final CharSequence title = sToolbar.getPotentiallyInconsistentTitle();
+
+ // TODO: Handle the case where the URL is shown instead of page title by preference.
+ // HACK: We want to wait until the title changes to the state a tester may assert
+ // (e.g. the page title). However, the title is set to the URL before the title is
+ // loaded from the server and set as the final page title; we ignore the
+ // intermediate URL loading state here.
+ final boolean isLoading = LOADING_PREFIX.matcher(title).lookingAt();
+ final boolean hasStateChanged = !isLoading && !mOldTitleText.equals(title);
+
+ if (hasStateChanged) {
+ sContext.dumpLog(LOGTAG, "state changed to title, \"" + title + "\".");
+ }
+ return hasStateChanged;
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java
new file mode 100644
index 000000000..e3afeb8d9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java
@@ -0,0 +1,240 @@
+/* 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 org.mozilla.gecko.AppConstants;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.robotium.solo.Condition;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of the ANR reporter.
+ */
+public class testANRReporter extends BaseTest {
+
+ private static final String ANR_ACTION = "android.intent.action.ANR";
+ private static final String PING_DIR = "saved-telemetry-pings";
+ private static final int WAIT_FOR_PING_TIMEOUT = 60000;
+ private static final String ANR_PATH = "/data/anr/traces.txt";
+ private static final String SAMPLE_ANR
+ = "----- pid 1 at 2014-01-15 18:55:51 -----\n"
+ + "Cmd line: " + AppConstants.ANDROID_PACKAGE_NAME + "\n"
+ + "\n"
+ + "JNI: CheckJNI is off; workarounds are off; pins=0; globals=397\n"
+ + "\n"
+ + "DALVIK THREADS:\n"
+ + "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)\n"
+ + "\n"
+ + "\"main\" prio=5 tid=1 WAIT\n"
+ + " | group=\"main\" sCount=1 dsCount=0 obj=0x41d6bc90 self=0x41d5a3c8\n"
+ + " | sysTid=3485 nice=0 sched=0/0 cgrp=apps handle=1074852180\n"
+ + " | state=S schedstat=( 0 0 0 ) utm=1065 stm=152 core=0\n"
+ + " at java.lang.Object.wait(Native Method)\n"
+ + " - waiting on <0x427ab340> (a org.mozilla.gecko.GeckoEditable$5)\n"
+ + " at java.lang.Object.wait(Object.java:364)\n"
+ + " at org.mozilla.gecko.GeckoEditable$5.run(GeckoEditable.java:746)\n"
+ + " at android.os.Handler.handleCallback(Handler.java:733)\n"
+ + " at android.os.Handler.dispatchMessage(Handler.java:95)\n"
+ + " at android.os.Looper.loop(Looper.java:137)\n"
+ + " at android.app.ActivityThread.main(ActivityThread.java:4998)\n"
+ + " at java.lang.reflect.Method.invokeNative(Native Method)\n"
+ + " at java.lang.reflect.Method.invoke(Method.java:515)\n"
+ + " at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)\n"
+ + " at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)\n"
+ + " at dalvik.system.NativeStart.main(Native Method)\n"
+ + "\n"
+ + "\"Gecko\" prio=5 tid=16 SUSPENDED\n"
+ + " | group=\"main\" sCount=1 dsCount=0 obj=0x426e2b28 self=0x76ae92e8\n"
+ + " | sysTid=3541 nice=0 sched=0/0 cgrp=apps handle=1991153472\n"
+ + " | state=S schedstat=( 0 0 0 ) utm=1118 stm=145 core=0\n"
+ + " #00 pc 00000904 /system/lib/libc.so (__futex_syscall3+4294832136)\n"
+ + " #01 pc 0000eec4 /system/lib/libc.so (__pthread_cond_timedwait_relative+48)\n"
+ + " #02 pc 0000ef24 /system/lib/libc.so (__pthread_cond_timedwait+64)\n"
+ + " #03 pc 000536b7 /system/lib/libdvm.so\n"
+ + " #04 pc 00053c79 /system/lib/libdvm.so (dvmChangeStatus(Thread*, ThreadStatus)+34)\n"
+ + " #05 pc 00049507 /system/lib/libdvm.so\n"
+ + " #06 pc 0004d84b /system/lib/libdvm.so\n"
+ + " #07 pc 0003f1df /dev/ashmem/libxul.so (deleted)\n"
+ + " at org.mozilla.gecko.mozglue.GeckoLoader.nativeRun(Native Method)\n"
+ + " at org.mozilla.gecko.GeckoAppShell.runGecko(GeckoAppShell.java:384)\n"
+ + " at org.mozilla.gecko.GeckoThread.run(GeckoThread.java:177)\n"
+ + "\n"
+ + "----- end 1 -----\n"
+ + "\n"
+ + "\n"
+ + "----- pid 2 at 2013-01-25 13:27:01 -----\n"
+ + "Cmd line: system_server\n"
+ + "\n"
+ + "----- end 2 -----\n";
+
+ private boolean mDone;
+
+ private JSONObject readPingFile(final File pingFile) throws Exception {
+ final long fileSize = pingFile.length();
+ if (fileSize == 0 || fileSize > Integer.MAX_VALUE) {
+ throw new Exception("Invalid ping file size");
+ }
+ final char[] buffer = new char[(int) fileSize];
+ final FileReader reader = new FileReader(pingFile);
+ try {
+ final int readSize = reader.read(buffer);
+ if (readSize == 0 || readSize > buffer.length) {
+ throw new Exception("Invalid number of bytes read");
+ }
+ } finally {
+ reader.close();
+ }
+ return new JSONObject(new String(buffer));
+ }
+
+ public void testANRReporter() throws Exception {
+ blockForGeckoReady();
+
+ // Cannot test ANR reporter if it's disabled.
+ if (!AppConstants.MOZ_ANDROID_ANR_REPORTER) {
+ mAsserter.ok(true, "ANR reporter is disabled", null);
+ return;
+ }
+
+ // For the ANR reporter to work, we need to provide sample ANR traces to it.
+ // Therefore, we need the ANR file to exist and writable. If not, we don't
+ // have the right permissions to create the file, so we just bail.
+ final File anrFile = new File(ANR_PATH);
+ if (!anrFile.exists()) {
+ mAsserter.ok(true, "ANR file does not exist", null);
+ return;
+ }
+ if (!anrFile.canWrite()) {
+ mAsserter.ok(true, "ANR file is not writable", null);
+ return;
+ }
+
+ final FileWriter anrWriter = new FileWriter(anrFile);
+ try {
+ anrWriter.write(SAMPLE_ANR);
+ } finally {
+ anrWriter.close();
+ }
+
+ // Block the UI thread to simulate an ANR
+ final Runnable uiBlocker = new Runnable() {
+ @Override
+ public synchronized void run() {
+ while (!mDone) {
+ try {
+ wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ }
+ };
+ getActivity().runOnUiThread(uiBlocker);
+
+ // Make sure our initial ping directory is empty.
+ final File pingDir = new File(mProfile, PING_DIR);
+ final String[] initialFiles = pingDir.list();
+ mAsserter.ok(initialFiles == null || initialFiles.length == 0,
+ "Ping directory is empty", null);
+
+ final Intent anrIntent = new Intent(ANR_ACTION);
+ anrIntent.setPackage(AppConstants.ANDROID_PACKAGE_NAME);
+ mAsserter.is(anrIntent.getPackage(), AppConstants.ANDROID_PACKAGE_NAME,
+ "Successfully set package name");
+
+ final Context testContext = getInstrumentation().getContext();
+ mAsserter.isnot(testContext, null, "testContext should not be null");
+
+ // Trigger the ANR.
+ mAsserter.info("Triggering ANR", null);
+ testContext.sendBroadcast(anrIntent);
+
+ // ANR reporter is supposed to ignore duplicate ANRs.
+ // This will be checked later when we look for ping files.
+ mAsserter.info("Triggering second ANR", null);
+ testContext.sendBroadcast(new Intent(anrIntent));
+
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ mAsserter.info("Waiting for ping", null);
+
+ try {
+ // Sleep to allow the ANR reporter thread time to process the ANR.
+ Thread.sleep(1000);
+ } catch (final InterruptedException e) {
+ }
+
+ final File[] newFiles = pingDir.listFiles();
+ if (newFiles == null || newFiles.length == 0) {
+ // Keep waiting.
+ return false;
+ }
+ // Make sure we have a complete file. We skip assertions and catch all
+ // exceptions here because the condition may not be satisfied now but may
+ // be satisfied later. After the wait is over, we will repeat the same
+ // steps with assertions and exceptions.
+ try {
+ return readPingFile(newFiles[0]).has("slug");
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+ }, WAIT_FOR_PING_TIMEOUT);
+
+ mAsserter.ok(pingDir.exists(), "Ping directory exists", null);
+ mAsserter.ok(pingDir.isDirectory(), "Ping directory is a directory", null);
+
+ final File[] newFiles = pingDir.listFiles();
+ mAsserter.isnot(newFiles, null, "Ping directory is not empty");
+ mAsserter.is(newFiles.length, 1, "ANR reporter wrote one ping");
+ mAsserter.ok(newFiles[0].exists(), "Ping exists", null);
+ mAsserter.ok(newFiles[0].isFile(), "Ping is a file", null);
+ mAsserter.ok(newFiles[0].canRead(), "Ping is readable", null);
+ mAsserter.info("Found ping file", newFiles[0].getPath());
+
+ // Check standard properties required by Telemetry server.
+ final JSONObject pingObject = readPingFile(newFiles[0]);
+ mAsserter.ok(pingObject.has("slug"), "Ping has slug property", null);
+ mAsserter.ok(pingObject.has("reason"), "Ping has reason property", null);
+ mAsserter.ok(pingObject.has("payload"), "Ping has payload property", null);
+
+ final JSONObject pingPayload = pingObject.getJSONObject("payload");
+ mAsserter.ok(pingPayload.has("ver"), "Payload has ver property", null);
+ mAsserter.ok(pingPayload.has("info"), "Payload has info property", null);
+ mAsserter.ok(pingPayload.has("androidANR"), "Payload has androidANR property", null);
+
+ final JSONObject pingInfo = pingPayload.getJSONObject("info");
+ mAsserter.ok(pingInfo.has("reason"), "Info has reason property", null);
+ mAsserter.ok(pingInfo.has("appName"), "Info has appName property", null);
+ mAsserter.ok(pingInfo.has("appUpdateChannel"), "Info has appUpdateChannel property", null);
+ mAsserter.ok(pingInfo.has("appVersion"), "Info has appVersion property", null);
+ mAsserter.ok(pingInfo.has("appBuildID"), "Info has appBuildID property", null);
+
+ // Do some profile clean up. This is not absolutely necessary because the profile
+ // is blown away after test runs anyways, so we don't check return values here.
+ for (final File ping : newFiles) {
+ ping.delete();
+ }
+ pingDir.delete();
+
+ // Unblock UI thread
+ synchronized (uiBlocker) {
+ mDone = true;
+ uiBlocker.notify();
+ }
+
+ // Clear the sample ANR
+ final FileWriter anrClearer = new FileWriter(anrFile);
+ anrClearer.close();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
new file mode 100644
index 000000000..68f3a38db
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
@@ -0,0 +1,107 @@
+/* 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 org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+/**
+ * Tests functionality related to navigating between the various about:home panels.
+ */
+public class testAboutHomePageNavigation extends UITest {
+ // TODO: Define this test dynamically by creating dynamic representations of the Page
+ // enum for both phone and tablet, then swiping through the panels. This will also
+ // benefit having a HomePager with custom panels.
+ public void testAboutHomePageNavigation() {
+ GeckoHelper.blockForDelayedStartup();
+
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
+
+ // Ideally these helpers would just be their own tests. However, by keeping this within
+ // one method, we're saving test setUp and tearDown resources.
+ if (DeviceHelper.isTablet()) {
+ helperTestTablet();
+ } else {
+ helperTestPhone();
+ }
+ }
+
+ private void helperTestTablet() {
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ // Edge case.
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+
+ // Edge case.
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+ }
+
+ private void helperTestPhone() {
+ // Edge case.
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ // Edge case.
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+ }
+
+ // TODO: bug 943706 - reimplement this old test code.
+ /*
+ // Removed by Bug 896576 - [fig] Remove [getAllPagesList] from BaseTest
+ // ListView list = getAllPagesList("about:firefox");
+
+ // Test normal sliding of the list left and right
+ ViewPager pager = (ViewPager)mSolo.getView(ViewPager.class, 0);
+ mAsserter.is(pager.getCurrentItem(), 0, "All pages is selected");
+
+ int width = mDriver.getGeckoWidth() / 2;
+ int y = mDriver.getGeckoHeight() / 2;
+ mActions.drag(width, 0, y, y);
+ mAsserter.is(pager.getCurrentItem(), 1, "Bookmarks page is selected");
+
+ mActions.drag(0, width, y, y);
+ mAsserter.is(pager.getCurrentItem(), 0, "All pages is selected");
+
+ // Test tapping on the tab strip changes tabs
+ TabWidget tabwidget = (TabWidget)mSolo.getView(TabWidget.class, 0);
+ mSolo.clickOnView(tabwidget.getChildAt(1));
+ mAsserter.is(pager.getCurrentItem(), 1, "Clicking on tab selected bookmarks page");
+
+ // Test typing in the awesomebar changes tabs and prevents panning
+ mSolo.typeText(0, "woot");
+ mAsserter.is(pager.getCurrentItem(), 0, "Searching switched to all pages tab");
+ mSolo.scrollToSide(Solo.LEFT);
+ mAsserter.is(pager.getCurrentItem(), 0, "Dragging left is not allowed when searching");
+
+ mSolo.scrollToSide(Solo.RIGHT);
+ mAsserter.is(pager.getCurrentItem(), 0, "Dragging right is not allowed when searching");
+
+ mSolo.goBack();
+ */
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java
new file mode 100644
index 000000000..3be6ed53f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java
@@ -0,0 +1,57 @@
+/* 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 org.mozilla.gecko.home.HomeConfig;
+import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * Tests the visibility of about:home after various interactions with the browser.
+ */
+public class testAboutHomeVisibility extends UITest {
+ public void testAboutHomeVisibility() {
+ GeckoHelper.blockForReady();
+
+ // Check initial state on about:home.
+ mToolbar.assertTitle(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ // Go to blank 01.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ mAboutHome.assertNotVisible();
+
+ // Go to blank 02.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ mAboutHome.assertNotVisible();
+
+ // Enter editing mode, where the about:home UI should be visible.
+ mToolbar.enterEditingMode();
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ // Dismiss editing mode, where the about:home UI should be gone.
+ mToolbar.dismissEditingMode();
+ mAboutHome.assertNotVisible();
+
+ // Loading about:home should show about:home again.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ mToolbar.assertTitle(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ // We can navigate to about:home panels by panel UUID.
+ mAboutHome.navigateToBuiltinPanelType(PanelType.BOOKMARKS)
+ .assertVisible()
+ .assertCurrentPanel(PanelType.BOOKMARKS);
+ mAboutHome.navigateToBuiltinPanelType(PanelType.COMBINED_HISTORY)
+ .assertVisible()
+ .assertCurrentPanel(PanelType.COMBINED_HISTORY);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
new file mode 100644
index 000000000..6a00acd96
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
@@ -0,0 +1,47 @@
+/* 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 org.mozilla.gecko.Actions;
+
+/* Tests related to the about: page:
+ * - check that about: loads from the URL bar
+ * - check that about: loads from Settings/About...
+ */
+public class testAboutPage extends PixelTest {
+
+ public void testAboutPage() {
+ blockForGeckoReady();
+
+ // Load the about: page and verify its title.
+ String url = mStringHelper.ABOUT_SCHEME;
+ loadAndPaint(url);
+
+ verifyUrlInContentDescription(url);
+
+ // Open a new page to remove the about: page from the current tab.
+ url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ loadUrlAndWait(url);
+
+ // At this point the page title should have been set.
+ verifyUrlInContentDescription(url);
+
+ // Set up listeners to catch the page load we're about to do.
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+
+ selectSettingsItem(mStringHelper.MOZILLA_SECTION_LABEL, mStringHelper.ABOUT_LABEL);
+
+ // Wait for the new tab and page to load
+ tabEventExpecter.blockForEvent();
+ contentEventExpecter.blockForEvent();
+
+ tabEventExpecter.unregisterListener();
+ contentEventExpecter.unregisterListener();
+
+ // Make sure the about: page was loaded.
+ verifyUrlInContentDescription(mStringHelper.ABOUT_SCHEME);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
new file mode 100644
index 000000000..d064eb1dd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
@@ -0,0 +1,76 @@
+/**
+ * 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+public class testAccessibleCarets extends JavascriptTest {
+ private static final String LOGTAG = "testAccessibleCarets";
+ private static final String TAB_CHANGE_EVENT = "testAccessibleCarets:TabChange";
+
+ private final TabsListener tabsListener;
+
+
+ public testAccessibleCarets() {
+ super("testAccessibleCarets.js");
+
+ tabsListener = new TabsListener();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ Tabs.registerOnTabsChangedListener(tabsListener);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ Tabs.unregisterOnTabsChangedListener(tabsListener);
+
+ super.tearDown();
+ }
+
+ @Override
+ public void testJavascript() throws Exception {
+ // This feature is currently only available in Nightly.
+ if (!AppConstants.NIGHTLY_BUILD) {
+ mAsserter.dumpLog(LOGTAG + " is disabled on non-Nightly builds: returning");
+ return;
+ }
+ super.testJavascript();
+ }
+
+ /**
+ * Observes tab change events to broadcast to the test script.
+ */
+ private class TabsListener implements Tabs.OnTabsChangedListener {
+ @Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+ switch (msg) {
+ case STOP:
+ final JSONObject args = new JSONObject();
+ try {
+ args.put("tabId", tab.getId());
+ args.put("event", msg.toString());
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error building JSON arguments for " + TAB_CHANGE_EVENT, e);
+ return;
+ }
+ mActions.sendGeckoEvent(TAB_CHANGE_EVENT, args.toString());
+ break;
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
new file mode 100644
index 000000000..b4b06a236
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
@@ -0,0 +1,94 @@
+/* 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 android.support.design.widget.NavigationView;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.home.activitystream.ActivityStream;
+import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
+
+/**
+ * This test is unfortunately closely coupled to the current implementation, however it is still
+ * useful in that it tests the bookmark/history state specific menu items for correctness.
+ */
+public class testActivityStreamContextMenu extends BaseTest {
+ public void testActivityStreamContextMenu() {
+ blockForGeckoReady();
+
+ final String testURL = "http://mozilla.org";
+
+ BrowserDB db = BrowserDB.from(getActivity());
+ db.removeHistoryEntry(getActivity().getContentResolver(), testURL);
+ db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL);
+
+ testMenuForUrl(testURL, false, false);
+
+ db.addBookmark(getActivity().getContentResolver(), "foobar", testURL);
+ testMenuForUrl(testURL, true, false);
+
+ db.updateVisitedHistory(getActivity().getContentResolver(), testURL);
+ testMenuForUrl(testURL, true, true);
+
+ db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL);
+ testMenuForUrl(testURL, false, true);
+ }
+
+ /**
+ * Test that the menu shows the expected menu items for a given URL, and that these items have
+ * the correct state.
+ */
+ private void testMenuForUrl(final String url, final boolean isBookmarked, final boolean isVisited) {
+ final View anchor = new View(getActivity());
+
+ final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, null, null, 100, 100);
+
+ final int expectedBookmarkString;
+ if (isBookmarked) {
+ expectedBookmarkString = R.string.bookmark_remove;
+ } else {
+ expectedBookmarkString = R.string.bookmark;
+ }
+
+ final MenuItem bookmarkItem = menu.getItemByID(R.id.bookmark);
+ assertMenuItemHasString(bookmarkItem, expectedBookmarkString);
+
+ final MenuItem deleteItem = menu.getItemByID(R.id.delete);
+ assertMenuItemIsVisible(deleteItem, isVisited);
+
+ menu.dismiss();
+ }
+
+ private void assertMenuItemIsVisible(final MenuItem item, final boolean shouldBeVisible) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (item.isVisible() == shouldBeVisible);
+ }
+ }, 5000);
+
+ mAsserter.is(item.isVisible(), shouldBeVisible, "menu item \"" + item.getTitle() + "\" should be visible");
+ }
+
+ private void assertMenuItemHasString(final MenuItem item, final int stringID) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return item.isEnabled();
+ }
+ }, 5000);
+
+ final String expectedTitle = getActivity().getResources().getString(stringID);
+ mAsserter.is(item.getTitle(), expectedTitle, "Title does not match expected title");
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java
new file mode 100644
index 000000000..44bd1f903
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java
@@ -0,0 +1,172 @@
+/* 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.util.ArrayList;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.SearchEngineBar;
+import org.mozilla.gecko.R;
+
+import android.widget.ImageView;
+import android.widget.ListView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test adding a search engine from an input field context menu.
+ * 1. Get the number of existing search engines from the SearchEngine:Data event and as displayed in about:home.
+ * 2. Load a page with a text field, open the context menu and add a search engine from the page.
+ * 3. Get the number of search engines after adding the new one and verify it has increased by 1.
+ */
+public class testAddSearchEngine extends AboutHomeTest {
+ private final int MAX_WAIT_TEST_MS = 5000;
+ private final String SEARCH_TEXT = "Firefox for Android";
+ private final String ADD_SEARCHENGINE_OPTION_TEXT = "Add as Search Engine";
+
+ public void testAddSearchEngine() {
+ String blankPageURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String searchEngineURL = getAbsoluteUrl(mStringHelper.ROBOCOP_SEARCH_URL);
+
+ blockForGeckoReady();
+ int height = mDriver.getGeckoTop() + 150;
+ int width = mDriver.getGeckoLeft() + 150;
+
+ inputAndLoadUrl(blankPageURL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+
+ // Get the searchengine data by clicking the awesomebar - this causes Gecko to send Java the list
+ // of search engines.
+ Actions.EventExpecter searchEngineDataEventExpector = mActions.expectGeckoEvent("SearchEngines:Data");
+ focusUrlBar();
+ mActions.sendKeys(SEARCH_TEXT);
+ String eventData = searchEngineDataEventExpector.blockForEventData();
+ searchEngineDataEventExpector.unregisterListener();
+
+ ArrayList<String> searchEngines;
+ try {
+ // Parse the data to get the number of searchengines.
+ searchEngines = getSearchEnginesNames(eventData);
+ } catch (JSONException e) {
+ mAsserter.ok(false, "Fatal exception in testAddSearchEngine while decoding JSON search engine string from Gecko prior to addition of new engine.", e.toString());
+ return;
+ }
+ final int initialNumSearchEngines = searchEngines.size();
+ mAsserter.dumpLog("Search Engines list = " + searchEngines.toString());
+
+ // Verify that the number of displayed search engines is the same as the one received through the SearchEngines:Data event.
+ verifyDisplayedSearchEnginesCount(initialNumSearchEngines);
+
+ // Load the page for the search engine to add.
+ inputAndLoadUrl(searchEngineURL);
+ verifyUrlBarTitle(searchEngineURL);
+
+ // Used to long-tap on the search input box for the search engine to add.
+ getInstrumentation().waitForIdleSync();
+ mAsserter.dumpLog("Long Clicking at width = " + String.valueOf(width) + " and height = " + String.valueOf(height));
+ mSolo.clickLongOnScreen(width,height);
+
+ ImageView view = waitForViewWithDescription(ImageView.class, ADD_SEARCHENGINE_OPTION_TEXT);
+ mAsserter.isnot(view, null, "The action mode was opened");
+
+ // Add the search engine
+ mSolo.clickOnView(view);
+ waitForText("Cancel");
+ clickOnButton("OK");
+ mAsserter.ok(!mSolo.searchText(ADD_SEARCHENGINE_OPTION_TEXT), "Adding the Search Engine", "The add Search Engine pop-up has been closed");
+ waitForText(mStringHelper.ROBOCOP_SEARCH_TITLE); // Make sure the pop-up is closed and we are back at the searchengine page
+
+ // Load Robocop Blank 1 again to give the time for the searchengine to be added
+ // TODO: This is a potential source of intermittent oranges - it's a race condition!
+ loadUrl(blankPageURL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+
+ // Load search engines again and check that the quantity of engines has increased by 1.
+ searchEngineDataEventExpector = mActions.expectGeckoEvent("SearchEngines:Data");
+ focusUrlBar();
+ mActions.sendKeys(SEARCH_TEXT);
+ eventData = searchEngineDataEventExpector.blockForEventData();
+
+ try {
+ // Parse the data to get the number of searchengines
+ searchEngines = getSearchEnginesNames(eventData);
+ } catch (JSONException e) {
+ mAsserter.ok(false, "Fatal exception in testAddSearchEngine while decoding JSON search engine string from Gecko after adding of new engine.", e.toString());
+ return;
+ }
+
+ mAsserter.dumpLog("Search Engines list = " + searchEngines.toString());
+ mAsserter.is(searchEngines.size(), initialNumSearchEngines + 1, "Checking the number of Search Engines has increased");
+
+ // Verify that the number of displayed searchengines is the same as the one received through the SearchEngines:Data event.
+ verifyDisplayedSearchEnginesCount(initialNumSearchEngines + 1);
+ searchEngineDataEventExpector.unregisterListener();
+
+ // Verify that the search plugin XML file for the new engine ended up where we expected it to.
+ // This file name is created in nsSearchService.js based on the name of the new engine.
+ final File f = GeckoProfile.get(getActivity()).getFile("searchplugins/robocop-search-engine.xml");
+ mAsserter.ok(f.exists(), "Checking that new search plugin file exists", "");
+ }
+
+ /**
+ * Helper method to decode a list of search engine names from the provided search engine information
+ * JSON string sent from Gecko.
+ * @param searchEngineData The JSON string representing the search engine array to process
+ * @return An ArrayList<String> containing the names of all the search engines represented in
+ * the provided JSON message.
+ * @throws JSONException In the event that the JSON provided cannot be decoded.
+ */
+ public ArrayList<String> getSearchEnginesNames(String searchEngineData) throws JSONException {
+ JSONObject data = new JSONObject(searchEngineData);
+ JSONArray engines = data.getJSONArray("searchEngines");
+
+ ArrayList<String> searchEngineNames = new ArrayList<String>();
+ for (int i = 0; i < engines.length(); i++) {
+ JSONObject engineJSON = engines.getJSONObject(i);
+ searchEngineNames.add(engineJSON.getString("name"));
+ }
+ return searchEngineNames;
+ }
+
+ /**
+ * Method to verify that the displayed number of search engines matches the expected number.
+ * @param expectedCount The expected number of search engines.
+ */
+ public void verifyDisplayedSearchEnginesCount(final int expectedCount) {
+ mSolo.clearEditText(0);
+ mActions.sendKeys(SEARCH_TEXT);
+ boolean correctNumSearchEnginesDisplayed = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ ListView searchResultList = findListViewWithTag(HomePager.LIST_TAG_BROWSER_SEARCH);
+ if (searchResultList == null || searchResultList.getAdapter() == null) {
+ return false;
+ }
+
+ SearchEngineBar searchEngineBar = (SearchEngineBar) mSolo.getView(R.id.search_engine_bar);
+ if (searchEngineBar == null || searchEngineBar.getAdapter() == null) {
+ return false;
+ }
+
+ final int actualCount = searchResultList.getAdapter().getCount()
+ + searchEngineBar.getAdapter().getItemCount()
+ - 1; // Subtract one for the search engine bar label (Bug 1172071)
+
+ return (actualCount == expectedCount);
+ }
+ }, MAX_WAIT_TEST_MS);
+
+ // Exit about:home
+ mSolo.goBack();
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+ mAsserter.ok(correctNumSearchEnginesDisplayed, expectedCount + " Search Engines should be displayed" , "The correct number of Search Engines has been displayed");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
new file mode 100644
index 000000000..4256d93c4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
@@ -0,0 +1,79 @@
+/* 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 org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+
+import android.util.DisplayMetrics;
+
+/**
+ * This test performs the following steps to check the behavior of the Add-on Manager:
+ *
+ * 1) Open the Add-on Manager from the Add-ons menu item, and then close it.
+ * 2) Open the Add-on Manager by visiting about:addons in the URL bar.
+ * 3) Open a new tab, select the Add-ons menu item, then verify that the existing
+ * Add-on Manager tab was selected, instead of opening a new tab.
+ */
+public class testAddonManager extends PixelTest {
+ public void testAddonManager() {
+ Actions.EventExpecter tabEventExpecter;
+ Actions.EventExpecter contentEventExpecter;
+ final String aboutAddonsURL = mStringHelper.ABOUT_ADDONS_URL;
+
+ blockForGeckoReady();
+
+ // Use the menu to open the Addon Manger
+ selectMenuItem(mStringHelper.ADDONS_LABEL);
+
+ // Set up listeners to catch the page load we're about to do
+ tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+
+ // Wait for the new tab and page to load
+ tabEventExpecter.blockForEvent();
+ contentEventExpecter.blockForEvent();
+
+ tabEventExpecter.unregisterListener();
+ contentEventExpecter.unregisterListener();
+
+ // Verify the url
+ verifyUrlBarTitle(aboutAddonsURL);
+
+ // Close the Add-on Manager
+ mSolo.goBack();
+
+ // Load the about:addons page and verify it was loaded
+ loadAndPaint(aboutAddonsURL);
+ verifyUrlBarTitle(aboutAddonsURL);
+
+ // Setup wait for tab to spawn and load
+ tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+
+ // Open a new tab
+ final String blankURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ addTab(blankURL);
+
+ // Wait for the new tab and page to load
+ tabEventExpecter.blockForEvent();
+ contentEventExpecter.blockForEvent();
+
+ tabEventExpecter.unregisterListener();
+ contentEventExpecter.unregisterListener();
+
+ // Verify tab count has increased
+ verifyTabCount(2);
+
+ // Verify the page was opened
+ verifyUrlBarTitle(blankURL);
+
+ // Addons Manager is not opened 2 separate times when opened from the menu
+ selectMenuItem(mStringHelper.ADDONS_LABEL);
+
+ // Verify tab count not increased
+ verifyTabCount(2);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java
new file mode 100644
index 000000000..13f7f817a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java
@@ -0,0 +1,39 @@
+/* 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 org.json.JSONObject;
+import org.mozilla.gecko.PaintedSurface;
+
+import android.os.Build;
+
+/**
+ * Tests that Flash is working
+ * - loads a page containing a Flash plugin
+ * - verifies it rendered properly
+ */
+public class testAdobeFlash extends PixelTest {
+ public void testLoad() {
+ // This test only works on ICS and higher
+ if (Build.VERSION.SDK_INT < 15) {
+ blockForGeckoReady();
+ return;
+ }
+
+ // Enable plugins
+ setPreferenceAndWaitForChange("plugin.enable", "1");
+
+ blockForGeckoReady();
+
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_ADOBE_FLASH_URL);
+ PaintedSurface painted = loadAndGetPainted(url);
+
+ mAsserter.ispixel(painted.getPixelAt(0, 0), 0, 0xff, 0, "Pixel at 0, 0");
+ mAsserter.ispixel(painted.getPixelAt(50, 50), 0, 0xff, 0, "Pixel at 50, 50");
+ mAsserter.ispixel(painted.getPixelAt(101, 0), 0xff, 0xff, 0xff, "Pixel at 101, 0");
+ mAsserter.ispixel(painted.getPixelAt(0, 101), 0xff, 0xff, 0xff, "Pixel at 0, 101");
+
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java
new file mode 100644
index 000000000..69efb4dec
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java
@@ -0,0 +1,77 @@
+/* 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 org.json.JSONObject;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.tests.components.AppMenuComponent;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Set of tests to test UI App menu and submenus the user interact with.
+ */
+public class testAppMenuPathways extends UITest {
+
+ /**
+ * Robocop supports only a single test function per test class. Therefore, we
+ * have a single top-level test function that dispatches to sub-tests.
+ */
+ public void testAppMenuPathways() {
+ GeckoHelper.blockForReady();
+
+ _testHardwareMenuKeyOpenClose();
+ _testSaveAsPDFPathway();
+ }
+
+ public void _testHardwareMenuKeyOpenClose() {
+ mAppMenu.assertMenuIsNotOpen();
+
+ mSolo.sendKey(Solo.MENU);
+ mAppMenu.waitForMenuOpen();
+ mAppMenu.assertMenuIsOpen();
+
+ mSolo.sendKey(Solo.MENU);
+ mAppMenu.waitForMenuClose();
+ mAppMenu.assertMenuIsNotOpen();
+ }
+
+ public void _testSaveAsPDFPathway() {
+ // Page menu should be disabled in about:home.
+ mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
+
+ // Generate a mock Content:LocationChange message with video mime-type for the current tab (tabId = 0).
+ final JSONObject message = new JSONObject();
+ try {
+ message.put("contentType", "video/webm");
+ message.put("baseDomain", "webmfiles.org");
+ message.put("type", "Content:LocationChange");
+ message.put("sameDocument", false);
+ message.put("userRequested", "");
+ message.put("uri", getAbsoluteIpUrl("/big-buck-bunny_trailer.webm"));
+ message.put("tabID", 0);
+ } catch (Exception ex) {
+ mAsserter.ok(false, "exception in testSaveAsPDFPathway", ex.toString());
+ }
+
+ // Mock video playback with the generated message and Content:LocationChange event.
+ Tabs.getInstance().handleMessage("Content:LocationChange", message);
+
+ // Save as pdf menu is disabled while playing video.
+ mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
+
+ // The above mock video playback test changes Java state, but not the associated JS state.
+ // Navigate to a new page so that the Java state is cleared.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ // Test save as pdf functionality.
+ // The following call doesn't wait for the resulting pdf but checks that no exception are thrown.
+ // NOTE: save as pdf functionality must be done at the end as it is slow and cause other test operations to fail.
+ mAppMenu.pressMenuItem(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java
new file mode 100644
index 000000000..72bf62e04
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java
@@ -0,0 +1,58 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+/**
+ * Basic test for axis locking behaviour.
+ * - Load page and verify it draws
+ * - Drag page upwards 100 pixels at a 5-degree angle off the vertical axis
+ * - Verify that the 5-degree angle was thrown out and it dragged vertically
+ * - Drag page upwards at a 45-degree angle
+ * - Verify that the 45-degree angle was not thrown out and it dragged diagonally
+ */
+public class testAxisLocking extends PixelTest {
+ public void testAxisLocking() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+
+ blockForGeckoReady();
+
+ // load page and check we're at 0,0
+ loadAndVerifyBoxes(url);
+
+ // drag page upwards by 100 pixels with a slight angle. verify that
+ // axis locking prevents any horizontal scrolling
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ meh.dragSync(20, 150, 10, 50);
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 100);
+ // since checkScrollWithBoxes only checks 4 points, it may not pick up a
+ // sub-100 pixel horizontal shift. so we check another point manually to make sure.
+ int[] color = getBoxColorAt(0, 100);
+ mAsserter.ispixel(painted.getPixelAt(99, 0), color[0], color[1], color[2], "Pixel at 99, 0 indicates no horizontal scroll");
+
+ // now drag at a 45-degree angle to ensure we break the axis lock, and
+ // verify that we have both horizontal and vertical scrolling
+ paintExpecter = mActions.expectPaint();
+ meh.dragSync(150, 150, 50, 50);
+ } finally {
+ painted.close();
+ }
+
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 100, 200);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java
new file mode 100644
index 000000000..b391f7920
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java
@@ -0,0 +1,47 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+import android.view.View;
+
+/**
+ * Tests that verify the behavior of back button in edit mode.
+ */
+public class testBackButtonInEditMode extends UITest {
+ public void testBackButtonInEditMode() {
+ GeckoHelper.blockForReady();
+
+ // Verify back button behavior for edit mode.
+ mToolbar.enterEditingMode()
+ .assertIsUrlEditTextSelected();
+ checkBackPressInEditMode();
+ checkExitUsingBackButton();
+
+ // Verify back button behavior in edit mode after input.
+ mToolbar.enterEditingMode()
+ .enterUrl("dummy")
+ .assertIsUrlEditTextSelected();
+ checkBackPressInEditMode();
+ checkExitUsingBackButton();
+
+ // Verify the swipe behavior in edit mode.
+ mToolbar.enterEditingMode()
+ .assertIsUrlEditTextSelected();
+ mAboutHome.swipeToPanelOnLeft();
+ mToolbar.assertIsUrlEditTextNotSelected()
+ .assertIsEditing();
+ checkExitUsingBackButton();
+ }
+
+ private void checkBackPressInEditMode() {
+ // Press back button and verify URLEditText is not selected.
+ getSolo().goBack();
+ mToolbar.assertIsUrlEditTextNotSelected()
+ .assertIsEditing();
+ }
+
+ private void checkExitUsingBackButton() {
+ getSolo().goBack();
+ mToolbar.assertIsNotEditing();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java
new file mode 100644
index 000000000..041b76e2f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java
@@ -0,0 +1,72 @@
+/* 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 com.robotium.solo.Condition;
+
+public class testBookmark extends AboutHomeTest {
+ private static String BOOKMARK_URL;
+ private static final int WAIT_FOR_BOOKMARKED_TIMEOUT = 10000;
+
+ public void testBookmark() {
+ BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ runAboutHomeTest();
+ runMenuTest();
+ }
+
+ public void runMenuTest() {
+ mAsserter.is(mDatabaseHelper.isBookmark(BOOKMARK_URL), false, "Page is not bookmarked initially");
+ setUpBookmark(); // loads the page, taps the star button, and waits for the "Bookmark Added" message
+ waitForBookmarked(true);
+
+ cleanUpBookmark(); // loads the page, taps the star button, and waits for the "Bookmark Removed" message
+ waitForBookmarked(false);
+ }
+
+ public void runAboutHomeTest() {
+ blockForGeckoReady();
+ for (String url : mStringHelper.DEFAULT_BOOKMARKS_URLS) {
+ mAsserter.ok(mDatabaseHelper.isBookmark(url), "Checking that " + url + " is bookmarked by default", url + " is bookmarked");
+ }
+
+ mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
+ waitForBookmarked(true);
+
+ isBookmarkDisplayed(BOOKMARK_URL);
+ loadBookmark(BOOKMARK_URL);
+ verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ mDatabaseHelper.deleteBookmark(BOOKMARK_URL);
+ waitForBookmarked(false);
+ }
+
+ private void waitForBookmarked(final boolean isBookmarked) {
+ boolean bookmarked = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (isBookmarked) ?
+ mDatabaseHelper.isBookmark(BOOKMARK_URL) :
+ !mDatabaseHelper.isBookmark(BOOKMARK_URL);
+ }
+ }, WAIT_FOR_BOOKMARKED_TIMEOUT);
+ mAsserter.is(bookmarked, true, BOOKMARK_URL + " was " + (isBookmarked ? "added as a bookmark" : "removed from bookmarks"));
+ }
+
+ private void setUpBookmark() {
+ // Bookmark a page for the test
+ loadUrl(BOOKMARK_URL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+ toggleBookmark();
+ mAsserter.is(waitForText(mStringHelper.BOOKMARK_ADDED_LABEL), true, "bookmark added successfully");
+ }
+
+ private void cleanUpBookmark() {
+ // Go back to the page we bookmarked
+ loadUrl(BOOKMARK_URL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+ toggleBookmark();
+ mAsserter.is(waitForText(mStringHelper.BOOKMARK_REMOVED_LABEL), true, "bookmark removed successfully");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java
new file mode 100644
index 000000000..6205337ea
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java
@@ -0,0 +1,169 @@
+/* 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 org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.sync.Utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+public class testBookmarkFolders extends AboutHomeTest {
+ private static String DESKTOP_BOOKMARK_URL;
+
+ public void testBookmarkFolders() {
+ DESKTOP_BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ setUpDesktopBookmarks();
+ checkBookmarkList();
+ }
+
+ private void checkBookmarkList() {
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+ waitForText(mStringHelper.DESKTOP_FOLDER_LABEL);
+ clickOnBookmarkFolder(mStringHelper.DESKTOP_FOLDER_LABEL);
+ waitForText(mStringHelper.TOOLBAR_FOLDER_LABEL);
+
+ // Verify the number of folders displayed in the Desktop Bookmarks folder is correct
+ ListView desktopFolderContent = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ ListAdapter adapter = desktopFolderContent.getAdapter();
+
+ // Three folders and "Up to Bookmarks".
+ mAsserter.is(adapter.getCount(), 4, "Checking that the correct number of folders is displayed in the Desktop Bookmarks folder");
+
+ clickOnBookmarkFolder(mStringHelper.TOOLBAR_FOLDER_LABEL);
+
+ // Go up in the bookmark folder hierarchy
+ clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.DESKTOP_FOLDER_LABEL));
+ mAsserter.ok(waitForText(mStringHelper.BOOKMARKS_MENU_FOLDER_LABEL), "Going up in the folder hierarchy", "We are back in the Desktop Bookmarks folder");
+
+ clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.BOOKMARKS_ROOT_LABEL));
+ mAsserter.ok(waitForText(mStringHelper.DESKTOP_FOLDER_LABEL), "Going up in the folder hierarchy", "We are back in the main Bookmarks List View");
+
+ clickOnBookmarkFolder(mStringHelper.DESKTOP_FOLDER_LABEL);
+ clickOnBookmarkFolder(mStringHelper.TOOLBAR_FOLDER_LABEL);
+ isBookmarkDisplayed(DESKTOP_BOOKMARK_URL);
+
+ // Open the bookmark from a bookmark folder hierarchy
+ loadBookmark(DESKTOP_BOOKMARK_URL);
+ verifyUrlBarTitle(DESKTOP_BOOKMARK_URL);
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+
+ // Check that folders don't have a context menu
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View desktopFolder = getBookmarkFolderView(mStringHelper.DESKTOP_FOLDER_LABEL);
+ if (desktopFolder == null) {
+ return false;
+ }
+ mSolo.clickLongOnView(desktopFolder);
+ return true; }
+ }, MAX_WAIT_MS);
+
+ mAsserter.ok(success, "Trying to long click on the Desktop Bookmarks","Desktop Bookmarks folder could not be long clicked");
+
+ final String contextMenuString = mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0];
+ mAsserter.ok(!waitForText(contextMenuString), "Folders do not have context menus", "The context menu was not opened");
+
+ // Even if no context menu is opened long clicking a folder still opens it. We need to close it.
+ clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.BOOKMARKS_ROOT_LABEL));
+ }
+
+ private void clickOnBookmarkFolder(final String folderName) {
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View bookmarksFolder = getBookmarkFolderView(folderName);
+ if (bookmarksFolder == null) {
+ return false;
+ }
+ mSolo.waitForView(bookmarksFolder);
+ mSolo.clickOnView(bookmarksFolder);
+ return true;
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success, "Trying to click on the " + folderName + " folder","The " + folderName + " folder was clicked");
+ }
+
+ private View getBookmarkFolderView(String folderName) {
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+ mSolo.hideSoftKeyboard();
+ getInstrumentation().waitForIdleSync();
+
+ ListView bookmarksTabList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ if (!waitForNonEmptyListToLoad(bookmarksTabList)) {
+ return null;
+ }
+
+ ListAdapter adapter = bookmarksTabList.getAdapter();
+ if (adapter == null) {
+ return null;
+ }
+
+ for (int i = 0; i < adapter.getCount(); i++ ) {
+ View bookmarkView = bookmarksTabList.getChildAt(i);
+ if (bookmarkView instanceof TextView) {
+ TextView folderTextView = (TextView) bookmarkView;
+ if (folderTextView.getText().equals(folderName)) {
+ return bookmarkView;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // Add a bookmark in the Desktop folder so we can check the folder navigation in the bookmarks page
+ private void setUpDesktopBookmarks() {
+ blockForGeckoReady();
+
+ // Get the folder id of the mStringHelper.DESKTOP_FOLDER_LABEL folder
+ Long desktopFolderId = mDatabaseHelper.getFolderIdFromGuid("toolbar");
+
+ // Generate a Guid for the bookmark
+ final String generatedGuid = Utils.generateGuid();
+ mAsserter.ok((generatedGuid != null), "Generating a random Guid for the bookmark", "We could not generate a Guid for the bookmark");
+
+ // Insert the bookmark
+ ContentResolver resolver = getActivity().getContentResolver();
+ Uri bookmarksUri = mDatabaseHelper.buildUri(DatabaseHelper.BrowserDataType.BOOKMARKS);
+
+ long now = System.currentTimeMillis();
+ ContentValues values = new ContentValues();
+ values.put("title", mStringHelper.ROBOCOP_BLANK_PAGE_02_TITLE);
+ values.put("url", DESKTOP_BOOKMARK_URL);
+ values.put("parent", desktopFolderId);
+ values.put("modified", now);
+ values.put("type", 1);
+ values.put("guid", generatedGuid);
+ values.put("position", 10);
+ values.put("created", now);
+
+ int updated = resolver.update(bookmarksUri,
+ values,
+ "url = ?",
+ new String[] { DESKTOP_BOOKMARK_URL });
+ if (updated == 0) {
+ Uri uri = resolver.insert(bookmarksUri, values);
+ mAsserter.ok(true, "Inserted at: ", uri.toString());
+ } else {
+ mAsserter.ok(false, "Failed to insert the Desktop bookmark", "Something went wrong");
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDatabaseHelper.deleteBookmark(DESKTOP_BOOKMARK_URL);
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java
new file mode 100644
index 000000000..363954bfa
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java
@@ -0,0 +1,28 @@
+/* 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;
+
+
+public class testBookmarkKeyword extends AboutHomeTest {
+ public void testBookmarkKeyword() {
+ blockForGeckoReady();
+
+ final String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ final String keyword = "testkeyword";
+
+ // Add a bookmark, and update it to have a keyword.
+ mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, url);
+ mDatabaseHelper.updateBookmark(url, mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, keyword);
+
+ // Enter the keyword in the urlbar.
+ inputAndLoadUrl(keyword);
+
+ // Make sure the title of the page appeared.
+ verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ // Delete the bookmark to clean up.
+ mDatabaseHelper.deleteBookmark(url);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java
new file mode 100644
index 000000000..4ae57104c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java
@@ -0,0 +1,46 @@
+/* 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 org.mozilla.gecko.Actions;
+
+import com.robotium.solo.Condition;
+
+
+public class testBookmarklets extends BaseTest {
+ public void testBookmarklets() {
+ final String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ final String title = "alertBookmarklet";
+ final String js = "javascript:alert(12 + 10)";
+ final String expected = "22";
+ boolean alerted;
+
+ blockForGeckoReady();
+
+ // Load a standard page so bookmarklets work
+ loadUrlAndWait(url);
+
+ // Verify that user-entered bookmarklets do *not* work
+ enterUrl(js);
+ mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
+ alerted = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return mSolo.searchButton("OK", true) || mSolo.searchText(expected, true);
+ }
+ }, 3000);
+ mAsserter.is(alerted, false, "Alert was not shown for user-entered bookmarklet");
+
+ // Verify that non-user-entered bookmarklets do work
+ loadUrl(js);
+ alerted = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return mSolo.searchButton("OK", true) && mSolo.searchText(expected, true);
+ }
+ }, 10000);
+ mAsserter.is(alerted, true, "Alert was shown for bookmarklet");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
new file mode 100644
index 000000000..a7e9505da
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
@@ -0,0 +1,174 @@
+/* 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 org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.StringUtils;
+
+public class testBookmarksPanel extends AboutHomeTest {
+ public void testBookmarksPanel() {
+ final String BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ JSONObject data = null;
+
+ // Make sure our default bookmarks are loaded.
+ // Technically this will race with the check below.
+ initializeProfile();
+
+ // Add a mobile bookmark.
+ mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
+
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+
+ // Check that the default bookmarks are displayed.
+ // We need to wait for the distribution to have been processed
+ // before this will succeed.
+ for (String url : mStringHelper.DEFAULT_BOOKMARKS_URLS) {
+ isBookmarkDisplayed(url);
+ }
+
+ assertAllContextMenuOptionsArePresent(mStringHelper.DEFAULT_BOOKMARKS_URLS[1],
+ mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
+
+ openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
+
+ // Test that "Open in New Tab" works
+ final Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter);
+ final int tabCountInt = Integer.parseInt(tabCount.getText());
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0]);
+ try {
+ data = new JSONObject(tabEventExpecter.blockForEventData());
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting event data", e.toString());
+ }
+ tabEventExpecter.unregisterListener();
+ mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed");
+ // extra check here on the Tab:Added message to be sure the right tab opened
+ int tabID = 0;
+ try {
+ mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
+ tabID = data.getInt("tabID");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception accessing event data", e.toString());
+ }
+ // close tab so about:firefox can be selected again
+ closeTab(tabID);
+
+ // Test that "Open in Private Tab" works
+ openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
+ tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[1]);
+ try {
+ data = new JSONObject(tabEventExpecter.blockForEventData());
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting event data", e.toString());
+ }
+ tabEventExpecter.unregisterListener();
+ mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed");
+ // extra check here on the Tab:Added message to be sure the right tab opened, again
+ try {
+ mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception accessing event data", e.toString());
+ }
+
+ // Test that "Edit" works
+ String[] editedBookmarkValues = new String[] { "New bookmark title", "www.NewBookmark.url", "newBookmarkKeyword" };
+ editBookmark(BOOKMARK_URL, editedBookmarkValues);
+ checkBookmarkEdit(editedBookmarkValues[1], editedBookmarkValues);
+
+ // Test that "Remove" works
+ openBookmarkContextMenu(editedBookmarkValues[1]);
+ mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[5]);
+ waitForText(mStringHelper.BOOKMARK_REMOVED_LABEL);
+ mAsserter.ok(!mDatabaseHelper.isBookmark(editedBookmarkValues[1]), "Checking that the bookmark was removed", "The bookmark was removed");
+ }
+
+ /**
+ * Asserts that all context menu items are present on the given links. For one link,
+ * the context menu is expected to not have the "Share" context menu item.
+ *
+ * @param shareableURL A URL that is expected to have the "Share" context menu item
+ * @param nonShareableURL A URL that is expected not to have the "Share" context menu item.
+ */
+ private void assertAllContextMenuOptionsArePresent(final String shareableURL,
+ final String nonShareableURL) {
+ mAsserter.ok(StringUtils.isShareableUrl(shareableURL), "Ensuring url is shareable", "");
+ mAsserter.ok(!StringUtils.isShareableUrl(nonShareableURL), "Ensuring url is not shareable", "");
+
+ openBookmarkContextMenu(shareableURL);
+ for (String contextMenuOption : mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS) {
+ mAsserter.ok(mSolo.searchText(contextMenuOption),
+ "Checking that the context menu option is present",
+ contextMenuOption + " is present");
+ }
+
+ // Close the menu.
+ mSolo.goBack();
+
+ openBookmarkContextMenu(nonShareableURL);
+ for (String contextMenuOption : mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS) {
+ // This link is not shareable: skip the "Share" option.
+ if ("Share".equals(contextMenuOption)) {
+ continue;
+ }
+
+ mAsserter.ok(mSolo.searchText(contextMenuOption),
+ "Checking that the context menu option is present",
+ contextMenuOption + " is present");
+ }
+
+ // The use of Solo.searchText is potentially fragile as It will only
+ // scroll the most recently drawn view. Works fine for now though.
+ mAsserter.ok(!mSolo.searchText("Share"),
+ "Checking that the Share option is not present",
+ "Share option is not present");
+
+ // Close the menu.
+ mSolo.goBack();
+ }
+
+ /**
+ * @param bookmarkUrl URL of the bookmark to edit
+ * @param values String array with the new values for all fields
+ */
+ private void editBookmark(String bookmarkUrl, String[] values) {
+ openBookmarkContextMenu(bookmarkUrl);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_EDIT);
+ waitForText(mStringHelper.EDIT_BOOKMARK);
+
+ // Update the fields with the new values
+ for (int i = 0; i < values.length; i++) {
+ mSolo.clearEditText(i);
+ mSolo.clickOnEditText(i);
+ mActions.sendKeys(values[i]);
+ }
+
+ mSolo.clickOnButton(mStringHelper.OK);
+ waitForText(mStringHelper.BOOKMARK_UPDATED_LABEL);
+ }
+
+ /**
+ * @param bookmarkUrl String with the original url
+ * @param values String array with the new values for all fields
+ */
+ private void checkBookmarkEdit(String bookmarkUrl, String[] values) {
+ openBookmarkContextMenu(bookmarkUrl);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_EDIT);
+ waitForText(mStringHelper.EDIT_BOOKMARK);
+
+ // Check the values of the fields
+ for (String value : values) {
+ mAsserter.ok(mSolo.searchText(value), "Checking that the value is correct", "The value = " + value + " is correct");
+ }
+
+ mSolo.clickOnButton("Cancel");
+ waitForText(mStringHelper.BOOKMARKS_LABEL);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java
new file mode 100644
index 000000000..eec5c4b33
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java
@@ -0,0 +1,150 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import org.mozilla.gecko.db.BrowserDatabaseHelper;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit.
+/**
+ * This test runs upgrades for the databases in (robocop-assets)/browser_db_upgrade. Currently,
+ * (robocop-assets)=mobile/android/tests/browser/robocop/assets/.
+ *
+ * It copies the old database from the robocop assets directory into a temporary file and opens a SQLiteHelper
+ * on the database to verify it gets upgraded to the correct version. If there is an issue with the upgrade,
+ * generally a SQLiteException will be thrown and the test will fail. For example:
+ * android.database.sqlite.SQLiteException: duplicate column name: calculated_pages_times_rating (code 1): , while compiling: ALTER TABLE book_information ADD COLUMN calculated_pages_times_rating INTEGER;
+ *
+ * Alternative upgrade tests:
+ * * Robolectric 2.3+ uses a real SQLite database implementation so we could run our upgrades there. However, the
+ * SQLite implementation may not be the same as we run on Android. Benefits: speed & we don't need the application to
+ * run (and thus a valid DB of the latest version) to run these tests.
+ * * We could copy the current database creation code into a new test, create the database, and then try to upgrade
+ * it. However, the tables are empty and thus not a realistic migration plan (e.g. foreign key constraints).
+ *
+ * TO EDIT THIS TEST:
+ * * Copy the current version of the database into (robocop-assets)/browser_db_upgrade/v##.db database. You can do
+ * this via Margaret's copy profile addon - take browser.db from the profile directory. This db copy should contain a
+ * used profile - e.g. history items, bookmarks. A good way to get a used profile is to sign into sync.
+ * * MAKE SURE YOU COPY YOUR DB FIRST. Then make your changes to the DB schema code.
+ * * Test!
+ * * Note: when the application starts for testing, it may need to upgrade the database from your existing version. If
+ * this fails, the application will crash and the test may fail to start.
+ *
+ * IMPORTANT:
+ * Test DBs must be created on the oldest version of Android that is currently supported. SQLite
+ * is not forwards compatible. E.g. uploading a DB created on a 6.0 device will cause failures
+ * when robocop tests running on 4.3 are unable to load it.
+ *
+ * Implementation inspired by:
+ * http://riggaroo.co.za/automated-testing-sqlite-database-upgrades-android/
+ */
+public class testBrowserDatabaseHelperUpgrades extends UITest {
+ private static final int TEST_FROM_VERSION = 27; // We only started testing on this version.
+
+ private ArrayList<File> temporaryDbFiles;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store
+ // this there. That being said, temporary files are still stored in the application directory so these temporary
+ // files will get cleaned up when the application is uninstalled or when data is cleared.
+ temporaryDbFiles = new ArrayList<>();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ for (final File dbFile : temporaryDbFiles) {
+ dbFile.delete();
+ }
+ }
+
+ /**
+ * @throws IOException if the database cannot be copied.
+ */
+ public void test() throws IOException {
+ for (int i = TEST_FROM_VERSION; i < BrowserDatabaseHelper.DATABASE_VERSION; ++i) {
+ Log.d(LOGTAG, "Testing upgrade from version: " + i);
+ final String tempDbPath = copyDatabase(i);
+
+ final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0);
+ try {
+ fAssertEquals("Input DB isn't the expected version",
+ i, db.getVersion());
+ } finally {
+ db.close();
+ }
+
+ final BrowserDatabaseHelper dbHelperToUpgrade = new BrowserDatabaseHelper(getActivity(), tempDbPath);
+ // Ideally, we'd test upgrading version i to version i + 1 but this method does not permit that. Alas!
+ final SQLiteDatabase upgradedDb = dbHelperToUpgrade.getWritableDatabase();
+ try {
+ fAssertEquals("DB helper should upgrade to latest version",
+ BrowserDatabaseHelper.DATABASE_VERSION, upgradedDb.getVersion());
+ } finally {
+ upgradedDb.close();
+ }
+ }
+ }
+
+ /**
+ * Copies the database from the assets directory to a temporary test file.
+ *
+ * @param version version of the database to copy.
+ * @return the String path to the new copy of the database
+ * @throws IOException if reading the existing database or writing the temporary database fails
+ */
+ private String copyDatabase(final int version) throws IOException {
+ final InputStream inputStream = openDbFromAssets(version);
+ try {
+ final File dbDestination = File.createTempFile("temporaryDB-v" + version + "_", "db");
+ temporaryDbFiles.add(dbDestination);
+ Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath());
+
+ final OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(dbDestination));
+ try {
+ final byte[] buffer = new byte[1024];
+ int len;
+ while ((len = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, len);
+ }
+ outputStream.flush();
+ } finally {
+ outputStream.close();
+ }
+
+ return dbDestination.getPath();
+ } finally {
+ inputStream.close();
+ }
+ }
+
+ private InputStream openDbFromAssets(final int version) throws IOException {
+ final String dbAssetPath = String.format("browser_db_upgrade" + File.separator + String.format("v%d.db", version));
+ Log.d(LOGTAG, "Opening DB from assets: " + dbAssetPath);
+ try {
+ return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(dbAssetPath));
+ } catch (final FileNotFoundException e) {
+ throw new IllegalStateException("If you're upgrading the browser.db version, " +
+ "you need to provide an old version of the database for this test! See the javadoc.", e);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java
new file mode 100644
index 000000000..2dab2996c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java
@@ -0,0 +1,13 @@
+/* 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;
+
+
+
+public class testBrowserDiscovery extends JavascriptTest {
+ public testBrowserDiscovery() {
+ super("testBrowserDiscovery.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
new file mode 100644
index 000000000..e0ebb5c8e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
@@ -0,0 +1,1921 @@
+/* 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.SyncStatus;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.URLMetadata;
+import org.mozilla.gecko.db.URLMetadataTable;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+/*
+ * This test is meant to exercise all operations exposed by Fennec's
+ * history and bookmarks content provider. It does so in an isolated
+ * environment (see ContentProviderTest) without affecting any UI-related
+ * code.
+ */
+public class testBrowserProvider extends ContentProviderTest {
+ private long mMobileFolderId;
+
+ private void loadMobileFolderId() throws Exception {
+ Cursor c = null;
+ try {
+ c = getBookmarkByGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ mAsserter.is(c.moveToFirst(), true, "Mobile bookmarks folder is present");
+
+ mMobileFolderId = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks._ID));
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private void ensureEmptyDatabase() throws Exception {
+ Cursor c;
+
+ String guid = BrowserContract.Bookmarks.GUID;
+
+ mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ?",
+ new String[] { BrowserContract.Bookmarks.PLACES_FOLDER_GUID,
+ BrowserContract.Bookmarks.MOBILE_FOLDER_GUID,
+ BrowserContract.Bookmarks.MENU_FOLDER_GUID,
+ BrowserContract.Bookmarks.TAGS_FOLDER_GUID,
+ BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID,
+ BrowserContract.Bookmarks.UNFILED_FOLDER_GUID });
+
+ c = mProvider.query(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
+ assertCountIsAndClose(c, 6, "All non-special bookmarks and folders were deleted");
+
+ mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ c = mProvider.query(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
+ assertCountIsAndClose(c, 0, "All history entries were deleted");
+
+ /**
+ * There's no reason why the following two parts should fail.
+ * But sometimes they do, and I'm not going to spend the time
+ * to figure out why in an unrelated bug.
+ */
+
+ mProvider.delete(appendUriParam(BrowserContract.Favicons.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ c = mProvider.query(appendUriParam(BrowserContract.Favicons.CONTENT_URI,
+ BrowserContract.PARAM_SHOW_DELETED, "1"),
+ null, null, null, null);
+
+ if (c.getCount() > 0) {
+ mAsserter.dumpLog("Unexpected favicons in ensureEmptyDatabase.");
+ }
+ c.close();
+
+ mAsserter.dumpLog("ensureEmptyDatabase: Favicon deletion completed."); // Bug 968951 debug.
+ // assertCountIsAndClose(c, 0, "All favicons were deleted");
+
+ mProvider.delete(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ c = mProvider.query(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI,
+ BrowserContract.PARAM_SHOW_DELETED, "1"),
+ null, null, null, null);
+
+ if (c.getCount() > 0) {
+ mAsserter.dumpLog("Unexpected thumbnails in ensureEmptyDatabase.");
+ }
+ c.close();
+
+ mAsserter.dumpLog("ensureEmptyDatabase: Thumbnail deletion completed."); // Bug 968951 debug.
+ // assertCountIsAndClose(c, 0, "All thumbnails were deleted");
+ }
+
+ private ContentValues createBookmark(String title, String url, long parentId,
+ int type, int position, String tags, String description, String keyword) throws Exception {
+ ContentValues bookmark = new ContentValues();
+
+ bookmark.put(BrowserContract.Bookmarks.TITLE, title);
+ bookmark.put(BrowserContract.Bookmarks.URL, url);
+ bookmark.put(BrowserContract.Bookmarks.PARENT, parentId);
+ bookmark.put(BrowserContract.Bookmarks.TYPE, type);
+ bookmark.put(BrowserContract.Bookmarks.POSITION, position);
+ bookmark.put(BrowserContract.Bookmarks.TAGS, tags);
+ bookmark.put(BrowserContract.Bookmarks.DESCRIPTION, description);
+ bookmark.put(BrowserContract.Bookmarks.KEYWORD, keyword);
+
+ return bookmark;
+ }
+
+ private ContentValues createOneBookmark() throws Exception {
+ return createBookmark("Example", "http://example.com", mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ }
+
+ private Cursor getBookmarksByParent(long parent) throws Exception {
+ // Order by position.
+ return mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null,
+ BrowserContract.Bookmarks.PARENT + " = ?",
+ new String[] { String.valueOf(parent) },
+ BrowserContract.Bookmarks.POSITION);
+ }
+
+ private Cursor getBookmarkByGuid(String guid) throws Exception {
+ return mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null,
+ BrowserContract.Bookmarks.GUID + " = ?",
+ new String[] { guid },
+ null);
+ }
+
+ private Cursor getBookmarkById(long id) throws Exception {
+ return getBookmarkById(BrowserContract.Bookmarks.CONTENT_URI, id, null);
+ }
+
+ private Cursor getBookmarkById(long id, String[] projection) throws Exception {
+ return getBookmarkById(BrowserContract.Bookmarks.CONTENT_URI, id, projection);
+ }
+
+ private Cursor getBookmarkById(Uri bookmarksUri, long id) throws Exception {
+ return getBookmarkById(bookmarksUri, id, null);
+ }
+
+ private Cursor getBookmarkById(Uri bookmarksUri, long id, String[] projection) throws Exception {
+ return mProvider.query(bookmarksUri, projection,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ }
+
+ private ContentValues createHistoryEntry(String title, String url, int visits, long lastVisited) throws Exception {
+ ContentValues historyEntry = new ContentValues();
+
+ historyEntry.put(BrowserContract.History.TITLE, title);
+ historyEntry.put(BrowserContract.History.URL, url);
+ historyEntry.put(BrowserContract.History.VISITS, visits);
+ historyEntry.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited);
+
+ return historyEntry;
+ }
+
+ private ContentValues createFaviconEntry(String pageUrl, String data) throws Exception {
+ ContentValues faviconEntry = new ContentValues();
+
+ faviconEntry.put(BrowserContract.Favicons.PAGE_URL, pageUrl);
+ faviconEntry.put(BrowserContract.Favicons.URL, pageUrl + "/favicon.ico");
+ faviconEntry.put(BrowserContract.Favicons.DATA, data.getBytes("UTF8"));
+
+ return faviconEntry;
+ }
+
+ private ContentValues createThumbnailEntry(String pageUrl, String data) throws Exception {
+ ContentValues thumbnailEntry = new ContentValues();
+
+ thumbnailEntry.put(BrowserContract.Thumbnails.URL, pageUrl);
+ thumbnailEntry.put(BrowserContract.Thumbnails.DATA, data.getBytes("UTF8"));
+
+ return thumbnailEntry;
+ }
+
+ private ContentValues createUrlMetadataEntry(final String url, final String tileImage, final String tileColor,
+ final String touchIcon) {
+ final ContentValues values = new ContentValues();
+ values.put(URLMetadataTable.URL_COLUMN, url);
+ values.put(URLMetadataTable.TILE_IMAGE_URL_COLUMN, tileImage);
+ values.put(URLMetadataTable.TILE_COLOR_COLUMN, tileColor);
+ values.put(URLMetadataTable.TOUCH_ICON_COLUMN, touchIcon);
+ return values;
+ }
+
+ private ContentValues createUrlAnnotationEntry(final String url, final String key, final String value,
+ final long dateCreated) {
+ final ContentValues values = new ContentValues();
+ values.put(BrowserContract.UrlAnnotations.URL, url);
+ values.put(BrowserContract.UrlAnnotations.KEY, key);
+ values.put(BrowserContract.UrlAnnotations.VALUE, value);
+ values.put(BrowserContract.UrlAnnotations.DATE_CREATED, dateCreated);
+ values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, dateCreated);
+ return values;
+ }
+
+ private ContentValues createOneHistoryEntry() throws Exception {
+ return createHistoryEntry("Example", "http://example.com", 10, System.currentTimeMillis());
+ }
+
+ private Cursor getHistoryEntryById(long id) throws Exception {
+ return getHistoryEntryById(BrowserContract.History.CONTENT_URI, id, null);
+ }
+
+ private Cursor getHistoryEntryById(long id, String[] projection) throws Exception {
+ return getHistoryEntryById(BrowserContract.History.CONTENT_URI, id, projection);
+ }
+
+ private Cursor getHistoryEntryById(Uri historyUri, long id) throws Exception {
+ return getHistoryEntryById(historyUri, id, null);
+ }
+
+ private Cursor getHistoryEntryById(Uri historyUri, long id, String[] projection) throws Exception {
+ return mProvider.query(historyUri, projection,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ }
+
+ private Cursor getFaviconsByUrl(String url) throws Exception {
+ return mProvider.query(BrowserContract.Combined.CONTENT_URI, null,
+ BrowserContract.Combined.URL + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ private Cursor getThumbnailByUrl(String url) throws Exception {
+ return mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null,
+ BrowserContract.Thumbnails.URL + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ private Cursor getUrlAnnotationByUrl(final String url) throws Exception {
+ return mProvider.query(BrowserContract.UrlAnnotations.CONTENT_URI, null,
+ BrowserContract.UrlAnnotations.URL + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ private Cursor getUrlMetadataByUrl(final String url) throws Exception {
+ return mProvider.query(URLMetadataTable.CONTENT_URI, null,
+ URLMetadataTable.URL_COLUMN + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
+
+ mTests.add(new TestSpecialFolders());
+
+ mTests.add(new TestInsertBookmarks());
+ mTests.add(new TestInsertBookmarksFavicons());
+ mTests.add(new TestDeleteBookmarks());
+ mTests.add(new TestDeleteBookmarksFavicons());
+ mTests.add(new TestUpdateBookmarks());
+ mTests.add(new TestUpdateBookmarksFavicons());
+ mTests.add(new TestPositionBookmarks());
+
+ mTests.add(new TestInsertHistory());
+ mTests.add(new TestInsertHistoryFavicons());
+ mTests.add(new TestDeleteHistory());
+ mTests.add(new TestDeleteHistoryFavicons());
+ mTests.add(new TestUpdateHistory());
+ mTests.add(new TestUpdateHistoryFavicons());
+ mTests.add(new TestUpdateOrInsertHistory());
+ mTests.add(new TestInsertHistoryThumbnails());
+ mTests.add(new TestUpdateHistoryThumbnails());
+ mTests.add(new TestDeleteHistoryThumbnails());
+
+ mTests.add(new TestInsertUrlAnnotations());
+ mTests.add(new TestInsertUrlMetadata());
+
+ mTests.add(new TestBatchOperations());
+
+ mTests.add(new TestCombinedView());
+ mTests.add(new TestCombinedViewDisplay());
+ mTests.add(new TestCombinedViewWithDeletedBookmark());
+
+ mTests.add(new TestBrowserProviderNotifications());
+ }
+
+ public void testBrowserProvider() throws Exception {
+ loadMobileFolderId();
+
+ for (int i = 0; i < mTests.size(); i++) {
+ Runnable test = mTests.get(i);
+
+ final String testName = test.getClass().getSimpleName();
+ setTestName(testName);
+ ensureEmptyDatabase();
+ mAsserter.dumpLog("testBrowserProvider: Database empty - Starting " + testName + ".");
+ test.run();
+ }
+ }
+
+ private class TestBatchOperations extends TestCase {
+ static final int TESTCOUNT = 100;
+
+ public void testApplyBatch() throws Exception {
+ ArrayList<ContentProviderOperation> mOperations
+ = new ArrayList<ContentProviderOperation>();
+
+ // Test a bunch of inserts with applyBatch
+ ContentValues values = new ContentValues();
+ ContentProviderOperation.Builder builder = null;
+
+ for (int i = 0; i < TESTCOUNT; i++) {
+ values.clear();
+ values.put(BrowserContract.History.VISITS, i);
+ values.put(BrowserContract.History.TITLE, "Test" + i);
+ values.put(BrowserContract.History.URL, "http://www.test.org/" + i);
+
+ // Insert
+ builder = ContentProviderOperation.newInsert(BrowserContract.History.CONTENT_URI);
+ builder.withValues(values);
+ // Queue the operation
+ mOperations.add(builder.build());
+ }
+
+ ContentProviderResult[] applyResult =
+ mProvider.applyBatch(mOperations);
+
+ boolean allFound = true;
+ for (int i = 0; i < TESTCOUNT; i++) {
+ Cursor cursor = mProvider.query(BrowserContract.History.CONTENT_URI,
+ null,
+ BrowserContract.History.URL + " = ?",
+ new String[] { "http://www.test.org/" + i },
+ null);
+
+ if (!cursor.moveToFirst())
+ allFound = false;
+ cursor.close();
+ }
+ mAsserter.is(allFound, true, "Found all batchApply entries");
+ mOperations.clear();
+
+ // Update all visits to 1
+ values.clear();
+ values.put(BrowserContract.History.VISITS, 1);
+ for (int i = 0; i < TESTCOUNT; i++) {
+ builder = ContentProviderOperation.newUpdate(BrowserContract.History.CONTENT_URI);
+ builder.withSelection(BrowserContract.History.URL + " = ?",
+ new String[] {"http://www.test.org/" + i});
+ builder.withValues(values);
+ builder.withExpectedCount(1);
+ // Queue the operation
+ mOperations.add(builder.build());
+ }
+
+ boolean seenException = false;
+ try {
+ applyResult = mProvider.applyBatch(mOperations);
+ } catch (OperationApplicationException ex) {
+ seenException = true;
+ }
+ mAsserter.is(seenException, false, "Batch updating succeeded");
+ mOperations.clear();
+
+ // Delete all visits
+ for (int i = 0; i < TESTCOUNT; i++) {
+ builder = ContentProviderOperation.newDelete(BrowserContract.History.CONTENT_URI);
+ builder.withSelection(BrowserContract.History.URL + " = ?",
+ new String[] {"http://www.test.org/" + i});
+ builder.withExpectedCount(1);
+ // Queue the operation
+ mOperations.add(builder.build());
+ }
+ try {
+ applyResult = mProvider.applyBatch(mOperations);
+ } catch (OperationApplicationException ex) {
+ seenException = true;
+ }
+ mAsserter.is(seenException, false, "Batch deletion succeeded");
+ }
+
+ // Force a Constraint error, see if later operations still apply correctly
+ public void testApplyBatchErrors() throws Exception {
+ ArrayList<ContentProviderOperation> mOperations
+ = new ArrayList<ContentProviderOperation>();
+
+ // Test a bunch of inserts with applyBatch
+ ContentProviderOperation.Builder builder = null;
+ ContentValues values = createFaviconEntry("http://www.test.org", "FAVICON");
+ builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI);
+ builder.withValues(values);
+ mOperations.add(builder.build());
+
+ // Make a duplicate, this will fail because of a UNIQUE constraint
+ builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI);
+ builder.withValues(values);
+ mOperations.add(builder.build());
+
+ // This is valid and should be in the table afterwards
+ values.put(BrowserContract.Favicons.URL, "http://www.test.org/valid.ico");
+ builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI);
+ builder.withValues(values);
+ mOperations.add(builder.build());
+
+ boolean seenException = false;
+
+ try {
+ ContentProviderResult[] applyResult =
+ mProvider.applyBatch(mOperations);
+ } catch (OperationApplicationException ex) {
+ seenException = true;
+ }
+
+ // This test may need to go away if Bug 717428 is fixed.
+ mAsserter.is(seenException, true, "Expected failure in favicons table");
+
+ boolean allFound = true;
+ Cursor cursor = mProvider.query(BrowserContract.Favicons.CONTENT_URI,
+ null,
+ BrowserContract.Favicons.URL + " = ?",
+ new String[] { "http://www.test.org/valid.ico" },
+ null);
+
+ if (!cursor.moveToFirst())
+ allFound = false;
+ cursor.close();
+
+ mAsserter.is(allFound, true, "Found all applyBatch (with error) entries");
+ }
+
+ public void testBulkInsert() throws Exception {
+ // Test a bunch of inserts with bulkInsert
+ ContentValues allVals[] = new ContentValues[TESTCOUNT];
+ for (int i = 0; i < TESTCOUNT; i++) {
+ allVals[i] = new ContentValues();
+ allVals[i].put(BrowserContract.History.URL, i);
+ allVals[i].put(BrowserContract.History.TITLE, "Test" + i);
+ allVals[i].put(BrowserContract.History.URL, "http://www.test.org/" + i);
+ }
+
+ int inserts = mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, allVals);
+ mAsserter.is(inserts, TESTCOUNT, "Excepted number of inserts matches");
+
+ boolean allFound = true;
+ for (int i = 0; i < TESTCOUNT; i++) {
+ Cursor cursor = mProvider.query(BrowserContract.History.CONTENT_URI,
+ null,
+ BrowserContract.History.URL + " = ?",
+ new String[] { "http://www.test.org/" + i },
+ null);
+
+ if (!cursor.moveToFirst())
+ allFound = false;
+ cursor.close();
+ }
+ mAsserter.is(allFound, true, "Found all bulkInsert entries");
+ }
+
+ @Override
+ public void test() throws Exception {
+ testApplyBatch();
+ // Clean up
+ ensureEmptyDatabase();
+
+ testBulkInsert();
+ ensureEmptyDatabase();
+
+ testApplyBatchErrors();
+ }
+ }
+
+ private class TestSpecialFolders extends TestCase {
+ @Override
+ public void test() throws Exception {
+ Cursor c = mProvider.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] { BrowserContract.Bookmarks._ID,
+ BrowserContract.Bookmarks.GUID,
+ BrowserContract.Bookmarks.PARENT },
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ?",
+ new String[] { BrowserContract.Bookmarks.PLACES_FOLDER_GUID,
+ BrowserContract.Bookmarks.MOBILE_FOLDER_GUID,
+ BrowserContract.Bookmarks.MENU_FOLDER_GUID,
+ BrowserContract.Bookmarks.TAGS_FOLDER_GUID,
+ BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID,
+ BrowserContract.Bookmarks.UNFILED_FOLDER_GUID},
+ null);
+
+ mAsserter.is(c.getCount(), 6, "Right number of special folders");
+
+ int rootId = BrowserContract.Bookmarks.FIXED_ROOT_ID;
+
+ while (c.moveToNext()) {
+ int id = c.getInt(c.getColumnIndex(BrowserContract.Bookmarks._ID));
+ String guid = c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID));
+ int parentId = c.getInt(c.getColumnIndex(BrowserContract.Bookmarks.PARENT));
+
+ if (guid.equals(BrowserContract.Bookmarks.PLACES_FOLDER_GUID)) {
+ mAsserter.is(id, rootId, "The id of places folder is correct");
+ }
+
+ mAsserter.is(parentId, rootId,
+ "The PARENT of the " + guid + " special folder is correct");
+ }
+
+ c.close();
+ }
+ }
+
+ private class TestInsertBookmarks extends TestCase {
+ private long insertWithNullCol(String colName) throws Exception {
+ ContentValues b = createOneBookmark();
+ b.putNull(colName);
+ long id = -1;
+
+ try {
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ } catch (Exception e) {}
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ final Cursor c = getBookmarkById(id);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), b.getAsString(BrowserContract.Bookmarks.TITLE),
+ "Inserted bookmark has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), b.getAsString(BrowserContract.Bookmarks.URL),
+ "Inserted bookmark has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), b.getAsString(BrowserContract.Bookmarks.TAGS),
+ "Inserted bookmark has correct tags");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), b.getAsString(BrowserContract.Bookmarks.KEYWORD),
+ "Inserted bookmark has correct keyword");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), b.getAsString(BrowserContract.Bookmarks.DESCRIPTION),
+ "Inserted bookmark has correct description");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), b.getAsString(BrowserContract.Bookmarks.POSITION),
+ "Inserted bookmark has correct position");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), b.getAsString(BrowserContract.Bookmarks.TYPE),
+ "Inserted bookmark has correct type");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), b.getAsString(BrowserContract.Bookmarks.PARENT),
+ "Inserted bookmark has correct parent ID");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(0),
+ "Inserted bookmark has correct is-deleted state");
+
+ id = insertWithNullCol(BrowserContract.Bookmarks.POSITION);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert bookmark with null position");
+
+ id = insertWithNullCol(BrowserContract.Bookmarks.TYPE);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert bookmark with null type");
+
+ if (Build.VERSION.SDK_INT >= 8 &&
+ Build.VERSION.SDK_INT < 16) {
+ b = createOneBookmark();
+ b.put(BrowserContract.Bookmarks.PARENT, -1);
+ id = -1;
+
+ try {
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ } catch (Exception e) {}
+
+ mAsserter.is(id, -1L,
+ "Should not be able to insert bookmark with invalid parent");
+ }
+
+ b = createOneBookmark();
+ b.remove(BrowserContract.Bookmarks.TYPE);
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ final Cursor c2 = getBookmarkById(id);
+ try {
+ mAsserter.is(c2.moveToFirst(), true, "Inserted bookmark found");
+ mAsserter.is(c2.getString(c2.getColumnIndex(BrowserContract.Bookmarks.TYPE)), String.valueOf(BrowserContract.Bookmarks.TYPE_BOOKMARK),
+ "Inserted bookmark has correct default type");
+ } finally {
+ c2.close();
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private class TestInsertBookmarksFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+
+ final String favicon = "FAVICON";
+ final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
+
+ Cursor c = getBookmarkById(id, new String[] { BrowserContract.Bookmarks.FAVICON });
+
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON)), "UTF8"),
+ favicon, "Inserted bookmark has corresponding favicon image");
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ private class TestDeleteBookmarks extends TestCase {
+ private long insertOneBookmark() throws Exception {
+ ContentValues b = createOneBookmark();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ Cursor c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+ c.close();
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ long id = insertOneBookmark();
+
+ int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
+
+ Cursor c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), true, "Deleted bookmark was only marked as deleted");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), null,
+ "Deleted bookmark title is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), null,
+ "Deleted bookmark URL is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), null,
+ "Deleted bookmark tags is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), null,
+ "Deleted bookmark keyword is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), null,
+ "Deleted bookmark description is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), String.valueOf(0),
+ "Deleted bookmark has correct position");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), null,
+ "Deleted bookmark parent ID is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(1),
+ "Deleted bookmark has correct is-deleted state");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON_ID)), null,
+ "Deleted bookmark Favicon ID is null");
+ mAsserter.isnot(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID)), null,
+ "Deleted bookmark GUID is not null");
+ c.close();
+
+ deleted = mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
+
+ c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), false, "Inserted bookmark is now actually deleted");
+ c.close();
+
+ id = insertOneBookmark();
+
+ deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
+ mAsserter.is((deleted == 1), true,
+ "Inserted bookmark was deleted using URI with id");
+
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), false,
+ "Inserted bookmark can't be found after deletion using URI with ID");
+ c.close();
+
+ if (Build.VERSION.SDK_INT >= 8 &&
+ Build.VERSION.SDK_INT < 16) {
+ ContentValues b = createBookmark("Folder", null, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "folderTags", "folderDescription", "folderKeyword");
+
+ long parentId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ c = getBookmarkById(parentId);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmarks folder found");
+ c.close();
+
+ b = createBookmark("Example", "http://example.com", parentId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+ c.close();
+
+ deleted = 0;
+ try {
+ Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, parentId);
+ deleted = mProvider.delete(appendUriParam(uri, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ } catch(Exception e) {}
+
+ mAsserter.is((deleted == 0), true,
+ "Should not be able to delete folder that causes orphan bookmarks");
+ }
+ }
+ }
+
+ private class TestDeleteBookmarksFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+
+ final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON"));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+ c.close();
+
+ mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
+ c.close();
+ }
+ }
+
+ private class TestUpdateBookmarks extends TestCase {
+ private int updateWithNullCol(long id, String colName) throws Exception {
+ ContentValues u = new ContentValues();
+ u.putNull(colName);
+
+ int updated = 0;
+
+ try {
+ updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ } catch (Exception e) {}
+
+ return updated;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ Cursor c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+
+ long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_CREATED));
+ long dateModified = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_MODIFIED));
+
+ ContentValues u = new ContentValues();
+ u.put(BrowserContract.Bookmarks.TITLE, b.getAsString(BrowserContract.Bookmarks.TITLE) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.URL, b.getAsString(BrowserContract.Bookmarks.URL) + "/more/stuff");
+ u.put(BrowserContract.Bookmarks.TAGS, b.getAsString(BrowserContract.Bookmarks.TAGS) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.DESCRIPTION, b.getAsString(BrowserContract.Bookmarks.DESCRIPTION) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.KEYWORD, b.getAsString(BrowserContract.Bookmarks.KEYWORD) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER);
+ u.put(BrowserContract.Bookmarks.POSITION, 10);
+
+ int updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((updated == 1), true, "Inserted bookmark was updated");
+ c.close();
+
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), u.getAsString(BrowserContract.Bookmarks.TITLE),
+ "Inserted bookmark has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
+ "Inserted bookmark has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), u.getAsString(BrowserContract.Bookmarks.TAGS),
+ "Inserted bookmark has correct tags");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), u.getAsString(BrowserContract.Bookmarks.KEYWORD),
+ "Inserted bookmark has correct keyword");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), u.getAsString(BrowserContract.Bookmarks.DESCRIPTION),
+ "Inserted bookmark has correct description");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), u.getAsString(BrowserContract.Bookmarks.POSITION),
+ "Inserted bookmark has correct position");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), u.getAsString(BrowserContract.Bookmarks.TYPE),
+ "Inserted bookmark has correct type");
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_CREATED)),
+ dateCreated,
+ "Updated bookmark has same creation date");
+
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_MODIFIED)),
+ dateModified,
+ "Updated bookmark has new modification date");
+
+ updated = updateWithNullCol(id, BrowserContract.Bookmarks.POSITION);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update bookmark with null position");
+
+ updated = updateWithNullCol(id, BrowserContract.Bookmarks.TYPE);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update bookmark with null type");
+
+ u = new ContentValues();
+ u.put(BrowserContract.Bookmarks.URL, "http://examples2.com");
+
+ updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), u, null, null);
+ c.close();
+
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
+ "Updated bookmark has correct URL using URI with id");
+ c.close();
+ }
+ }
+
+ private class TestUpdateBookmarksFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+
+ final String favicon = "FAVICON";
+ final String newFavicon = "NEW_FAVICON";
+ final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
+
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b);
+
+ // Insert the favicon into the favicons table
+ ContentValues f = createFaviconEntry(pageUrl, favicon);
+ long faviconId = ContentUris.parseId(mProvider.insert(BrowserContract.Favicons.CONTENT_URI, f));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+
+ ContentValues u = createFaviconEntry(pageUrl, newFavicon);
+ mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ newFavicon, "Updated favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ /**
+ * Create a folder of one thousand and one bookmarks, then impose an order
+ * on them.
+ *
+ * Verify that the reordering worked by querying.
+ */
+ private class TestPositionBookmarks extends TestCase {
+
+ public String makeGUID(final long in) {
+ String part = String.valueOf(in);
+ return "aaaaaaaaaaaa".substring(0, (12 - part.length())) + part;
+ }
+
+ public void compareCursorToItems(final Cursor c, final String[] items, final int count) {
+ mAsserter.is(c.moveToFirst(), true, "Folder has children.");
+
+ int posColumn = c.getColumnIndex(BrowserContract.Bookmarks.POSITION);
+ int guidColumn = c.getColumnIndex(BrowserContract.Bookmarks.GUID);
+ int i = 0;
+
+ while (!c.isAfterLast()) {
+ String guid = c.getString(guidColumn);
+ long pos = c.getLong(posColumn);
+ if ((pos != i) || (guid == null) || (!guid.equals(items[i]))) {
+ mAsserter.is(pos, (long) i, "Position matches sequence.");
+ mAsserter.is(guid, items[i], "GUID matches sequence.");
+ }
+ ++i;
+ c.moveToNext();
+ }
+
+ mAsserter.is(i, count, "Folder has the right number of children.");
+ c.close();
+ }
+
+ public static final int NUMBER_OF_CHILDREN = 1001;
+ @Override
+ public void test() throws Exception {
+ // Create the containing folder.
+ ContentValues folder = createBookmark("FolderFolder", "", mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "",
+ "description", "keyword");
+ folder.put(BrowserContract.Bookmarks.GUID, "folderfolder");
+ long folderId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, folder));
+
+ mAsserter.dumpLog("TestPositionBookmarks: Folder inserted"); // Bug 968951 debug.
+
+ // Create the children.
+ String[] items = new String[NUMBER_OF_CHILDREN];
+
+ // Reuse the same ContentValues.
+ ContentValues item = createBookmark("Test Bookmark", "http://example.com", folderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "",
+ "description", "keyword");
+
+ for (int i = 0; i < NUMBER_OF_CHILDREN; ++i) {
+ String guid = makeGUID(i);
+ items[i] = guid;
+ item.put(BrowserContract.Bookmarks.GUID, guid);
+ item.put(BrowserContract.Bookmarks.POSITION, i);
+ item.put(BrowserContract.Bookmarks.URL, "http://example.com/" + guid);
+ item.put(BrowserContract.Bookmarks.TITLE, "Test Bookmark " + guid);
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, item);
+ }
+
+ mAsserter.dumpLog("TestPositionBookmarks: Bookmarks inserted"); // Bug 968951 debug.
+
+ Cursor c;
+
+ // Verify insertion.
+ c = getBookmarksByParent(folderId);
+ mAsserter.dumpLog("TestPositionBookmarks: Got bookmarks by parent"); // Bug 968951 debug.
+ compareCursorToItems(c, items, NUMBER_OF_CHILDREN);
+ c.close();
+
+ // Now permute the items array.
+ Random rand = new Random();
+ for (int i = 0; i < NUMBER_OF_CHILDREN; ++i) {
+ final int newPosition = rand.nextInt(NUMBER_OF_CHILDREN);
+ final String switched = items[newPosition];
+ items[newPosition] = items[i];
+ items[i] = switched;
+ }
+
+ // Impose the positions.
+ long updated = mProvider.update(BrowserContract.Bookmarks.POSITIONS_CONTENT_URI, null, null, items);
+ mAsserter.is(updated, (long) NUMBER_OF_CHILDREN, "Updated " + NUMBER_OF_CHILDREN + " positions.");
+
+ // Verify that the database was updated.
+ c = getBookmarksByParent(folderId);
+ compareCursorToItems(c, items, NUMBER_OF_CHILDREN);
+ c.close();
+ }
+ }
+
+ private class TestInsertHistory extends TestCase {
+ private long insertWithNullCol(String colName) throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ h.putNull(colName);
+ long id = -1;
+
+ try {
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ } catch (Exception e) {}
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ Cursor c = getHistoryEntryById(id);
+
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), h.getAsString(BrowserContract.History.TITLE),
+ "Inserted history entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), h.getAsString(BrowserContract.History.URL),
+ "Inserted history entry has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.VISITS)), h.getAsString(BrowserContract.History.VISITS),
+ "Inserted history entry has correct number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)), h.getAsString(BrowserContract.History.DATE_LAST_VISITED),
+ "Inserted history entry has correct last visited date");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.IS_DELETED)), String.valueOf(0),
+ "Inserted history entry has correct is-deleted state");
+
+ id = insertWithNullCol(BrowserContract.History.URL);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert history with null URL");
+
+ id = insertWithNullCol(BrowserContract.History.VISITS);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert history with null number of visits");
+ c.close();
+ }
+ }
+
+ private class TestInsertHistoryFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String favicon = "FAVICON";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
+
+ Cursor c = getHistoryEntryById(id, new String[] { BrowserContract.History.FAVICON });
+
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.History.FAVICON)), "UTF8"),
+ favicon, "Inserted history entry has corresponding favicon image");
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ private class TestDeleteHistory extends TestCase {
+ private long insertOneHistoryEntry() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ Cursor c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+ c.close();
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ long id = insertOneHistoryEntry();
+
+ int deleted = mProvider.delete(BrowserContract.History.CONTENT_URI,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted history entry was deleted");
+
+ Cursor c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), true, "Deleted history entry was only marked as deleted");
+
+ deleted = mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted history entry was deleted");
+ c.close();
+
+ c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), false, "Inserted history is now actually deleted");
+
+ id = insertOneHistoryEntry();
+
+ deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+ mAsserter.is((deleted == 1), true,
+ "Inserted history entry was deleted using URI with id");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), false,
+ "Inserted history entry can't be found after deletion using URI with ID");
+ c.close();
+ }
+ }
+
+ private class TestDeleteHistoryFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON"));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
+ c.close();
+ }
+ }
+
+ private class TestUpdateHistory extends TestCase {
+ private int updateWithNullCol(long id, String colName) throws Exception {
+ ContentValues u = new ContentValues();
+ u.putNull(colName);
+
+ int updated = 0;
+
+ try {
+ updated = mProvider.update(BrowserContract.History.CONTENT_URI, u,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ } catch (Exception e) {}
+
+ return updated;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ Cursor c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+
+ long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
+ long dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
+
+ ContentValues u = new ContentValues();
+ u.put(BrowserContract.History.VISITS, h.getAsInteger(BrowserContract.History.VISITS) + 1);
+ u.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis());
+ u.put(BrowserContract.History.TITLE, h.getAsString(BrowserContract.History.TITLE) + "CHANGED");
+ u.put(BrowserContract.History.URL, h.getAsString(BrowserContract.History.URL) + "/more/stuff");
+
+ int updated = mProvider.update(BrowserContract.History.CONTENT_URI, u,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), u.getAsString(BrowserContract.History.TITLE),
+ "Updated history entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
+ "Updated history entry has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.VISITS)), u.getAsString(BrowserContract.History.VISITS),
+ "Updated history entry has correct number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)), u.getAsString(BrowserContract.History.DATE_LAST_VISITED),
+ "Updated history entry has correct last visited date");
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)),
+ dateCreated,
+ "Updated history entry has same creation date");
+
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)),
+ dateModified,
+ "Updated history entry has new modification date");
+
+ updated = updateWithNullCol(id, BrowserContract.History.URL);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update history with null URL");
+
+ updated = updateWithNullCol(id, BrowserContract.History.VISITS);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update history with null number of visits");
+
+ u = new ContentValues();
+ u.put(BrowserContract.History.URL, "http://examples2.com");
+
+ updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), u, null, null);
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
+ "Updated history entry has correct URL using URI with id");
+ c.close();
+ }
+ }
+
+ private class TestUpdateHistoryFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String favicon = "FAVICON";
+ final String newFavicon = "NEW_FAVICON";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ mProvider.insert(BrowserContract.History.CONTENT_URI, h);
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+
+ ContentValues u = createFaviconEntry(pageUrl, newFavicon);
+
+ mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ newFavicon, "Updated favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ private class TestUpdateOrInsertHistory extends TestCase {
+ private final String TEST_URL_1 = "http://example.com";
+ private final String TEST_URL_2 = "http://example.org";
+ private final String TEST_TITLE = "Example";
+
+ private long getHistoryEntryIdByUrl(String url) {
+ Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI,
+ new String[] { BrowserContract.History._ID },
+ BrowserContract.History.URL + " = ?",
+ new String[] { url },
+ null);
+ c.moveToFirst();
+ long id = c.getLong(0);
+ c.close();
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ Uri updateHistoryUri = BrowserContract.History.CONTENT_URI.buildUpon().
+ appendQueryParameter("increment_visits", "true").build();
+ Uri updateOrInsertHistoryUri = BrowserContract.History.CONTENT_URI.buildUpon().
+ appendQueryParameter("insert_if_needed", "true").
+ appendQueryParameter("increment_visits", "true").build();
+
+ // Update a non-existent history entry, without specifying visits or title
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.History.URL, TEST_URL_1);
+
+ int updated = mProvider.update(updateHistoryUri, values,
+ BrowserContract.History.URL + " = ?",
+ new String[] { TEST_URL_1 });
+ mAsserter.is((updated == 0), true, "History entry was not updated");
+ Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, null, null, null);
+ mAsserter.is(c.moveToFirst(), false, "History entry was not inserted");
+ c.close();
+
+ // Now let's try with update-or-insert.
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History.URL + " = ?",
+ new String[] { TEST_URL_1 });
+ mAsserter.is((updated == 1), true, "History entry was inserted");
+
+ long id = getHistoryEntryIdByUrl(TEST_URL_1);
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "History entry was inserted");
+
+ long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
+ long dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 1L,
+ "Inserted history entry has correct default number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_URL_1,
+ "Inserted history entry has correct default title");
+
+ // Update the history entry, without specifying an additional visit count
+ values = new ContentValues();
+ values.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis());
+ values.put(BrowserContract.History.TITLE, TEST_TITLE);
+
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
+ "Updated history entry has correct title");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 2L,
+ "Updated history entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated,
+ "Updated history entry has same creation date");
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified,
+ "Updated history entry has new modification date");
+
+ // Create a new history entry, specifying visits and history
+ values = new ContentValues();
+ values.put(BrowserContract.History.URL, TEST_URL_2);
+ values.put(BrowserContract.History.TITLE, TEST_TITLE);
+ values.put(BrowserContract.History.VISITS, 10);
+
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History.URL + " = ?",
+ new String[] { values.getAsString(BrowserContract.History.URL) });
+ mAsserter.is((updated == 1), true, "History entry was inserted");
+
+ id = getHistoryEntryIdByUrl(TEST_URL_2);
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "History entry was inserted");
+
+ dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
+ dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 10L,
+ "Inserted history entry has correct specified number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
+ "Inserted history entry has correct specified title");
+
+ // Update the history entry, specifying additional visit count.
+ // The expectation is that the value is ignored, and count is bumped by 1 only.
+ // At the same time, a visit is inserted into the visits table.
+ // See junit4 tests in BrowserProviderHistoryVisitsTest.
+ values = new ContentValues();
+ values.put(BrowserContract.History.VISITS, 10);
+
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
+ "Updated history entry has correct unchanged title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), TEST_URL_2,
+ "Updated history entry has correct unchanged URL");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 11L,
+ "Updated history entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated,
+ "Updated history entry has same creation date");
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified,
+ "Updated history entry has new modification date");
+ c.close();
+
+ }
+ }
+
+ private class TestInsertHistoryThumbnails extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String thumbnail = "THUMBNAIL";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ // Insert the thumbnail into the thumbnails table
+ mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail));
+
+ Cursor c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
+ thumbnail, "Inserted thumbnail has corresponding thumbnail image");
+ c.close();
+ }
+ }
+
+ private class TestUpdateHistoryThumbnails extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String thumbnail = "THUMBNAIL";
+ final String newThumbnail = "NEW_THUMBNAIL";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ mProvider.insert(BrowserContract.History.CONTENT_URI, h);
+
+ // Insert the thumbnail into the thumbnails table
+ mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail));
+
+ Cursor c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
+ thumbnail, "Inserted thumbnail has corresponding thumbnail image");
+
+ ContentValues u = createThumbnailEntry(pageUrl, newThumbnail);
+
+ mProvider.update(BrowserContract.Thumbnails.CONTENT_URI, u, null, null);
+ c.close();
+
+ c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Updated thumbnail found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
+ newThumbnail, "Updated thumbnail has corresponding thumbnail image");
+ c.close();
+ }
+ }
+
+ private class TestDeleteHistoryThumbnails extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ // Insert the thumbnail into the thumbnails table
+ mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, "THUMBNAIL"));
+
+ Cursor c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
+
+ mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+ c.close();
+
+ c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), false, "Thumbnail is deleted with last reference to it");
+ c.close();
+ }
+ }
+
+ private class TestInsertUrlAnnotations extends TestCase {
+ @Override
+ public void test() throws Exception {
+ testInsertionViaContentProvider();
+ testInsertionViaUrlAnnotations();
+ }
+
+ private void testInsertionViaContentProvider() throws Exception {
+ final String url = "http://mozilla.org";
+ final String key = "todo";
+ final String value = "v";
+ final long dateCreated = System.currentTimeMillis();
+
+ mProvider.insert(BrowserContract.UrlAnnotations.CONTENT_URI, createUrlAnnotationEntry(url, key, value, dateCreated));
+
+ final Cursor c = getUrlAnnotationByUrl(url);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "Inserted url annotation found");
+ assertKeyValueSync(c, key, value);
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_CREATED)), dateCreated,
+ "Inserted url annotation has correct date created");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_MODIFIED)), dateCreated,
+ "Inserted url annotation has correct date modified");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void testInsertionViaUrlAnnotations() throws Exception {
+ final String url = "http://hello.org";
+ final String key = "toTheUniverse";
+ final String value = "42a";
+ final long timeBeforeCreation = System.currentTimeMillis();
+
+ BrowserDB.from(getTestProfile()).getUrlAnnotations().insertAnnotation(mResolver, url, key, value);
+
+ final Cursor c = getUrlAnnotationByUrl(url);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "Inserted url annotation found");
+ assertKeyValueSync(c, key, value);
+ mAsserter.is(true, c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_CREATED)) >= timeBeforeCreation,
+ "Inserted url annotation has date created greater than or equal to time saved before insertion");
+ mAsserter.is(true, c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_MODIFIED)) >= timeBeforeCreation,
+ "Inserted url annotation has correct date modified greater than or equal to time saved before insertion");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void assertKeyValueSync(final Cursor c, final String key, final String value) {
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.UrlAnnotations.KEY)), key,
+ "Inserted url annotation has correct key");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.UrlAnnotations.VALUE)), value,
+ "Inserted url annotation has correct value");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.UrlAnnotations.SYNC_STATUS)), SyncStatus.NEW.getDBValue(),
+ "Inserted url annotation has default sync status");
+ }
+ }
+
+ private class TestInsertUrlMetadata extends TestCase {
+ @Override
+ public void test() throws Exception {
+ testInsertionViaContentProvider();
+ testInsertionViaUrlMetadata();
+ // testRetrievalViaUrlMetadata depends on data added in the previous two tests
+ testRetrievalViaUrlMetadata();
+ }
+
+ final String url1 = "http://mozilla.org";
+ final String url2 = "http://hello.org";
+
+ private void testInsertionViaContentProvider() throws Exception {
+ final String tileImage = "http://mozilla.org/tileImage.png";
+ final String tileColor = "#FF0000";
+ final String touchIcon = "http://mozilla.org/touchIcon.png";
+
+ // We can only use update since the redirection machinery doesn't exist for insert
+ mProvider.update(URLMetadataTable.CONTENT_URI.buildUpon().appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ createUrlMetadataEntry(url1, tileImage, tileColor, touchIcon),
+ URLMetadataTable.URL_COLUMN + "=?",
+ new String[] {url1}
+ );
+
+ final Cursor c = getUrlMetadataByUrl(url1);
+ try {
+ mAsserter.is(c.getCount(), 1, "URL metadata inserted via Content Provider not found");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void testInsertionViaUrlMetadata() throws Exception {
+ final String tileImage = "http://hello.org/tileImage.png";
+ final String tileColor = "#FF0000";
+ final String touchIcon = "http://hello.org/touchIcon.png";
+
+ final Map<String, Object> data = new HashMap<>();
+ data.put(URLMetadataTable.URL_COLUMN, url2);
+ data.put(URLMetadataTable.TILE_IMAGE_URL_COLUMN, tileImage);
+ data.put(URLMetadataTable.TILE_COLOR_COLUMN, tileColor);
+ data.put(URLMetadataTable.TOUCH_ICON_COLUMN, touchIcon);
+
+ BrowserDB.from(getTestProfile()).getURLMetadata().save(mResolver, data);
+
+ final Cursor c = getUrlMetadataByUrl(url2);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "URL metadata inserted via UrlMetadata not found");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void testRetrievalViaUrlMetadata() {
+ // LocalURLMetadata has some caching of results: we need to test that this caching
+ // doesn't prevent us from accessing data that might not have been loaded into the cache.
+ // We do this by first doing queries with a subset of data, then later querying additional
+ // data for a given URL. E.g. even if the first query results in only the requested
+ // column being cached, the subsequent query should still retrieve all requested columns.
+ // (In this case the URL may be cached but without all data, we need to make sure that
+ // this state is correctly handled.)
+ URLMetadata metadata = BrowserDB.from(getTestProfile()).getURLMetadata();
+
+ Map<String, Map<String, Object>> results;
+ Map<String, Object> urlData;
+
+ // 1: retrieve just touch Icons for URL 1
+ results = metadata.getForURLs(mResolver,
+ Collections.singletonList(url1),
+ Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN));
+
+ mAsserter.is(results.containsKey(url1), true, "URL 1 not found in results");
+
+ urlData = results.get(url1);
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TOUCH_ICON_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+
+ // 2: retrieve just tile color for URL 2
+ results = metadata.getForURLs(mResolver,
+ Collections.singletonList(url2),
+ Collections.singletonList(URLMetadataTable.TILE_COLOR_COLUMN));
+
+ mAsserter.is(results.containsKey(url2), true, "URL 2 not found in results");
+
+ urlData = results.get(url2);
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_COLOR_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+
+
+ // 3: retrieve all columns for both URLs
+ final List<String> urls = Arrays.asList(url1, url2);
+
+ results = metadata.getForURLs(mResolver,
+ urls,
+ Arrays.asList(URLMetadataTable.TILE_IMAGE_URL_COLUMN,
+ URLMetadataTable.TILE_COLOR_COLUMN,
+ URLMetadataTable.TOUCH_ICON_COLUMN
+ ));
+
+ mAsserter.is(results.containsKey(url1), true, "URL 1 not found in results");
+ mAsserter.is(results.containsKey(url2), true, "URL 2 not found in results");
+
+
+ for (final String url : urls) {
+ urlData = results.get(url);
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_IMAGE_URL_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_COLOR_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TOUCH_ICON_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+ }
+ }
+ }
+
+ private class TestCombinedView extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE_1 = "Test Page 1";
+ final String TITLE_2 = "Test Page 2";
+ final String TITLE_3_HISTORY = "Test Page 3 (History Entry)";
+ final String TITLE_3_BOOKMARK = "Test Page 3 (Bookmark Entry)";
+ final String TITLE_3_BOOKMARK2 = "Test Page 3 (Bookmark Entry 2)";
+
+ final String URL_1 = "http://example1.com";
+ final String URL_2 = "http://example2.com";
+ final String URL_3 = "http://example3.com";
+
+ final int VISITS = 10;
+ final long LAST_VISITED = System.currentTimeMillis();
+
+ // Create a basic history entry
+ ContentValues basicHistory = createHistoryEntry(TITLE_1, URL_1, VISITS, LAST_VISITED);
+ long basicHistoryId = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, basicHistory));
+
+ // Create a basic bookmark entry
+ ContentValues basicBookmark = createBookmark(TITLE_2, URL_2, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long basicBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, basicBookmark));
+
+ // Create a history entry and bookmark entry with the same URL to
+ // represent a visited bookmark
+ ContentValues combinedHistory = createHistoryEntry(TITLE_3_HISTORY, URL_3, VISITS, LAST_VISITED);
+ long combinedHistoryId = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory));
+
+
+ ContentValues combinedBookmark = createBookmark(TITLE_3_BOOKMARK, URL_3, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long combinedBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark));
+
+ ContentValues combinedBookmark2 = createBookmark(TITLE_3_BOOKMARK2, URL_3, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long combinedBookmarkId2 = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark2));
+
+ // Create a bookmark folder to make sure it _doesn't_ show up in the results
+ ContentValues folderBookmark = createBookmark("", "", mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "tags", "description", "keyword");
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, folderBookmark);
+
+ // Sort entries by url so we can check them individually
+ final Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, BrowserContract.Combined.URL);
+
+ try {
+ mAsserter.is(c.getCount(), 3, "3 combined entries found");
+
+ // First combined entry is basic history entry
+ mAsserter.is(c.moveToFirst(), true, "Found basic history entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L,
+ "Combined _id column should always be 0");
+ // TODO: Should we change BrowserProvider to make this return -1, not 0?
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), 0L,
+ "Bookmark id should be 0 for basic history entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), basicHistoryId,
+ "Basic history entry has correct history id");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)), TITLE_1,
+ "Basic history entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_1,
+ "Basic history entry has correct url");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS,
+ "Basic history entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), LAST_VISITED,
+ "Basic history entry has correct last visit time");
+
+ // Second combined entry is basic bookmark entry
+ mAsserter.is(c.moveToNext(), true, "Found basic bookmark entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L,
+ "Combined _id column should always be 0");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), basicBookmarkId,
+ "Basic bookmark entry has correct bookmark id");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), -1L,
+ "History id should be -1 for basic bookmark entry");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)), TITLE_2,
+ "Basic bookmark entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_2,
+ "Basic bookmark entry has correct url");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), -1,
+ "Visits should be -1 for basic bookmark entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), -1L,
+ "Basic entry has correct last visit time");
+
+ // Third combined entry is a combined history/bookmark entry
+ mAsserter.is(c.moveToNext(), true, "Found third combined entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L,
+ "Combined _id column should always be 0");
+ // The bookmark data (bookmark_id and title) associated with the combined entry is non-deterministic,
+ // it might end up with data coming from any of the matching bookmark entries.
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)) == combinedBookmarkId ||
+ c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)) == combinedBookmarkId2, true,
+ "Combined entry has correct bookmark id");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)).equals(TITLE_3_BOOKMARK) ||
+ c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)).equals(TITLE_3_BOOKMARK2), true,
+ "Combined entry has title corresponding to bookmark entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), combinedHistoryId,
+ "Combined entry has correct history id");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_3,
+ "Combined entry has correct url");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS,
+ "Combined entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), LAST_VISITED,
+ "Combined entry has correct last visit time");
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private class TestCombinedViewDisplay extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE_1 = "Test Page 1";
+ final String TITLE_2 = "Test Page 2";
+ final String TITLE_3_HISTORY = "Test Page 3 (History Entry)";
+ final String TITLE_3_BOOKMARK = "Test Page 3 (Bookmark Entry)";
+
+ final String URL_1 = "http://example.com";
+ final String URL_2 = "http://example.org";
+ final String URL_3 = "http://examples2.com";
+
+ final int VISITS = 10;
+ final long LAST_VISITED = System.currentTimeMillis();
+
+ // Create a basic history entry
+ ContentValues basicHistory = createHistoryEntry(TITLE_1, URL_1, VISITS, LAST_VISITED);
+ ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, basicHistory));
+
+ // Create a basic bookmark entry
+ ContentValues basicBookmark = createBookmark(TITLE_2, URL_2, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, basicBookmark);
+
+ // Create a history entry and bookmark entry with the same URL to
+ // represent a visited bookmark
+ ContentValues combinedHistory = createHistoryEntry(TITLE_3_HISTORY, URL_3, VISITS, LAST_VISITED);
+ mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory);
+
+ ContentValues combinedBookmark = createBookmark(TITLE_3_BOOKMARK, URL_3, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark);
+
+ final Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
+ try {
+ mAsserter.is(c.getCount(), 3, "3 combined entries found");
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private class TestCombinedViewWithDeletedBookmark extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE = "Test Page 1";
+ final String URL = "http://example.com";
+ final int VISITS = 10;
+ final long LAST_VISITED = System.currentTimeMillis();
+
+ // Create a combined history entry
+ ContentValues combinedHistory = createHistoryEntry(TITLE, URL, VISITS, LAST_VISITED);
+ mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory);
+
+ // Create a combined bookmark entry
+ ContentValues combinedBookmark = createBookmark(TITLE, URL, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long combinedBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark));
+
+ Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
+ mAsserter.is(c.getCount(), 1, "1 combined entry found");
+
+ mAsserter.is(c.moveToFirst(), true, "Found combined entry with bookmark id");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), combinedBookmarkId,
+ "Bookmark id should be set correctly on combined entry");
+
+ int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(combinedBookmarkId) });
+
+ mAsserter.is((deleted == 1), true, "Inserted combined bookmark was deleted");
+ c.close();
+
+ c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
+ mAsserter.is(c.getCount(), 1, "1 combined entry found");
+
+ mAsserter.is(c.moveToFirst(), true, "Found combined entry without bookmark id");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), 0L,
+ "Bookmark id should not be set to removed bookmark id");
+ c.close();
+ }
+ }
+
+ /*
+ * Verify that insert, update, delete, and bulkInsert operations
+ * notify the ambient content resolver. Each operation calls the
+ * content resolver notifyChange method synchronously, so it is
+ * okay to test sequentially.
+ */
+ private class TestBrowserProviderNotifications extends TestCase {
+ public static final String LOGTAG = "TestBPNotifications";
+
+ protected void ensureOnlyChangeNotifiedStartsWith(Uri expectedUri, String operation) {
+ if (expectedUri == null) {
+ throw new IllegalArgumentException("expectedUri must not be null");
+ }
+
+ if (mResolver.notifyChangeList.size() != 1) {
+ // Log to help post-mortem debugging
+ Log.w(LOGTAG, "after operation, notifyChangeList = " + mResolver.notifyChangeList);
+ }
+
+ mAsserter.is((long) mResolver.notifyChangeList.size(),
+ 1L,
+ "Content observer was notified exactly once by " + operation);
+
+ Uri uri = mResolver.notifyChangeList.poll();
+
+ mAsserter.isnot(uri,
+ null,
+ "Notification from " + operation + " was valid");
+
+ mAsserter.ok(uri.toString().startsWith(expectedUri.toString()),
+ "Content observer was notified exactly once by " + operation,
+ uri.toString() + " starts with expected prefix " + expectedUri);
+ }
+
+ @Override
+ public void test() throws Exception {
+ // Insert
+ final ContentValues h = createOneHistoryEntry();
+
+ mResolver.notifyChangeList.clear();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ mAsserter.isnot(id,
+ -1L,
+ "Inserted item has valid id");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "insert");
+
+ // Update
+ mResolver.notifyChangeList.clear();
+ h.put(BrowserContract.History.TITLE, "http://newexample.com");
+
+ long numUpdated = mProvider.update(BrowserContract.History.CONTENT_URI, h,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is(numUpdated,
+ 1L,
+ "Correct number of items are updated");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "update");
+
+ // Delete
+ mResolver.notifyChangeList.clear();
+ long numDeleted = mProvider.delete(BrowserContract.History.CONTENT_URI, null, null);
+
+ mAsserter.is(numDeleted,
+ 1L,
+ "Correct number of items are deleted");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "delete");
+
+ // Bulk insert
+ final ContentValues[] hs = new ContentValues[] { createOneHistoryEntry() };
+
+ mResolver.notifyChangeList.clear();
+ long numBulkInserted = mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, hs);
+
+ mAsserter.is(numBulkInserted,
+ 1L,
+ "Correct number of items are bulkInserted");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "bulkInsert");
+ }
+ }
+
+ /**
+ * Assert that the provided cursor has the expected number of rows,
+ * closing the cursor afterwards.
+ */
+ private void assertCountIsAndClose(Cursor c, int expectedCount, String message) {
+ try {
+ mAsserter.is(c.getCount(), expectedCount, message);
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java
new file mode 100644
index 000000000..d8fc793fc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java
@@ -0,0 +1,69 @@
+/* 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 android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test for browser search visibility.
+ * Sends queries from url bar input and verifies that browser search
+ * visibility is correct.
+ */
+public class testBrowserSearchVisibility extends BaseTest {
+ public void testSearchSuggestions() {
+ blockForGeckoReady();
+
+ focusUrlBar();
+
+ // search should not be visible when editing mode starts
+ assertBrowserSearchVisibility(false);
+
+ mActions.sendKeys("a");
+
+ // search should be visible when entry is not empty
+ assertBrowserSearchVisibility(true);
+
+ mActions.sendKeys("b");
+
+ // search continues to be visible when more text is added
+ assertBrowserSearchVisibility(true);
+
+ mActions.sendKeyCode(KeyEvent.KEYCODE_DEL);
+
+ // search continues to be visible when not all text is deleted
+ assertBrowserSearchVisibility(true);
+
+ mActions.sendKeyCode(KeyEvent.KEYCODE_DEL);
+
+ // search should not be visible, entry is empty now
+ assertBrowserSearchVisibility(false);
+ }
+
+ private void assertBrowserSearchVisibility(final boolean isVisible) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ final Fragment browserSearch = getBrowserSearch();
+
+ // The fragment should not be present at all. Testing if the
+ // fragment is present but has no defined view is not a valid
+ // state.
+ if (browserSearch == null)
+ return !isVisible;
+
+ final View v = browserSearch.getView();
+ if (isVisible && v != null && v.getVisibility() == View.VISIBLE)
+ return true;
+
+ return false;
+ }
+ }, 5000);
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java
new file mode 100644
index 000000000..fe3c047a3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java
@@ -0,0 +1,31 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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 org.mozilla.gecko.Telemetry;
+
+public class testBug1217581 extends BaseTest {
+ // Take arbitrary histogram names used by Fennec.
+ private static final String TEST_HISTOGRAM_NAME = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED";
+ private static final String TEST_KEYED_HISTOGRAM_NAME = "FX_MIGRATION_ERRORS";
+ private static final String TEST_KEY_NAME = "testBug1217581";
+
+
+ public void testBug1217581() {
+ blockForGeckoReady();
+
+ mAsserter.ok(true, "Checking that adding to a keyed histogram then adding to a normal histogram does not cause a crash.", "");
+ Telemetry.addToKeyedHistogram(TEST_KEYED_HISTOGRAM_NAME, TEST_KEY_NAME, 1);
+ Telemetry.addToHistogram(TEST_HISTOGRAM_NAME, 1);
+ mAsserter.ok(true, "Adding to a keyed histogram then to a normal histogram was a success!", "");
+
+ mAsserter.ok(true, "Checking that adding to a normal histogram then adding to a keyed histogram does not cause a crash.", "");
+ Telemetry.addToHistogram(TEST_HISTOGRAM_NAME, 1);
+ Telemetry.addToKeyedHistogram(TEST_KEYED_HISTOGRAM_NAME, TEST_KEY_NAME, 1);
+ mAsserter.ok(true, "Adding to a normal histogram then to a keyed histogram was a success!", "");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java
new file mode 100644
index 000000000..fc538b5bf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java
@@ -0,0 +1,61 @@
+/* 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 org.json.JSONObject;
+
+public class testCheck2 extends PixelTest {
+ @Override
+ protected Type getTestType() {
+ return Type.TALOS;
+ }
+
+ public void testCheck2() {
+ String url = getAbsoluteUrl("/startup_test/fennecmark/cnn/cnn.com/index.html");
+
+ // Enable double-tap zooming
+ setPreferenceAndWaitForChange("browser.ui.zoom.force-user-scalable", true);
+
+ blockForGeckoReady();
+ loadAndPaint(url);
+
+ mDriver.setupScrollHandling();
+
+ /*
+ * for this test, we load the timecube page, and replay a recorded sequence of events
+ * that is a user panning/zooming around the page. specific things in the sequence
+ * include:
+ * - scroll on one axis followed by scroll on another axis
+ * - pinch zoom (in and out)
+ * - double-tap zoom (in and out)
+ * - multi-fling panning with different velocities on each fling
+ *
+ * this checkerboarding metric is going to be more of a "functional" style test than
+ * a "unit" style test; i.e. it covers a little bit of a lot of things to measure
+ * overall performance, but doesn't really allow identifying which part is slow.
+ */
+
+ MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(),
+ mDriver.getGeckoWidth(), mDriver.getGeckoHeight());
+
+ float completeness = 0.0f;
+ mDriver.startCheckerboardRecording();
+ // replay the events
+ try {
+ mer.replayEvents(getAsset("testcheck2-motionevents"));
+ // give it some time to draw any final frames
+ Thread.sleep(1000);
+ completeness = mDriver.stopCheckerboardRecording();
+ } catch (Exception e) {
+ e.printStackTrace();
+ mAsserter.ok(false, "Exception while replaying events", e.toString());
+ }
+
+ mAsserter.dumpLog("__start_report" + completeness + "__end_report");
+ System.out.println("Completeness score: " + completeness);
+ long msecs = System.currentTimeMillis();
+ mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java
new file mode 100644
index 000000000..28915bdbc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java
@@ -0,0 +1,61 @@
+/* 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 org.json.JSONObject;
+
+public class testCheck3 extends PixelTest {
+ @Override
+ protected Type getTestType() {
+ return Type.TALOS;
+ }
+
+ public void testCheck3() {
+ String url = getAbsoluteUrl("/facebook.com/www.facebook.com/barackobama.html");
+
+ // Enable double-tap zooming
+ setPreferenceAndWaitForChange("browser.ui.zoom.force-user-scalable", true);
+
+ blockForGeckoReady();
+ loadAndPaint(url);
+
+ mDriver.setupScrollHandling();
+
+ /*
+ * for this test, we load the timecube page, and replay a recorded sequence of events
+ * that is a user panning/zooming around the page. specific things in the sequence
+ * include:
+ * - scroll on one axis followed by scroll on another axis
+ * - pinch zoom (in and out)
+ * - double-tap zoom (in and out)
+ * - multi-fling panning with different velocities on each fling
+ *
+ * this checkerboarding metric is going to be more of a "functional" style test than
+ * a "unit" style test; i.e. it covers a little bit of a lot of things to measure
+ * overall performance, but doesn't really allow identifying which part is slow.
+ */
+
+ MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(),
+ mDriver.getGeckoWidth(), mDriver.getGeckoHeight());
+
+ float completeness = 0.0f;
+ mDriver.startCheckerboardRecording();
+ // replay the events
+ try {
+ mer.replayEvents(getAsset("testcheck2-motionevents"));
+ // give it some time to draw any final frames
+ Thread.sleep(1000);
+ completeness = mDriver.stopCheckerboardRecording();
+ } catch (Exception e) {
+ e.printStackTrace();
+ mAsserter.ok(false, "Exception while replaying events", e.toString());
+ }
+
+ mAsserter.dumpLog("__start_report" + completeness + "__end_report");
+ System.out.println("Completeness score: " + completeness);
+ long msecs = System.currentTimeMillis();
+ mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java
new file mode 100644
index 000000000..700c1c255
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java
@@ -0,0 +1,70 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import org.mozilla.gecko.db.DBUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class testDBUtils extends BaseTest {
+ public void testDBUtils() throws IOException {
+ final File cacheDir = getInstrumentation().getContext().getCacheDir();
+ final File dbFile = File.createTempFile("testDBUtils", ".db", cacheDir);
+ final SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
+ try {
+ mAsserter.ok(db != null, "Created DB.", null);
+ db.execSQL("CREATE TABLE foo (x INTEGER NOT NULL DEFAULT 0, y TEXT)");
+ final ContentValues v = new ContentValues();
+ v.put("x", 5);
+ v.put("y", "a");
+ db.insert("foo", null, v);
+ v.put("x", 2);
+ v.putNull("y");
+ db.insert("foo", null, v);
+ v.put("x", 3);
+ v.put("y", "z");
+ db.insert("foo", null, v);
+
+ DBUtils.UpdateOperation[] ops = {DBUtils.UpdateOperation.BITWISE_OR, DBUtils.UpdateOperation.ASSIGN};
+ ContentValues[] values = {new ContentValues(), new ContentValues()};
+ values[0].put("x", 0xff);
+ values[1].put("y", "hello");
+
+ final int updated = DBUtils.updateArrays(db, "foo", values, ops, "x >= 3", null);
+
+ mAsserter.ok(updated == 2, "Updated two rows.", null);
+ final Cursor out = db.query("foo", new String[]{"x", "y"}, null, null, null, null, "x");
+ try {
+ mAsserter.ok(out.moveToNext(), "Has first result.", null);
+ mAsserter.ok(2 == out.getInt(0), "1: First column was untouched.", null);
+ mAsserter.ok(out.isNull(1), "1: Second column was untouched.", null);
+
+ mAsserter.ok(out.moveToNext(), "Has second result.", null);
+ mAsserter.ok((0xff | 3) == out.getInt(0), "2: First column was ORed correctly.", null);
+ mAsserter.ok("hello".equals(out.getString(1)), "2: Second column was assigned correctly.", null);
+
+ mAsserter.ok(out.moveToNext(), "Has third result.", null);
+ mAsserter.ok((0xff | 5) == out.getInt(0), "3: First column was ORed correctly.", null);
+ mAsserter.ok("hello".equals(out.getString(1)), "3: Second column was assigned correctly.", null);
+
+ mAsserter.ok(!out.moveToNext(), "No more results.", null);
+ } finally {
+ out.close();
+ }
+
+ } finally {
+ try {
+ db.close();
+ } catch (Exception e) {
+ }
+ dbFile.delete();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java
new file mode 100644
index 000000000..4cc08cc5c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java
@@ -0,0 +1,556 @@
+/* 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.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Locale;
+import java.util.jar.JarInputStream;
+import java.util.NoSuchElementException;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.BrowserLocaleManager;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.distribution.ReferrerDescriptor;
+import org.mozilla.gecko.distribution.ReferrerReceiver;
+import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+/**
+ * Tests distribution customization.
+ * mock-package.zip should contain the following directory structure:
+ *
+ * distribution/
+ * preferences.json
+ * bookmarks.json
+ * searchplugins/
+ * common/
+ * engine.xml
+ * suggestedsites/
+ * locales/
+ * en-US/
+ * suggestedsites.json
+ * extensions/
+ * distribution.test@mozilla.org.xpi
+ */
+public class testDistribution extends ContentProviderTest {
+ private static final String CLASS_REFERRER_RECEIVER = "org.mozilla.gecko.distribution.ReferrerReceiver";
+ private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
+ private static final int WAIT_TIMEOUT_MSEC = 10000;
+ public static final String LOGTAG = "GeckoTestDistribution";
+
+ public static class TestableDistribution extends Distribution {
+ @Override
+ protected JarInputStream fetchDistribution(URI uri,
+ HttpURLConnection connection) throws IOException {
+ Log.i(LOGTAG, "Not downloading: this is a test.");
+ return null;
+ }
+
+ public TestableDistribution(Context context) {
+ super(context);
+ }
+
+ public void go() {
+ doInit();
+ }
+
+ public static void clearReferrerDescriptorForTesting() {
+ referrer = null;
+ }
+
+ public static ReferrerDescriptor getReferrerDescriptorForTesting() {
+ return referrer;
+ }
+ }
+
+ private static final String MOCK_PACKAGE = "mock-package.zip";
+ private static final int PREF_REQUEST_ID = 0x7357;
+
+ private Activity mActivity;
+
+ /**
+ * This is a hack.
+ *
+ * Startup results in us writing prefs -- we fetch the Distribution, which
+ * caches its state. Our tests try to wipe those prefs, but apparently
+ * sometimes race with startup, which leads to us not getting one of our
+ * expected messages. The test fails.
+ *
+ * This hack waits for any existing background tasks -- such as the one that
+ * writes prefs -- to finish before we begin the test.
+ */
+ private void waitForBackgroundHappiness() {
+ final Object signal = new Object();
+ final Runnable done = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (signal) {
+ signal.notify();
+ }
+ }
+ };
+ synchronized (signal) {
+ ThreadUtils.postToBackgroundThread(done);
+ try {
+ signal.wait();
+ } catch (InterruptedException e) {
+ mAsserter.ok(false, "InterruptedException waiting on background thread.", e.toString());
+ }
+ }
+ mAsserter.dumpLog("Background task completed. Proceeding.");
+ }
+
+ public void testDistribution() throws Exception {
+ mActivity = getActivity();
+
+ String mockPackagePath = getMockPackagePath();
+
+ // Wait for any startup-related background distribution shenanigans to
+ // finish. This reduces the chance of us racing with startup pref writes.
+ waitForBackgroundHappiness();
+
+ // Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests
+ clearDistributionPref();
+ clearDistributionFromDataData();
+
+ setTestLocale("en-US");
+ try {
+ initDistribution(mockPackagePath);
+ } catch(NoSuchElementException e) {
+ // TODO: determine why this exception is intermittently thrown
+ Log.w(LOGTAG, "NoSuchElementException on first initDistribution -- will retry");
+ mSolo.sleep(4000);
+ initDistribution(mockPackagePath);
+ }
+ checkPreferences();
+ checkAndroidPreferences();
+ checkLocalizedPreferences("en-US");
+ checkSearchPlugin();
+ checkAddon();
+
+ // Pre-clear distribution pref, and run es-MX localized preferences Test
+ clearDistributionPref();
+ clearDistributionFromDataData();
+ setTestLocale("es-MX");
+ initDistribution(mockPackagePath);
+ checkLocalizedPreferences("es-MX");
+
+ // Test the (stubbed) download interaction.
+ setTestLocale("en-US");
+ clearDistributionPref();
+ clearDistributionFromDataData();
+ doTestValidReferrerIntent();
+
+ clearDistributionPref();
+ clearDistributionFromDataData();
+ doTestInvalidReferrerIntent();
+ }
+
+ private void setOSLocale(Locale locale) {
+ Locale.setDefault(locale);
+ BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(mActivity), locale);
+ }
+
+ private abstract class ExpectNoDistributionCallback implements Distribution.ReadyCallback {
+ @Override
+ public void distributionFound(final Distribution distribution) {
+ mAsserter.ok(false, "No distributionFound.", "Wasn't expecting a distribution!");
+ synchronized (distribution) {
+ distribution.notifyAll();
+ }
+ }
+
+ @Override
+ public void distributionArrivedLate(final Distribution distribution) {
+ mAsserter.ok(false, "No distributionArrivedLate.", "Wasn't expecting a late distribution!");
+ }
+ }
+
+ private void doReferrerTest(String ref, final TestableDistribution distribution, final Distribution.ReadyCallback distributionReady) throws InterruptedException {
+ final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
+ intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
+ intent.putExtra("referrer", ref);
+
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(LOGTAG, "Test received " + intent.getAction());
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ distribution.addOnDistributionReadyCallback(distributionReady);
+ distribution.go();
+ }
+ });
+ }
+ };
+
+ IntentFilter intentFilter = new IntentFilter(ReferrerReceiver.ACTION_REFERRER_RECEIVED);
+ final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mActivity);
+ localBroadcastManager.registerReceiver(receiver, intentFilter);
+
+ Log.i(LOGTAG, "Broadcasting referrer intent.");
+ try {
+ mActivity.sendBroadcast(intent, null);
+ synchronized (distribution) {
+ distribution.wait(WAIT_TIMEOUT_MSEC);
+ }
+ } finally {
+ localBroadcastManager.unregisterReceiver(receiver);
+ }
+ }
+
+ public void doTestValidReferrerIntent() throws Exception {
+ // Equivalent to
+ // am broadcast -a com.android.vending.INSTALL_REFERRER \
+ // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
+ // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"
+ final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution";
+ final TestableDistribution distribution = new TestableDistribution(mActivity);
+ final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
+ @Override
+ public void distributionNotFound() {
+ Log.i(LOGTAG, "Test told distribution processing is done.");
+ mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline.");
+ ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
+ mAsserter.dumpLog("Referrer was " + referrerValue);
+ mAsserter.is(referrerValue.content, "testcontent", "Referrer content");
+ mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium");
+ mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign");
+ synchronized (distribution) {
+ distribution.notifyAll();
+ }
+ }
+ };
+
+ doReferrerTest(ref, distribution, distributionReady);
+ }
+
+ /**
+ * Test processing if the campaign isn't "distribution". The intent shouldn't
+ * result in a download, and won't be saved as the temporary referrer,
+ * even if we *do* include it in a Campaign:Set message.
+ */
+ public void doTestInvalidReferrerIntent() throws Exception {
+ // Equivalent to
+ // am broadcast -a com.android.vending.INSTALL_REFERRER \
+ // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
+ // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"
+ final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname";
+ final TestableDistribution distribution = new TestableDistribution(mActivity);
+ final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
+ @Override
+ public void distributionNotFound() {
+ mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong.");
+ ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
+ mAsserter.is(referrerValue, null, "No referrer.");
+ synchronized (distribution) {
+ distribution.notifyAll();
+ }
+ }
+ };
+
+ doReferrerTest(ref, distribution, distributionReady);
+ }
+
+ // Initialize the distribution from the mock package.
+ private Distribution initDistribution(String aPackagePath) {
+ // Call Distribution.init with the mock package.
+ Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK");
+ Distribution dist = Distribution.init(mActivity, aPackagePath, "prefs-" + System.currentTimeMillis());
+ distributionSetExpecter.blockForEvent();
+ distributionSetExpecter.unregisterListener();
+ DistroSharedPrefsImport.importPreferences(mActivity, dist);
+ return dist;
+ }
+
+ // Test distribution and preferences values stored in preferences.json
+ private void checkPreferences() {
+ String prefID = "distribution.id";
+ String prefAbout = "distribution.about";
+ String prefVersion = "distribution.version";
+ String prefTestBoolean = "distribution.test.boolean";
+ String prefTestString = "distribution.test.string";
+ String prefTestInt = "distribution.test.int";
+
+ try {
+ final String[] prefNames = { prefID,
+ prefAbout,
+ prefVersion,
+ prefTestBoolean,
+ prefTestString,
+ prefTestInt };
+
+ final JSONArray preferences = getPrefs(prefNames);
+ for (int i = 0; i < preferences.length(); i++) {
+ JSONObject pref = (JSONObject) preferences.get(i);
+ String name = pref.getString("name");
+
+ if (name.equals(prefID)) {
+ mAsserter.is(pref.getString("value"), "test-partner", "check " + prefID);
+ } else if (name.equals(prefAbout)) {
+ mAsserter.is(pref.getString("value"), "Test Partner", "check " + prefAbout);
+ } else if (name.equals(prefVersion)) {
+ mAsserter.is(pref.getInt("value"), 1, "check " + prefVersion);
+ } else if (name.equals(prefTestBoolean)) {
+ mAsserter.is(pref.getBoolean("value"), true, "check " + prefTestBoolean);
+ } else if (name.equals(prefTestString)) {
+ mAsserter.is(pref.getString("value"), "test", "check " + prefTestString);
+ } else if (name.equals(prefTestInt)) {
+ mAsserter.is(pref.getInt("value"), 5, "check " + prefTestInt);
+ }
+ }
+
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting preferences", e.toString());
+ }
+ }
+
+ private void checkAndroidPreferences() {
+ final SharedPreferences sharedPreferences = GeckoSharedPrefs.forProfile(getActivity());
+ String prefTestBoolean = "android.distribution.test.boolean";
+ String prefTestString = "android.distribution.test.string";
+ String prefTestInt = "android.distribution.test.int";
+ String prefTestLong = "android.distribution.test.long";
+
+ final String[] prefNames = { prefTestBoolean,
+ prefTestString,
+ prefTestInt,
+ prefTestLong };
+
+ try {
+ for (String name : prefNames) {
+ if (name.equals(prefTestBoolean)) {
+ mAsserter.is(sharedPreferences.getBoolean(GeckoPreferences.NON_PREF_PREFIX + name, false), true, "check " + prefTestBoolean);
+ } else if (name.equals(prefTestString)) {
+ mAsserter.is(sharedPreferences.getString(GeckoPreferences.NON_PREF_PREFIX + name, ""), "test", "check " + prefTestString);
+ } else if (name.equals(prefTestInt)) {
+ mAsserter.is(sharedPreferences.getInt(GeckoPreferences.NON_PREF_PREFIX + name, 0), 1, "check " + prefTestInt);
+ } else if (name.equals(prefTestLong)) {
+ mAsserter.is(sharedPreferences.getLong(GeckoPreferences.NON_PREF_PREFIX + name, 0), 2147483648l, "check " + prefTestLong);
+ }
+ }
+ } catch (ClassCastException e) {
+ mAsserter.ok(false, "exception getting preferences", e.toString());
+ }
+ }
+
+ private void checkSearchPlugin() {
+ Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data");
+ mActions.sendGeckoEvent("SearchEngines:GetVisible", null);
+
+ try {
+ JSONObject data = new JSONObject(eventExpecter.blockForEventData());
+ eventExpecter.unregisterListener();
+ JSONArray searchEngines = data.getJSONArray("searchEngines");
+ boolean foundEngine = false;
+ for (int i = 0; i < searchEngines.length(); i++) {
+ JSONObject engine = (JSONObject) searchEngines.get(i);
+ String name = engine.getString("name");
+ if (name.equals("Test search engine")) {
+ foundEngine = true;
+ break;
+ }
+ }
+ mAsserter.ok(foundEngine, "check search plugin", "found test search plugin");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting search plugins", e.toString());
+ }
+ }
+
+ private void checkAddon() {
+ try {
+ final String[] prefNames = { "distribution.test.addonEnabled" };
+ final JSONArray preferences = getPrefs(prefNames);
+ final JSONObject pref = (JSONObject) preferences.get(0);
+ mAsserter.is(pref.getBoolean("value"), true, "check distribution add-on is enabled");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting preferences", e.toString());
+ }
+ }
+
+ private JSONArray getPrefs(String[] prefNames) throws JSONException {
+ final JSONArray result = new JSONArray();
+
+ mActions.getPrefs(prefNames, new Actions.PrefHandlerBase() {
+ private void addItem(String pref, Object value) {
+ try {
+ final JSONObject item = new JSONObject();
+ item.put("name", pref).put("value", value);
+ result.put(item);
+ } catch (final JSONException e) {
+ mAsserter.ok(false, "exception getting prefs", e.toString());
+ }
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean value) {
+ addItem(pref, value);
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, int value) {
+ addItem(pref, value);
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, String value) {
+ addItem(pref, value);
+ }
+ }).waitForFinish();
+
+ return result;
+ }
+
+ // Sets the distribution locale preference for the test.
+ private void setTestLocale(String locale) {
+ BrowserLocaleManager.getInstance().setSelectedLocale(mActivity, locale);
+ }
+
+ // Test localized distribution and preferences values stored in preferences.json
+ private void checkLocalizedPreferences(final String aLocale) {
+ final String prefAbout = "distribution.about";
+ final String prefLocalizeable = "distribution.test.localizeable";
+ final String prefLocalizeableOverride = "distribution.test.localizeable-override";
+ final String[] prefNames = { prefAbout, prefLocalizeable, prefLocalizeableOverride };
+
+ mActions.getPrefs(prefNames, new Actions.PrefHandlerBase() {
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String name, String value) {
+ if (name.equals(prefAbout)) {
+ if (aLocale.equals("en-US")) {
+ mAsserter.is(value, "Test Partner", "check " + prefAbout);
+ } else if (aLocale.equals("es-MX")) {
+ mAsserter.is(value, "Afiliado de Prueba", "check " + prefAbout);
+ }
+ } else if (name.equals(prefLocalizeable)) {
+ if (aLocale.equals("en-US")) {
+ mAsserter.is(value, "http://test.org/en-US/en-US/", "check " + prefLocalizeable);
+ } else if (aLocale.equals("es-MX")) {
+ mAsserter.is(value, "http://test.org/es-MX/es-MX/", "check " + prefLocalizeable);
+ }
+ } else if (name.equals(prefLocalizeableOverride)) {
+ if (aLocale.equals("en-US")) {
+ mAsserter.is(value, "http://cheese.com", "check " + prefLocalizeableOverride);
+ } else if (aLocale.equals("es-MX")) {
+ mAsserter.is(value, "http://test.org/es-MX/", "check " + prefLocalizeableOverride);
+ }
+ } else {
+ // Raise exception.
+ super.prefValue(name, value);
+ }
+ }
+ }).waitForFinish();
+ }
+
+ // Copies the mock package to the data directory and returns the file path to it.
+ private String getMockPackagePath() {
+ String mockPackagePath = "";
+
+ try {
+ InputStream inStream = getAsset(MOCK_PACKAGE);
+ File dataDir = new File(mActivity.getApplicationInfo().dataDir);
+ File outFile = new File(dataDir, MOCK_PACKAGE);
+
+ OutputStream outStream = new FileOutputStream(outFile);
+ int b;
+ while ((b = inStream.read()) != -1) {
+ outStream.write(b);
+ }
+ inStream.close();
+ outStream.close();
+
+ mockPackagePath = outFile.getPath();
+
+ } catch (Exception e) {
+ mAsserter.ok(false, "exception copying mock distribution package to data directory", e.toString());
+ }
+
+ return mockPackagePath;
+ }
+
+ /**
+ * Clears the distribution pref to return distribution state to STATE_UNKNOWN,
+ * and wipes the in-memory referrer pigeonhole.
+ */
+ private void clearDistributionPref() {
+ mAsserter.dumpLog("Clearing distribution pref.");
+ SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE);
+ String keyName = mActivity.getPackageName() + ".distribution_state";
+ settings.edit().remove(keyName).commit();
+ TestableDistribution.clearReferrerDescriptorForTesting();
+ }
+
+ /**
+ * Clears any distribution found in /data/data.
+ */
+ private void clearDistributionFromDataData() throws Exception {
+ File dataDir = new File(mActivity.getApplicationInfo().dataDir);
+
+ // Recursively delete distribution files that Distribution.init copied to data directory.
+ File distDir = new File(dataDir, "distribution");
+ if (distDir.exists()) {
+ mAsserter.dumpLog("Clearing distribution from " + distDir.getAbsolutePath());
+ delete(distDir);
+ } else {
+ mAsserter.dumpLog("No distribution to clear from " + distDir.getAbsolutePath());
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ // TODO: Set up the content provider after setting the distribution.
+ super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
+ }
+
+ private void delete(File file) throws Exception {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ for (File f : files) {
+ delete(f);
+ }
+ }
+ mAsserter.ok(file.delete(), "clean up distribution files", "deleted " + file.getPath());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ File dataDir = new File(mActivity.getApplicationInfo().dataDir);
+
+ // Delete mock package from data directory.
+ File mockPackage = new File(dataDir, MOCK_PACKAGE);
+ mAsserter.ok(mockPackage.delete(), "clean up mock package", "deleted " + mockPackage.getPath());
+
+ clearDistributionFromDataData();
+ clearDistributionPref();
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
new file mode 100644
index 000000000..2c3feb3a8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
@@ -0,0 +1,205 @@
+/* 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 android.widget.CheckBox;
+import android.view.View;
+import com.robotium.solo.Condition;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+
+/* This test will test if doorhangers are displayed and dismissed
+ The test will test:
+ * geolocation doorhangers - sharing and not sharing the location dismisses the doorhanger
+ * opening a new tab hides the doorhanger
+ * offline storage permission doorhangers - allowing and not allowing offline storage dismisses the doorhanger
+ * Password Manager doorhangers - Remember and Not Now options dismiss the doorhanger
+*/
+public class testDoorHanger extends BaseTest {
+ private boolean offlineAllowedByDefault = true;
+
+ public void testDoorHanger() {
+ String GEO_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_GEOLOCATION_URL);
+ String BLANK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String OFFLINE_STORAGE_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_OFFLINE_STORAGE_URL);
+
+ blockForGeckoReady();
+
+ // Test geolocation notification
+ loadUrlAndWait(GEO_URL);
+ waitForText(mStringHelper.GEO_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), true, "Geolocation doorhanger has been displayed");
+
+ // Test "Share" button hides the notification
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.GEO_ALLOW);
+ waitForTextDismissed(mStringHelper.GEO_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), false, "Geolocation doorhanger has been hidden when allowing share");
+
+ // Re-trigger geolocation notification
+ loadUrlAndWait(GEO_URL);
+ waitForText(mStringHelper.GEO_MESSAGE);
+
+ // Test "Don't share" button hides the notification
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.GEO_DENY);
+ waitForTextDismissed(mStringHelper.GEO_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), false, "Geolocation doorhanger has been hidden when denying share");
+
+ /* FIXME: disabled on fig - bug 880060 (for some reason this fails because of some raciness)
+ // Re-trigger geolocation notification
+ loadUrlAndWait(GEO_URL);
+ waitForText(GEO_MESSAGE);
+
+ // Add a new tab
+ addTab(BLANK_URL);
+
+ // Make sure doorhanger is hidden
+ mAsserter.is(mSolo.searchText(GEO_MESSAGE), false, "Geolocation doorhanger notification is hidden when opening a new tab");
+ */
+
+ // Save offline-allow-by-default preferences first
+ mActions.getPrefs(new String[] { "offline-apps.allow_by_default" },
+ new Actions.PrefHandlerBase() {
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean value) {
+ mAsserter.is(pref, "offline-apps.allow_by_default", "Expecting correct pref name");
+ offlineAllowedByDefault = value;
+ }
+ }).waitForFinish();
+
+ setPreferenceAndWaitForChange("offline-apps.allow_by_default", false);
+
+ // Load offline storage page
+ loadUrlAndWait(OFFLINE_STORAGE_URL);
+ waitForText(mStringHelper.OFFLINE_MESSAGE);
+
+ // Test doorhanger dismissed when tapping "Don't share"
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.OFFLINE_DENY);
+ waitForTextDismissed(mStringHelper.OFFLINE_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when denying storage");
+
+ // Load offline storage page
+ loadUrlAndWait(OFFLINE_STORAGE_URL);
+ waitForText(mStringHelper.OFFLINE_MESSAGE);
+
+ // Test doorhanger dismissed when tapping "Allow" and is not displayed again
+ mSolo.clickOnButton(mStringHelper.OFFLINE_ALLOW);
+ waitForTextDismissed(mStringHelper.OFFLINE_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when allowing storage");
+ loadUrlAndWait(OFFLINE_STORAGE_URL);
+ mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger is no longer triggered");
+
+ // Revert offline setting
+ setPreferenceAndWaitForChange("offline-apps.allow_by_default", offlineAllowedByDefault);
+
+ // Load new login page
+ loadUrlAndWait(getAbsoluteUrl(mStringHelper.ROBOCOP_LOGIN_01_URL));
+ waitForText(mStringHelper.LOGIN_MESSAGE);
+
+ // Test doorhanger is dismissed when tapping "Remember".
+ mSolo.clickOnButton(mStringHelper.LOGIN_ALLOW);
+ waitForTextDismissed(mStringHelper.LOGIN_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when allowing saving password");
+
+ // Load login page
+ loadUrlAndWait(getAbsoluteUrl(mStringHelper.ROBOCOP_LOGIN_02_URL));
+ waitForText(mStringHelper.LOGIN_MESSAGE);
+
+ // Test doorhanger is dismissed when tapping "Never".
+ mSolo.clickOnButton(mStringHelper.LOGIN_DENY);
+ waitForTextDismissed(mStringHelper.LOGIN_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when denying saving password");
+
+ testPopupBlocking();
+ }
+
+ private void testPopupBlocking() {
+ String POPUP_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_POPUP_URL);
+
+ setPreferenceAndWaitForChange("dom.disable_open_during_load", true);
+
+ // Load page with popup
+ loadUrlAndWait(POPUP_URL);
+ waitForText(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed");
+
+ // Wait for the popup to be shown.
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.POPUP_ALLOW);
+ waitForTextDismissed(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup allowed");
+
+ try {
+ final JSONObject data = new JSONObject(tabEventExpecter.blockForEventData());
+
+ // Check to make sure the popup window was opened.
+ mAsserter.is("data:text/plain;charset=utf-8,a", data.getString("uri"), "Checking popup URL");
+
+ // Close the popup window.
+ closeTab(data.getInt("tabID"));
+
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting event data", e.toString());
+ }
+ tabEventExpecter.unregisterListener();
+
+ // Load page with popup
+ loadUrlAndWait(POPUP_URL);
+ waitForText(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed");
+
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.POPUP_DENY);
+ waitForTextDismissed(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup denied");
+
+ // Check that we're on the same page to verify that the popup was not shown.
+ verifyUrl(POPUP_URL);
+
+ setPreferenceAndWaitForChange("dom.disable_open_during_load", false);
+ }
+
+ // wait for a CheckBox view that is clickable
+ private void waitForCheckBox() {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ for (CheckBox view : mSolo.getCurrentViews(CheckBox.class)) {
+ // checking isClickable alone is not sufficient --
+ // intermittent "cannot click" errors persist unless
+ // additional checks are used
+ if (view.isClickable() &&
+ view.getVisibility() == View.VISIBLE &&
+ view.getWidth() > 0 &&
+ view.getHeight() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ }
+
+ // wait until the specified text is *not* displayed
+ private void waitForTextDismissed(final String text) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !mSolo.searchText(text);
+ }
+ }, MAX_WAIT_MS);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java
new file mode 100644
index 000000000..ad40459d5
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java
@@ -0,0 +1,450 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.os.Bundle;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of EventDispatcher,
+ * including associated NativeJSObject objects.
+ */
+public class testEventDispatcher extends JavascriptBridgeTest
+ implements BundleEventListener, GeckoEventListener, NativeEventListener {
+
+ private static final String TEST_JS = "testEventDispatcher.js";
+ private static final String GECKO_EVENT = "Robocop:TestGeckoEvent";
+ private static final String GECKO_RESPONSE_EVENT = "Robocop:TestGeckoResponse";
+ private static final String NATIVE_EVENT = "Robocop:TestNativeEvent";
+ private static final String NATIVE_RESPONSE_EVENT = "Robocop:TestNativeResponse";
+ private static final String NATIVE_EXCEPTION_EVENT = "Robocop:TestNativeException";
+ private static final String UI_EVENT = "Robocop:TestUIEvent";
+ private static final String UI_RESPONSE_EVENT = "Robocop:TestUIResponse";
+ private static final String BACKGROUND_EVENT = "Robocop:TestBackgroundEvent";
+ private static final String BACKGROUND_RESPONSE_EVENT = "Robocop:TestBackgrondResponse";
+
+ private static final long WAIT_FOR_BUNDLE_EVENT_TIMEOUT_MILLIS = 20000; // 20 seconds
+
+ private NativeJSObject savedMessage;
+
+ private boolean handledGeckoEvent;
+ private boolean handledNativeEvent;
+ private boolean handledAsyncEvent;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(
+ (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT);
+ EventDispatcher.getInstance().registerGeckoThreadListener(
+ (NativeEventListener) this,
+ NATIVE_EVENT, NATIVE_RESPONSE_EVENT, NATIVE_EXCEPTION_EVENT);
+ EventDispatcher.getInstance().registerUiThreadListener(
+ this, UI_EVENT, UI_RESPONSE_EVENT);
+ EventDispatcher.getInstance().registerBackgroundThreadListener(
+ this, BACKGROUND_EVENT, BACKGROUND_RESPONSE_EVENT);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(
+ (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT);
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(
+ (NativeEventListener) this,
+ NATIVE_EVENT, NATIVE_RESPONSE_EVENT, NATIVE_EXCEPTION_EVENT);
+ EventDispatcher.getInstance().unregisterUiThreadListener(
+ this, UI_EVENT, UI_RESPONSE_EVENT);
+ EventDispatcher.getInstance().unregisterBackgroundThreadListener(
+ this, BACKGROUND_EVENT, BACKGROUND_RESPONSE_EVENT);
+
+ super.tearDown();
+ }
+
+ private synchronized void waitForAsyncEvent() {
+ final long startTime = System.nanoTime();
+ while (!handledAsyncEvent) {
+ if (System.nanoTime() - startTime
+ >= WAIT_FOR_BUNDLE_EVENT_TIMEOUT_MILLIS * 1e6 /* ns per ms */) {
+ fFail("Should have completed event before timeout");
+ }
+ try {
+ wait(1000); // Wait for 1 second at a time.
+ } catch (final InterruptedException e) {
+ // Attempt waiting again.
+ }
+ }
+ handledAsyncEvent = false;
+ }
+
+ private synchronized void notifyAsyncEvent() {
+ handledAsyncEvent = true;
+ notifyAll();
+ }
+
+ public void testEventDispatcher() {
+ blockForReadyAndLoadJS(TEST_JS);
+
+ getJS().syncCall("send_test_message", GECKO_EVENT);
+ fAssertTrue("Should have handled Gecko event synchronously", handledGeckoEvent);
+
+ getJS().syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "success");
+ getJS().syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "error");
+
+ getJS().syncCall("send_test_message", NATIVE_EVENT);
+ fAssertTrue("Should have handled native event synchronously", handledNativeEvent);
+
+ getJS().syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "success");
+ getJS().syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "error");
+
+ getJS().syncCall("send_test_message", NATIVE_EXCEPTION_EVENT);
+
+ getJS().syncCall("send_test_message", UI_EVENT);
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", UI_RESPONSE_EVENT, "success");
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", UI_RESPONSE_EVENT, "error");
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_test_message", BACKGROUND_EVENT);
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", BACKGROUND_RESPONSE_EVENT, "success");
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", BACKGROUND_RESPONSE_EVENT, "error");
+ waitForAsyncEvent();
+
+ getJS().syncCall("finish_test");
+ }
+
+ @Override
+ public void handleMessage(final String event, final Bundle message,
+ final EventCallback callback) {
+
+ if (UI_EVENT.equals(event) || UI_RESPONSE_EVENT.equals(event)) {
+ fAssertTrue("UI event should be on UI thread", ThreadUtils.isOnUiThread());
+
+ } else if (BACKGROUND_EVENT.equals(event) || BACKGROUND_RESPONSE_EVENT.equals(event)) {
+ fAssertTrue("Background event should be on background thread",
+ ThreadUtils.isOnBackgroundThread());
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+
+ if (UI_EVENT.equals(event) || BACKGROUND_EVENT.equals(event)) {
+ checkBundle(message);
+ checkBundle(message.getBundle("object"));
+
+ } else if (UI_RESPONSE_EVENT.equals(event) || BACKGROUND_RESPONSE_EVENT.equals(event)) {
+ final String response = message.getString("response");
+ if ("success".equals(response)) {
+ callback.sendSuccess(response);
+ } else if ("error".equals(response)) {
+ callback.sendError(response);
+ } else {
+ fFail("Response type should be valid: " + response);
+ }
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+
+ notifyAsyncEvent();
+ }
+
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ ThreadUtils.assertOnGeckoThread();
+
+ try {
+ if (GECKO_EVENT.equals(event)) {
+ checkJSONObject(message);
+ checkJSONObject(message.getJSONObject("object"));
+ handledGeckoEvent = true;
+
+ } else if (GECKO_RESPONSE_EVENT.equals(event)) {
+ final String response = message.getString("response");
+ if ("success".equals(response)) {
+ EventDispatcher.sendResponse(message, response);
+ } else if ("error".equals(response)) {
+ EventDispatcher.sendError(message, response);
+ } else {
+ fFail("Response type should be valid: " + response);
+ }
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+ } catch (final JSONException e) {
+ fFail(e.toString());
+ }
+ }
+
+ @Override
+ public void handleMessage(final String event, final NativeJSObject message,
+ final EventCallback callback) {
+ ThreadUtils.assertOnGeckoThread();
+
+ if (NATIVE_EVENT.equals(event)) {
+ checkNativeJSObject(message);
+ checkNativeJSObject(message.getObject("object"));
+ fAssertNotSame("optObject returns existent value",
+ null, message.optObject("object", null));
+ fAssertSame("optObject returns fallback value if nonexistent",
+ null, message.optObject("nonexistent_object", null));
+
+ final NativeJSObject[] objectArray = message.getObjectArray("objectArray");
+ fAssertNotNull("Native object array should exist", objectArray);
+ fAssertEquals("Native object array has correct length", 2, objectArray.length);
+ fAssertSame("Native object array index 0 has correct value", null, objectArray[0]);
+ fAssertNotSame("Native object array index 1 has correct value", null, objectArray[1]);
+ checkNativeJSObject(objectArray[1]);
+ fAssertNotSame("optObjectArray returns existent value",
+ null, message.optObjectArray("objectArray", null));
+ fAssertSame("optObjectArray returns fallback value if nonexistent",
+ null, message.optObjectArray("nonexistent_objectArray", null));
+
+ final Bundle bundle = message.toBundle();
+ checkBundle(bundle);
+ checkBundle(bundle.getBundle("object"));
+ fAssertNotSame("optBundle returns property value if it exists",
+ null, message.optBundle("object", null));
+ fAssertSame("optBundle returns fallback value if property does not exist",
+ null, message.optBundle("nonexistent_object", null));
+
+ final Bundle[] bundleArray = message.getBundleArray("objectArray");
+ fAssertNotNull("Native bundle array should exist", bundleArray);
+ fAssertEquals("Native bundle array has correct length", 2, bundleArray.length);
+ fAssertSame("Native bundle array index 0 has correct value", null, bundleArray[0]);
+ fAssertNotSame("Native bundle array index 1 has correct value", null, bundleArray[1]);
+ checkBundle(bundleArray[1]);
+ fAssertNotSame("optBundleArray returns existent value",
+ null, message.optBundleArray("objectArray", null));
+ fAssertSame("optBundleArray returns fallback value if nonexistent",
+ null, message.optBundleArray("nonexistent_objectArray", null));
+
+ handledNativeEvent = true;
+
+ } else if (NATIVE_RESPONSE_EVENT.equals(event)) {
+ final String response = message.getString("response");
+ if ("success".equals(response)) {
+ callback.sendSuccess(response);
+ } else if ("error".equals(response)) {
+ callback.sendError(response);
+ } else {
+ fFail("Response type should be valid: " + response);
+ }
+
+ // Save this message for post-disposal check.
+ savedMessage = message;
+
+ } else if (NATIVE_EXCEPTION_EVENT.equals(event)) {
+ // Make sure we throw the right exceptions.
+ try {
+ message.getString(null);
+ fFail("null property name should throw IllegalArgumentException");
+ } catch (final IllegalArgumentException e) {
+ }
+
+ try {
+ message.getString("nonexistent_string");
+ fFail("Nonexistent property name should throw InvalidPropertyException");
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ }
+
+ try {
+ message.getString("int");
+ fFail("Wrong property type should throw InvalidPropertyException");
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ }
+
+ fAssertNotSame("Should have saved a message", null, savedMessage);
+ try {
+ savedMessage.toString();
+ fFail("Using NativeJSContainer should throw after disposal");
+ } catch (final NullPointerException e) {
+ }
+
+ // Save this test for last; make sure EventDispatcher catches InvalidPropertyException.
+ message.getString("nonexistent_string");
+ fFail("EventDispatcher should catch InvalidPropertyException");
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+ }
+
+ private void checkBundle(final Bundle bundle) {
+ fAssertEquals("Bundle boolean has correct value", true, bundle.getBoolean("boolean"));
+ fAssertEquals("Bundle int has correct value", 1, bundle.getInt("int"));
+ fAssertEquals("Bundle double has correct value", 0.5, bundle.getDouble("double"));
+ fAssertEquals("Bundle string has correct value", "foo", bundle.getString("string"));
+
+ final boolean[] booleanArray = bundle.getBooleanArray("booleanArray");
+ fAssertNotNull("Bundle boolean array should exist", booleanArray);
+ fAssertEquals("Bundle boolean array has correct length", 2, booleanArray.length);
+ fAssertEquals("Bundle boolean array index 0 has correct value", false, booleanArray[0]);
+ fAssertEquals("Bundle boolean array index 1 has correct value", true, booleanArray[1]);
+
+ final int[] intArray = bundle.getIntArray("intArray");
+ fAssertNotNull("Bundle int array should exist", intArray);
+ fAssertEquals("Bundle int array has correct length", 2, intArray.length);
+ fAssertEquals("Bundle int array index 0 has correct value", 2, intArray[0]);
+ fAssertEquals("Bundle int array index 1 has correct value", 3, intArray[1]);
+
+ final double[] doubleArray = bundle.getDoubleArray("doubleArray");
+ fAssertNotNull("Bundle double array should exist", doubleArray);
+ fAssertEquals("Bundle double array has correct length", 2, doubleArray.length);
+ fAssertEquals("Bundle double array index 0 has correct value", 1.5, doubleArray[0]);
+ fAssertEquals("Bundle double array index 1 has correct value", 2.5, doubleArray[1]);
+
+ final String[] stringArray = bundle.getStringArray("stringArray");
+ fAssertNotNull("Bundle string array should exist", stringArray);
+ fAssertEquals("Bundle string array has correct length", 2, stringArray.length);
+ fAssertEquals("Bundle string array index 0 has correct value", "bar", stringArray[0]);
+ fAssertEquals("Bundle string array index 1 has correct value", "baz", stringArray[1]);
+ }
+
+ private void checkJSONObject(final JSONObject object) throws JSONException {
+ fAssertEquals("JSON boolean has correct value", true, object.getBoolean("boolean"));
+ fAssertEquals("JSON int has correct value", 1, object.getInt("int"));
+ fAssertEquals("JSON double has correct value", 0.5, object.getDouble("double"));
+ fAssertEquals("JSON string has correct value", "foo", object.getString("string"));
+
+ final JSONArray booleanArray = object.getJSONArray("booleanArray");
+ fAssertNotNull("JSON boolean array should exist", booleanArray);
+ fAssertEquals("JSON boolean array has correct length", 2, booleanArray.length());
+ fAssertEquals("JSON boolean array index 0 has correct value",
+ false, booleanArray.getBoolean(0));
+ fAssertEquals("JSON boolean array index 1 has correct value",
+ true, booleanArray.getBoolean(1));
+
+ final JSONArray intArray = object.getJSONArray("intArray");
+ fAssertNotNull("JSON int array should exist", intArray);
+ fAssertEquals("JSON int array has correct length", 2, intArray.length());
+ fAssertEquals("JSON int array index 0 has correct value",
+ 2, intArray.getInt(0));
+ fAssertEquals("JSON int array index 1 has correct value",
+ 3, intArray.getInt(1));
+
+ final JSONArray doubleArray = object.getJSONArray("doubleArray");
+ fAssertNotNull("JSON double array should exist", doubleArray);
+ fAssertEquals("JSON double array has correct length", 2, doubleArray.length());
+ fAssertEquals("JSON double array index 0 has correct value",
+ 1.5, doubleArray.getDouble(0));
+ fAssertEquals("JSON double array index 1 has correct value",
+ 2.5, doubleArray.getDouble(1));
+
+ final JSONArray stringArray = object.getJSONArray("stringArray");
+ fAssertNotNull("JSON string array should exist", stringArray);
+ fAssertEquals("JSON string array has correct length", 2, stringArray.length());
+ fAssertEquals("JSON string array index 0 has correct value",
+ "bar", stringArray.getString(0));
+ fAssertEquals("JSON string array index 1 has correct value",
+ "baz", stringArray.getString(1));
+ }
+
+ private void checkNativeJSObject(final NativeJSObject object) {
+ fAssertEquals("Native boolean has correct value",
+ true, object.getBoolean("boolean"));
+ fAssertEquals("optBoolean returns existent value",
+ true, object.optBoolean("boolean", false));
+ fAssertEquals("optBoolean returns fallback value if nonexistent",
+ false, object.optBoolean("nonexistent_boolean", false));
+
+ fAssertEquals("Native int has correct value",
+ 1, object.getInt("int"));
+ fAssertEquals("optInt returns existent value",
+ 1, object.optInt("int", 0));
+ fAssertEquals("optInt returns fallback value if nonexistent",
+ 0, object.optInt("nonexistent_int", 0));
+
+ fAssertEquals("Native double has correct value",
+ 0.5, object.getDouble("double"));
+ fAssertEquals("optDouble returns existent value",
+ 0.5, object.optDouble("double", -0.5));
+ fAssertEquals("optDouble returns fallback value if nonexistent",
+ -0.5, object.optDouble("nonexistent_double", -0.5));
+
+ fAssertEquals("Native string has correct value",
+ "foo", object.getString("string"));
+ fAssertEquals("optDouble returns existent value",
+ "foo", object.optString("string", "bar"));
+ fAssertEquals("optDouble returns fallback value if nonexistent",
+ "bar", object.optString("nonexistent_string", "bar"));
+
+ final boolean[] booleanArray = object.getBooleanArray("booleanArray");
+ fAssertNotNull("Native boolean array should exist", booleanArray);
+ fAssertEquals("Native boolean array has correct length", 2, booleanArray.length);
+ fAssertEquals("Native boolean array index 0 has correct value", false, booleanArray[0]);
+ fAssertEquals("Native boolean array index 1 has correct value", true, booleanArray[1]);
+ fAssertNotSame("optBooleanArray returns existent value",
+ null, object.optBooleanArray("booleanArray", null));
+ fAssertSame("optBooleanArray returns fallback value if nonexistent",
+ null, object.optBooleanArray("nonexistent_booleanArray", null));
+
+ final int[] intArray = object.getIntArray("intArray");
+ fAssertNotNull("Native int array should exist", intArray);
+ fAssertEquals("Native int array has correct length", 2, intArray.length);
+ fAssertEquals("Native int array index 0 has correct value", 2, intArray[0]);
+ fAssertEquals("Native int array index 1 has correct value", 3, intArray[1]);
+ fAssertNotSame("optIntArray returns existent value",
+ null, object.optIntArray("intArray", null));
+ fAssertSame("optIntArray returns fallback value if nonexistent",
+ null, object.optIntArray("nonexistent_intArray", null));
+
+ final double[] doubleArray = object.getDoubleArray("doubleArray");
+ fAssertNotNull("Native double array should exist", doubleArray);
+ fAssertEquals("Native double array has correct length", 2, doubleArray.length);
+ fAssertEquals("Native double array index 0 has correct value", 1.5, doubleArray[0]);
+ fAssertEquals("Native double array index 1 has correct value", 2.5, doubleArray[1]);
+ fAssertNotSame("optDoubleArray returns existent value",
+ null, object.optDoubleArray("doubleArray", null));
+ fAssertSame("optDoubleArray returns fallback value if nonexistent",
+ null, object.optDoubleArray("nonexistent_doubleArray", null));
+
+ final String[] stringArray = object.getStringArray("stringArray");
+ fAssertNotNull("Native string array should exist", stringArray);
+ fAssertEquals("Native string array has correct length", 2, stringArray.length);
+ fAssertEquals("Native string array index 0 has correct value", "bar", stringArray[0]);
+ fAssertEquals("Native string array index 1 has correct value", "baz", stringArray[1]);
+ fAssertNotSame("optStringArray returns existent value",
+ null, object.optStringArray("stringArray", null));
+ fAssertSame("optStringArray returns fallback value if nonexistent",
+ null, object.optStringArray("nonexistent_stringArray", null));
+
+ fAssertEquals("Native has(null) is false", false, object.has("null"));
+ fAssertEquals("Native has(emptyString) is true", true, object.has("emptyString"));
+
+ fAssertEquals("Native optBoolean returns fallback value if null",
+ true, object.optBoolean("null", true));
+ fAssertEquals("Native optInt returns fallback value if null",
+ 42, object.optInt("null", 42));
+ fAssertEquals("Native optDouble returns fallback value if null",
+ -3.1415926535, object.optDouble("null", -3.1415926535));
+ fAssertEquals("Native optString returns fallback value if null",
+ "baz", object.optString("null", "baz"));
+
+ fAssertNotEquals("Native optString does not return fallback value if emptyString",
+ "baz", object.optString("emptyString", "baz"));
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
new file mode 100644
index 000000000..c613eca8f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
@@ -0,0 +1,52 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class testFilePicker extends JavascriptTest implements GeckoEventListener {
+ private static final String TEST_FILENAME = "/mnt/sdcard/my-favorite-martian.png";
+
+ public testFilePicker() {
+ super("testFilePicker.js");
+ }
+
+ @Override
+ public void handleMessage(String event, final JSONObject message) {
+ // We handle the FilePicker message here so we can send back hard coded file information. We
+ // don't want to try to emulate "picking" a file using the Android intent chooser.
+ if (event.equals("FilePicker:Show")) {
+ try {
+ message.put("file", TEST_FILENAME);
+ } catch (JSONException ex) {
+ fFail("Can't add filename to message " + TEST_FILENAME);
+ }
+
+ mActions.sendGeckoEvent("FilePicker:Result", message.toString());
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "FilePicker:Show");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "FilePicker:Show");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java
new file mode 100644
index 000000000..3c57b864a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java
@@ -0,0 +1,133 @@
+/* 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.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.PrivateTab;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.TabsProvider;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.database.Cursor;
+
+/**
+ * Tests that local tabs are filtered prior to upload.
+ * - create a set of tabs and persists them through TabsAccessor.
+ * - verifies that tabs are filtered by querying.
+ */
+public class testFilterOpenTab extends ContentProviderTest {
+ private static final String[] TABS_PROJECTION_COLUMNS = new String[] {
+ BrowserContract.Tabs.TITLE,
+ BrowserContract.Tabs.URL,
+ BrowserContract.Clients.GUID,
+ BrowserContract.Clients.NAME
+ };
+
+ private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
+
+ /**
+ * Factory function that makes new ContentProvider instances.
+ * <p>
+ * We want a fresh provider each test, so this should be invoked in
+ * <code>setUp</code> before each individual test.
+ */
+ protected static Callable<ContentProvider> sTabProviderCallable = new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new TabsProvider();
+ }
+ };
+
+ private Cursor getTabsFromLocalClient() throws Exception {
+ return mProvider.query(BrowserContract.Tabs.CONTENT_URI,
+ TABS_PROJECTION_COLUMNS,
+ LOCAL_TABS_SELECTION,
+ null,
+ null);
+ }
+
+ private Tab createTab(int id, String url, boolean external, int parentId, String title) {
+ return new Tab((Context) getActivity(), id, url, external, parentId, title);
+ }
+
+ private Tab createPrivateTab(int id, String url, boolean external, int parentId, String title) {
+ return new PrivateTab((Context) getActivity(), id, url, external, parentId, title);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sTabProviderCallable, BrowserContract.TABS_AUTHORITY, "tabs.db");
+ mTests.add(new TestInsertLocalTabs());
+ }
+
+ public void testFilterOpenTab() throws Exception {
+ blockForGeckoReady();
+
+ for (int i = 0; i < mTests.size(); i++) {
+ Runnable test = mTests.get(i);
+
+ setTestName(test.getClass().getSimpleName());
+ test.run();
+ }
+ }
+
+ private class TestInsertLocalTabs extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE1 = "Google";
+ final String URL1 = "http://www.google.com/";
+ final String TITLE2 = "Mozilla Start Page";
+ final String URL2 = "about:home";
+ final String TITLE3 = "Chrome Weave URL";
+ final String URL3 = "chrome://weave/";
+ final String TITLE4 = "What You Cache Is What You Get";
+ final String URL4 = "wyciwyg://1/test.com";
+ final String TITLE5 = "Root Folder";
+ final String URL5 = "file:///";
+
+ // Create a list of local tabs.
+ List<Tab> tabs = new ArrayList<Tab>(6);
+ Tab tab1 = createTab(1, URL1, false, 0, TITLE1);
+ Tab tab2 = createTab(2, URL2, false, 0, TITLE2);
+ Tab tab3 = createTab(3, URL3, false, 0, TITLE3);
+ Tab tab4 = createTab(4, URL4, false, 0, TITLE4);
+ Tab tab5 = createTab(5, URL5, false, 0, TITLE5);
+ Tab tab6 = createPrivateTab(6, URL1, false, 0, TITLE1);
+ tabs.add(tab1);
+ tabs.add(tab2);
+ tabs.add(tab3);
+ tabs.add(tab4);
+ tabs.add(tab5);
+ tabs.add(tab6);
+
+ // Persist the created tabs. Normally, you should be careful that you get a profile on the
+ // original thread, and do the work in a background one, but for testing we don't.
+ final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter);
+ helper.getProfileDB().getTabsAccessor().persistLocalTabs(mResolver, tabs);
+
+ // Get the persisted tab and check if urls are filtered.
+ Cursor c = getTabsFromLocalClient();
+ assertCountIsAndClose(c, 1, 1 + " tabs entries found");
+ }
+ }
+
+ /**
+ * Assert that the provided cursor has the expected number of rows,
+ * closing the cursor afterwards.
+ */
+ private void assertCountIsAndClose(Cursor c, int expectedCount, String message) {
+ try {
+ mAsserter.is(c.getCount(), expectedCount, message);
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java
new file mode 100644
index 000000000..2797fdf5b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java
@@ -0,0 +1,107 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONObject;
+
+import com.robotium.solo.Condition;
+
+public class testFindInPage extends JavascriptTest implements GeckoEventListener {
+ private static final int WAIT_FOR_CONDITION_MS = 3000;
+
+ protected Element next, close;
+
+ public testFindInPage() {
+ super("testFindInPage.js");
+ }
+
+ @Override
+ public void handleMessage(String event, final JSONObject message) {
+ if (event.equals("Test:FindInPage")) {
+ try {
+ final String text = message.getString("text");
+ final int nrOfMatches = Integer.parseInt(message.getString("nrOfMatches"));
+ findText(text, nrOfMatches);
+ } catch (Exception e) {
+ fFail("Can't extract find query from JSON");
+ }
+ }
+
+ if (event.equals("Test:CloseFindInPage")) {
+ try {
+ close.click();
+ } catch (Exception e) {
+ fFail("FindInPage prompt not opened");
+ }
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Test:FindInPage",
+ "Test:CloseFindInPage");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Test:FindInPage",
+ "Test:CloseFindInPage");
+ }
+
+ public void findText(String text, int nrOfMatches){
+ selectMenuItem(mStringHelper.FIND_IN_PAGE_LABEL);
+ close = mDriver.findElement(getActivity(), R.id.find_close);
+ boolean success = waitForCondition ( new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ next = mDriver.findElement(getActivity(), R.id.find_next);
+ if (next != null) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, WAIT_FOR_CONDITION_MS);
+ mAsserter.ok(success, "Looking for the next search match button in the Find in Page UI", "Found the next match button");
+
+ // TODO: Find a better way to wait and then enter the text
+ // Without the sleep this seems to work but the actions are not updated in the UI
+ mSolo.sleep(500);
+
+ mActions.sendKeys(text);
+ mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
+
+ // Advance a few matches to scroll the page
+ for (int i=1;i < nrOfMatches;i++) {
+ success = waitForCondition ( new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ if (next.click()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, WAIT_FOR_CONDITION_MS);
+ mSolo.sleep(500); // TODO: Find a better way to wait here because waitForCondition is not enough
+ mAsserter.ok(success, "Checking if the next button was clicked", "button was clicked");
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java
new file mode 100644
index 000000000..e173a8c16
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java
@@ -0,0 +1,52 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+/**
+ * Basic fling correctness test.
+ * - Loads a page and verifies it draws
+ * - Drags page upwards by 200 pixels to get ready for a fling
+ * - Fling the page downwards so we get back to the top and verify.
+ */
+public class testFlingCorrectness extends PixelTest {
+ public void testFlingCorrectness() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+
+ blockForGeckoReady();
+
+ // load page and check we're at 0,0
+ loadAndVerifyBoxes(url);
+
+ // drag page upwards by 200 pixels (use two drags instead of one in case
+ // the screen size is small)
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ meh.dragSync(10, 150, 10, 50);
+ meh.dragSync(10, 150, 10, 50);
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 200);
+ } finally {
+ painted.close();
+ }
+
+ // now fling page downwards using a 100-pixel drag but a velocity of 15px/sec, so that
+ // we scroll the full 200 pixels back to the top of the page
+ paintExpecter = mActions.expectPaint();
+ meh.flingSync(10, 50, 10, 150, 15);
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 0);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java
new file mode 100644
index 000000000..40968b9be
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java
@@ -0,0 +1,104 @@
+/* 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 org.mozilla.gecko.db.BrowserContract.FormHistory;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * A basic form history contentprovider test.
+ * - inserts an element in form history when it is not yet set up
+ * - inserts an element in form history
+ * - updates an element in form history
+ * - deletes an element in form history
+ */
+public class testFormHistory extends BaseTest {
+ private static final String DB_NAME = "formhistory.sqlite";
+
+ public void testFormHistory() {
+ Context context = (Context)getActivity();
+ ContentResolver cr = context.getContentResolver();
+ ContentValues[] cvs = new ContentValues[1];
+ cvs[0] = new ContentValues();
+
+ blockForGeckoReady();
+
+ Uri formHistoryUri;
+ Uri insertUri;
+ Uri expectedUri;
+ int numUpdated;
+ int numDeleted;
+
+ cvs[0].put("fieldname", "fieldname");
+ cvs[0].put("value", "value");
+ cvs[0].put("timesUsed", "0");
+ cvs[0].put("guid", "guid");
+
+ // Attempt to insert into the db
+ formHistoryUri = FormHistory.CONTENT_URI;
+ Uri.Builder builder = formHistoryUri.buildUpon();
+ formHistoryUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ insertUri = cr.insert(formHistoryUri, cvs[0]);
+ expectedUri = formHistoryUri.buildUpon().appendPath("1").build();
+ mAsserter.is(expectedUri.toString(), insertUri.toString(), "Insert returned correct uri");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ cvs[0].put("fieldname", "fieldname2");
+ cvs[0].putNull("guid");
+
+ numUpdated = cr.update(formHistoryUri, cvs[0], null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ numDeleted = cr.delete(formHistoryUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ cvs = new ContentValues[0];
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ cvs = new ContentValues[1];
+ cvs[0] = new ContentValues();
+ cvs[0].put("fieldname", "fieldname");
+ cvs[0].put("value", "value");
+ cvs[0].put("timesUsed", "0");
+ cvs[0].putNull("guid");
+
+ insertUri = cr.insert(formHistoryUri, cvs[0]);
+ expectedUri = formHistoryUri.buildUpon().appendPath("1").build();
+ mAsserter.is(expectedUri.toString(), insertUri.toString(), "Insert returned correct uri");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ cvs[0].put("guid", "guid");
+
+ numUpdated = cr.update(formHistoryUri, cvs[0], null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ numDeleted = cr.delete(formHistoryUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ cvs = new ContentValues[0];
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // remove the entire signons.sqlite file
+ File profile = new File(mProfile);
+ File db = new File(profile, "formhistory.sqlite");
+ if (db.delete()) {
+ mAsserter.dumpLog("tearDown deleted "+db.toString());
+ } else {
+ mAsserter.dumpLog("tearDown did not delete "+db.toString());
+ }
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java
new file mode 100644
index 000000000..eb9a705be
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java
@@ -0,0 +1,295 @@
+/* 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.util.Enumeration;
+import java.util.Hashtable;
+
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoProfileDirectories;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+/**
+ * This patch tests GeckoProfile. It has unit tests for basic getting and removing of profiles, as well as
+ * some guest mode tests. It does not test locking and unlocking profiles yet. It does not test the file management in GeckoProfile.
+ */
+
+public class testGeckoProfile extends PixelTest {
+ private final String TEST_PROFILE_NAME = "testProfile";
+ private File mozDir;
+ public void testGeckoProfile() {
+ blockForGeckoReady();
+
+ try {
+ mozDir = GeckoProfileDirectories.getMozillaDirectory(getActivity());
+ } catch(Exception ex) {
+ // If we can't get the moz dir, something is wrong. Just fail quickly.
+ mAsserter.ok(false, "Couldn't get moz dir", ex.toString());
+ return;
+ }
+
+ checkProfileCreationDeletion();
+ checkGuestProfile();
+ }
+
+ // This getter just passes an activity. Passing null should throw.
+ private void checkDefaultGetter() {
+ // "Default" is a custom profile set up by the test harness.
+ mAsserter.info("Test using the test profile", GeckoProfile.CUSTOM_PROFILE);
+ GeckoProfile profile = GeckoProfile.get(getActivity());
+ verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, ((GeckoApp) getActivity()).getProfile().getDir(), true);
+
+ try {
+ profile = GeckoProfile.get(null);
+ mAsserter.ok(false, "Passing a null context should throw", profile.toString());
+ } catch(Exception ex) {
+ mAsserter.ok(true, "Passing a null context should throw", ex.toString());
+ }
+ }
+
+ // Test get(Context, String) methods
+ private void checkNamedGetter(String name) {
+ mAsserter.info("Test using a named profile", name);
+ GeckoProfile profile = GeckoProfile.get(getActivity(), name);
+ if (name != null) {
+ verifyProfile(profile, name, findDir(name), false);
+ removeProfile(profile, true);
+ } else {
+ // Passing in null for a profile name, should get you the default
+ File defaultProfile = ((GeckoApp) getActivity()).getProfile().getDir();
+ verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, defaultProfile, true);
+ }
+ }
+
+ // Test get(Context, String, String) methods
+ private void checkNameAndPathGetter(String name, boolean createBefore) {
+ if (name == null) {
+ checkNameAndPathGetter(name, null, createBefore);
+ } else {
+ checkNameAndPathGetter(name, name + "_FORCED_DIR", createBefore);
+ }
+ }
+
+ // Test get(Context, String, String) methods
+ private void checkNameAndPathGetter(String name, String path, boolean createBefore) {
+ mAsserter.info("Test using a named profile and path", name + ", " + path);
+ checkNameAndDirGetter(name, /* useFile */ false, path, /* file */ null, createBefore);
+ }
+
+ private void checkNameAndFileGetter(String name, boolean createBefore) {
+ if (name == null) {
+ checkNameAndFileGetter(name, null, createBefore);
+ } else {
+ checkNameAndFileGetter(name, new File(mozDir, name + "_FORCED_DIR"), createBefore);
+ }
+ }
+
+ private void checkNameAndFileGetter(String name, File f, boolean createBefore) {
+ mAsserter.info("Test using a named profile and File", name + ", " + f);
+ checkNameAndDirGetter(name, /* useFile */ true, /* path */ null, f, createBefore);
+ }
+
+ private void checkNameAndDirGetter(final String name, final boolean useFile,
+ String path, final File file,
+ final boolean createBefore) {
+ final File f;
+ if (useFile) {
+ f = file;
+ } else if (!TextUtils.isEmpty(path)) {
+ f = new File(mozDir, path);
+ path = f.getAbsolutePath();
+ } else {
+ f = null;
+ }
+
+ if (f != null && createBefore) {
+ // For some tests we create explicitly beforehand
+ f.mkdir();
+ }
+
+ final File testProfileDir = ((GeckoApp) getActivity()).getProfile().getDir();
+ final String expectedName = name != null ? name : GeckoProfile.CUSTOM_PROFILE;
+
+ final GeckoProfile profile;
+ if (useFile) {
+ profile = GeckoProfile.get(getActivity(), name, file);
+ } else {
+ profile = GeckoProfile.get(getActivity(), name, path);
+ }
+
+ if (name != null || f != null) {
+ // GeckoProfile will create a directory and add an ini section if f is null
+ // here. Therefore, when f is null, shouldHaveFound is false for the
+ // verifyProfile call, and inProfileIni is true for the removeProfile call.
+ verifyProfile(profile, expectedName, f, f != null);
+ removeProfile(profile, f == null);
+ if (name == null) {
+ // A side effect of calling GeckoProfile.get with null name is it changes
+ // the test profile's directory to the new directory. Restore it back.
+ GeckoProfile.get(getActivity(), null, testProfileDir);
+ mAsserter.is(GeckoProfile.get(getActivity()).getDir(), testProfileDir,
+ "Test profile should be restored");
+ }
+ } else {
+ // Passing in null for a profile name and path, should get you the default
+ verifyProfile(profile, expectedName, testProfileDir, true);
+ }
+ }
+
+ private void checkProfileCreationDeletion() {
+ // Test
+ checkDefaultGetter();
+
+ int index = 0;
+ checkNamedGetter(TEST_PROFILE_NAME + (index++)); // 0
+ checkNamedGetter(null);
+
+ // name and path
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), true);
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), false);
+ checkNameAndPathGetter(null, false);
+ // null name and path
+ checkNameAndPathGetter(null, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR", true);
+ checkNameAndPathGetter(null, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR", false);
+ // name and null path
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), null, false);
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), "", false);
+ // null name and null path
+ checkNameAndPathGetter(null, null, false);
+ checkNameAndPathGetter(null, "", false);
+
+ // name and path
+ checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), true);
+ checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), false);
+ checkNameAndFileGetter(null, false);
+ // null name and path
+ checkNameAndFileGetter(null, new File(mozDir, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR"), true);
+ checkNameAndFileGetter(null, new File(mozDir, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR"), false);
+ // name and null path
+ checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), null, false);
+ // null name and null path
+ checkNameAndFileGetter(null, null, false);
+ }
+
+ // Tests of Guest profile methods
+ private void checkGuestProfile() {
+ final File testProfileDir = ((GeckoApp) getActivity()).getProfile().getDir();
+
+ mAsserter.info("Test getting a guest profile", "");
+ GeckoProfile profile = GeckoProfile.getGuestProfile(getActivity());
+ verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, getActivity().getFileStreamPath("guest"), true);
+ mAsserter.ok(profile.inGuestMode(), "Profile is in guest mode", profile.getName());
+
+ final File dir = profile.getDir();
+ mAsserter.info("Test deleting a guest profile", "");
+ mAsserter.ok(GeckoProfile.removeProfile(getActivity(), profile), "Cleaned up unlocked guest profile", profile.getName());
+ mAsserter.ok(!dir.exists(), "Guest dir was deleted", dir.toString());
+
+ // Restore test profile directory, which was changed in the last GeckoProfile.get call.
+ GeckoProfile.get(getActivity(), null, testProfileDir);
+ mAsserter.is(GeckoProfile.get(getActivity()).getDir(), testProfileDir,
+ "Test profile should be restored");
+ }
+
+ // Runs generic tests on a profile to make sure it looks correct
+ private void verifyProfile(GeckoProfile profile, String name, File requestedDir, boolean shouldHaveFound) {
+ mAsserter.is(profile.getName(), name, "Profile name is correct");
+
+ File dir = null;
+ if (!shouldHaveFound) {
+ mAsserter.is(findDir(name), null, "Dir with name doesn't exist yet");
+
+ dir = profile.getDir();
+ mAsserter.isnot(requestedDir, dir, "Profile should not have used expectedDir");
+
+ // The used dir should be based on the name passed in.
+ requestedDir = findDir(name);
+ } else {
+ dir = profile.getDir();
+ }
+
+ mAsserter.is(dir, requestedDir, "Profile dir is correct");
+ mAsserter.ok(dir.exists(), "Profile dir exists after getting it", dir.toString());
+ }
+
+ // Tries to find a profile in profiles.ini. Makes sure its name and path match what is expected
+ private void findInProfilesIni(final String name, final File dir, final boolean shouldFind) {
+ final File mozDir;
+ try {
+ mozDir = GeckoProfileDirectories.getMozillaDirectory(getActivity());
+ } catch(Exception ex) {
+ mAsserter.ok(false, "Couldn't get moz dir", ex.toString());
+ return;
+ }
+
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozDir);
+ final Hashtable<String, INISection> sections = parser.getSections();
+
+ boolean found = false;
+ for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ String iniName = section.getStringProperty("Name");
+ if (iniName == null || !iniName.equals(name)) {
+ continue;
+ }
+
+ found = true;
+
+ String iniPath = section.getStringProperty("Path");
+ mAsserter.is(name, iniName, "Section with name found");
+ mAsserter.is(dir.getName(), iniPath, "Section has correct path");
+ }
+
+ mAsserter.is(found, shouldFind, "Found profile where expected");
+ }
+
+ // Tries to remove a profile from Gecko profile. Verifies that it's removed from profiles.ini and its directory is deleted.
+ // TODO: Reconsider profile removal. Firefox would not normally remove a
+ // profile. Outstanding tasks may still try to access files in the profile.
+ private void removeProfile(GeckoProfile profile, boolean inProfilesIni) {
+ final String name = profile.getName();
+ final File dir = profile.getDir();
+ findInProfilesIni(name, dir, inProfilesIni);
+ mAsserter.ok(dir.exists(), "Profile dir exists before removing", dir.toString());
+ mAsserter.ok(GeckoProfile.removeProfile(getActivity(), profile), "Remove was successful", name);
+ mAsserter.ok(!dir.exists(), "Profile dir was deleted when it was removed", dir.toString());
+ findInProfilesIni(name, dir, false);
+ }
+
+ // Looks for a dir whose name ends with the passed-in string.
+ private File findDir(String name) {
+ final File root;
+ try {
+ root = GeckoProfileDirectories.getMozillaDirectory(getActivity());
+ } catch(Exception ex) {
+ return null;
+ }
+
+ File[] dirs = root.listFiles();
+ for (File dir : dirs) {
+ if (dir.getName().endsWith(name)) {
+ return dir;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // Clear SharedPreferences.
+ final Context context = getInstrumentation().getContext();
+ GeckoSharedPrefs.forProfile(context).edit().clear().apply();
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java
new file mode 100644
index 000000000..ac4a9862c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java
@@ -0,0 +1,121 @@
+/* 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.util.concurrent.atomic.AtomicBoolean;
+
+import com.robotium.solo.Condition;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.tests.helpers.AssertionHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.NativeJSObject;
+
+/**
+ * Tests sending and receiving Gecko requests using the GeckoRequest API.
+ */
+public class testGeckoRequest extends JavascriptBridgeTest {
+ private static final String TEST_JS = "testGeckoRequest.js";
+ private static final String REQUEST_EVENT = "Robocop:GeckoRequest";
+ private static final String REQUEST_EXCEPTION_EVENT = "Robocop:GeckoRequestException";
+ private static final int MAX_WAIT_MS = 5000;
+
+ public void testGeckoRequest() {
+ blockForReadyAndLoadJS(TEST_JS);
+
+ // Register a listener for this request.
+ getJS().syncCall("add_request_listener", REQUEST_EVENT);
+
+ // Make sure we receive the expected response.
+ checkFooRequest();
+
+ // Try registering a second listener for this request, which should fail.
+ getJS().syncCall("add_second_request_listener", REQUEST_EVENT);
+
+ // Unregister the listener for this request.
+ getJS().syncCall("remove_request_listener", REQUEST_EVENT);
+
+ // Make sure we don't receive a response after removing the listener.
+ checkUnregisteredRequest();
+
+ // Check that we still receive a response for listeners that throw.
+ getJS().syncCall("add_exception_listener", REQUEST_EXCEPTION_EVENT);
+ checkExceptionRequest();
+ getJS().syncCall("remove_request_listener", REQUEST_EXCEPTION_EVENT);
+
+ getJS().syncCall("finish_test");
+ }
+
+ private void checkFooRequest() {
+ final AtomicBoolean responseReceived = new AtomicBoolean(false);
+ final String data = "foo";
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EVENT, data) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ // Ensure we receive the expected response from Gecko.
+ final String result = nativeJSObject.getString("result");
+ AssertionHelper.fAssertEquals("Sent and received request data", data + "bar", result);
+ responseReceived.set(true);
+ }
+ });
+
+ WaitHelper.waitFor("Received response for registered listener", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return responseReceived.get();
+ }
+ }, MAX_WAIT_MS);
+ }
+
+ private void checkExceptionRequest() {
+ final AtomicBoolean responseReceived = new AtomicBoolean(false);
+ final AtomicBoolean errorReceived = new AtomicBoolean(false);
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EXCEPTION_EVENT, null) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ responseReceived.set(true);
+ }
+
+ @Override
+ public void onError(NativeJSObject error) {
+ errorReceived.set(true);
+ }
+ });
+
+ WaitHelper.waitFor("Received error for listener with exception", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return errorReceived.get();
+ }
+ }, MAX_WAIT_MS);
+
+ AssertionHelper.fAssertTrue("onResponse not called for listener with exception", !responseReceived.get());
+ }
+
+ private void checkUnregisteredRequest() {
+ final AtomicBoolean responseReceived = new AtomicBoolean(false);
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EVENT, null) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ responseReceived.set(true);
+ }
+ });
+
+ // This check makes sure that we do *not* receive a response for an unregistered listener,
+ // meaning waitForCondition() should always time out.
+ getSolo().waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return responseReceived.get();
+ }
+ }, MAX_WAIT_MS);
+
+ AssertionHelper.fAssertTrue("Did not receive response for unregistered listener", !responseReceived.get());
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java
new file mode 100644
index 000000000..405ddef7a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java
@@ -0,0 +1,159 @@
+/* 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 org.mozilla.gecko.AppConstants;
+
+import android.widget.Spinner;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+import android.hardware.Camera;
+import android.os.Build;
+
+public class testGetUserMedia extends BaseTest {
+ private static final String LOGTAG = testGetUserMedia.class.getSimpleName();
+
+ private static final String GUM_MESSAGE = "Would you like to share your camera and microphone with";
+ private static final String GUM_ALLOW = "^Share$";
+ private static final String GUM_DENY = "^Don't Share$";
+
+ private static final String GUM_BACK_CAMERA = "Back facing camera";
+ private static final String GUM_SELECT_TAB = "Choose a tab to stream";
+
+ private static final String GUM_PAGE_TITLE = "gUM Test Page";
+ private static final String GUM_PAGE_FAILED = "failed gumtest";
+ private static final String GUM_PAGE_AUDIO = "audio gumtest";
+ private static final String GUM_PAGE_VIDEO = "video gumtest";
+ private static final String GUM_PAGE_AUDIOVIDEO = "audiovideo gumtest";
+
+ public void testGetUserMedia() {
+ // TabShare.js is disabled on release builds.
+ if (AppConstants.RELEASE_OR_BETA) {
+ mAsserter.dumpLog(LOGTAG + " is disabled on release builds: returning");
+ return;
+ }
+
+ // Only try GUM test if the device has a camera (emulation).
+ if (Camera.getNumberOfCameras() <= 0) {
+ return;
+ }
+
+ blockForGeckoReady();
+
+ final String GUM_CAMERA_URL = getAbsoluteUrl("/robocop/robocop_getusermedia2.html");
+ final String GUM_TAB_URL = getAbsoluteUrl("/robocop/robocop_getusermedia.html");
+ // Browser constraint needs HTTPS
+ final String GUM_TAB_HTTPS_URL = GUM_TAB_URL.replace("http://mochi.test:8888", "https://example.com");
+
+ // Tests on Camera page will test camera enumeration code, but
+ // the actual cameras don't seem to work on the emulators, so
+ // the enumeration is all that gets tested.
+
+ // Test GUM notification showing
+ loadUrlAndWait(GUM_CAMERA_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+ waitForSpinner();
+ // At least one camera detected
+ mAsserter.is(mSolo.searchText(GUM_BACK_CAMERA), true, "getUserMedia found a camera");
+ mSolo.clickOnButton(GUM_DENY);
+ waitForTextDismissed(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
+ verifyUrlBarTitle(GUM_CAMERA_URL);
+
+ // Cameras don't work on the testing hardware, so stream a tab
+ loadUrlAndWait(GUM_TAB_HTTPS_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+ waitForSpinner();
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
+ mAsserter.is(mSolo.searchText("MICROPHONE TO USE"), true, "Microphone selection available");
+ mAsserter.is(mSolo.searchText("Microphone 1"), true, "Microphone 1 available");
+ mSolo.clickOnText("Microphone 1");
+ waitForText("No Audio");
+ mAsserter.is(mSolo.searchText("No Audio"), true, "No 'No Audio' selection available");
+ mSolo.clickOnText("No Audio");
+ waitForTextDismissed("Microphone 1");
+ mAsserter.is(mSolo.searchText("Microphone 1"), false, "Audio selection hidden after dismissal");
+ mAsserter.is(mSolo.searchText(GUM_ALLOW), true, "Share button available after selection");
+ mSolo.clickOnButton(GUM_ALLOW);
+ waitForTextDismissed(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
+ waitForText(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed");
+ mSolo.clickOnText(GUM_PAGE_TITLE);
+ waitForTextDismissed(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden");
+ verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
+
+ // Android 2.3 testers fail because of audio issues:
+ // E/AudioRecord( 650): Unsupported configuration: sampleRate 44100, format 1, channelCount 1
+ // E/libOpenSLES( 650): android_audioRecorder_realize(0x26d7d8) error creating AudioRecord object
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return;
+ }
+
+ loadUrlAndWait(GUM_TAB_HTTPS_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+
+ waitForSpinner();
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
+ mSolo.clickOnButton(GUM_ALLOW);
+ waitForTextDismissed(GUM_MESSAGE);
+ waitForText(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed");
+ mSolo.clickOnText(GUM_PAGE_TITLE);
+ waitForTextDismissed(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden");
+ verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
+
+ loadUrlAndWait(GUM_TAB_HTTPS_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+
+ waitForSpinner();
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
+ mSolo.clickOnText(GUM_SELECT_TAB);
+ waitForText("No Video");
+ mAsserter.is(mSolo.searchText("No Video"), true, "'No video' source selection available");
+ mSolo.clickOnText("No Video");
+ waitForTextDismissed(GUM_SELECT_TAB);
+ mSolo.clickOnButton(GUM_ALLOW);
+ waitForTextDismissed(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
+ verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
+ }
+
+ // wait for a Spinner view that is clickable
+ private void waitForSpinner() {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ for (Spinner view : mSolo.getCurrentViews(Spinner.class)) {
+ if (view.isClickable() &&
+ view.getVisibility() == View.VISIBLE &&
+ view.getWidth() > 0 &&
+ view.getHeight() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ }
+
+ // wait until the specified text is *not* displayed
+ private void waitForTextDismissed(final String text) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !mSolo.searchText(text);
+ }
+ }, MAX_WAIT_MS);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java
new file mode 100644
index 000000000..1f2fbbd38
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java
@@ -0,0 +1,74 @@
+/* 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 android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import org.mozilla.gecko.home.HomePager;
+
+import com.robotium.solo.Condition;
+
+public class testHistory extends AboutHomeTest {
+ private View mFirstChild;
+
+ public void testHistory() {
+ blockForGeckoReady();
+
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String url2 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ String url3 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL);
+
+ inputAndLoadUrl(url);
+ verifyUrlBarTitle(url);
+ inputAndLoadUrl(url2);
+ verifyUrlBarTitle(url2);
+ inputAndLoadUrl(url3);
+ verifyUrlBarTitle(url3);
+
+ openAboutHomeTab(AboutHomeTabs.HISTORY);
+
+ final ListView hList = findListViewWithTag(HomePager.LIST_TAG_HISTORY);
+ mAsserter.is(waitForNonEmptyListToLoad(hList), true, "list is properly loaded");
+
+ // Click on the history item and wait for the page to load
+ // wait for the history list to be populated
+ mFirstChild = null;
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ mFirstChild = hList.getChildAt(1);
+ if (mFirstChild == null) {
+ return false;
+ }
+ if (mFirstChild instanceof android.view.ViewGroup) {
+ ViewGroup group = (ViewGroup)mFirstChild;
+ if (group.getChildCount() < 1) {
+ return false;
+ }
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View grandChild = group.getChildAt(i);
+ if (grandChild instanceof android.widget.TextView) {
+ mAsserter.ok(true, "found TextView:", ((android.widget.TextView)grandChild).getText().toString());
+ }
+ }
+ } else {
+ mAsserter.dumpLog("first child not a ViewGroup: "+mFirstChild);
+ return false;
+ }
+ return true;
+ }
+ }, MAX_WAIT_MS);
+
+ mAsserter.isnot(mFirstChild, null, "Got history item");
+ mSolo.clickOnView(mFirstChild);
+
+ // The first item here (since it was just visited) should be a "Switch to tab" item
+ // i.e. don't expect a DOMContentLoaded event
+ verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL);
+ verifyUrl(url3);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java
new file mode 100644
index 000000000..4c605f6c3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java
@@ -0,0 +1,12 @@
+/* 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;
+
+public class testHistoryService extends JavascriptTest {
+
+ public testHistoryService() {
+ super("testHistoryService.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java
new file mode 100644
index 000000000..be36ae5a0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java
@@ -0,0 +1,94 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+public class testHomeBanner extends UITest {
+
+ private static final String TEST_URL = "chrome://roboextender/content/robocop_home_banner.html";
+ private static final String TEXT = "The quick brown fox jumps over the lazy dog.";
+
+ public void testHomeBanner() {
+ GeckoHelper.blockForReady();
+
+ // Make sure the banner is not visible to start.
+ mAboutHome.assertVisible()
+ .assertBannerNotVisible();
+
+ // These test methods depend on being run in this order.
+ addBannerTest();
+
+ // Make sure the banner hides when the user starts interacting with the url bar.
+ hideOnToolbarFocusTest();
+
+ // Make sure to test dismissing the banner after everything else, since dismissing
+ // the banner will prevent it from showing up again.
+ dismissBannerTest();
+ }
+
+ /**
+ * Adds a banner message, verifies that it appears when it should, and verifies that
+ * onshown/onclick handlers are called in JS.
+ *
+ * Note: This test does not remove the message after it is done.
+ */
+ private void addBannerTest() {
+ // Load about:home and make sure the onshown handler is called.
+ Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageShown");
+ addBannerMessage();
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ eventExpecter.blockForEvent();
+
+ // Verify that the banner is visible with the correct text.
+ mAboutHome.assertBannerText(TEXT);
+
+ // Verify that the banner isn't visible after navigating away from about:home.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_FIREFOX_URL);
+ mAboutHome.assertBannerNotVisible();
+ }
+
+
+ private void hideOnToolbarFocusTest() {
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible()
+ .assertBannerVisible();
+
+ mToolbar.enterEditingMode();
+ mAboutHome.assertBannerNotVisible();
+
+ mToolbar.dismissEditingMode();
+ mAboutHome.assertBannerVisible();
+ }
+
+ /**
+ * Adds a banner message, verifies that its ondismiss handler is called in JS,
+ * and verifies that the banner is no longer shown after it is dismissed.
+ *
+ * Note: This test does not remove the message after it is done.
+ */
+ private void dismissBannerTest() {
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible();
+
+ // Test to make sure the ondismiss handler is called when the close button is clicked.
+ final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageDismissed");
+ mAboutHome.dismissBanner();
+ eventExpecter.blockForEvent();
+
+ mAboutHome.assertBannerNotVisible();
+ }
+
+ /**
+ * Loads the roboextender page to add a message to the banner.
+ */
+ private void addBannerMessage() {
+ final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageAdded");
+ NavigationHelper.enterAndLoadUrl(TEST_URL + "#addMessage");
+ eventExpecter.blockForEvent();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java
new file mode 100644
index 000000000..fbe2df82f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java
@@ -0,0 +1,118 @@
+/* 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 android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class testHomeListsProvider extends ContentProviderTest {
+ // This test does not run, so it just needs to compile. The test was
+ // disabled at the time the real Contract was removed; to leave a skeleton
+ // for a future re-implementor, we include this dummy Contract class.
+ private static class Contract {
+ public static final Uri CONTENT_URI = null;
+ public static final Uri CONTENT_FAKE_URI = null;
+
+ public static final String _ID = null;
+ public static final String PROVIDER_ID = null;
+ public static final String TITLE = null;
+ public static final String URL = null;
+ }
+
+ @SuppressWarnings("unused")
+ private void ensureEmptyDatabase() throws Exception {
+ // Delete all the list entries.
+ mProvider.delete(Contract.CONTENT_URI, null, null);
+
+ final Cursor c = mProvider.query(Contract.CONTENT_URI, null, null, null, null);
+ mAsserter.is(c.getCount(), 0, "All list entries were deleted");
+ c.close();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ // This test is disabled, so this just needs to compile.
+ super.setUp(null, null, "homelists.db");
+
+ mTests.add(new TestFakeItems());
+
+ // Disabled until database support lands
+ //mTests.add(new TestInsertItem());
+ }
+
+ public void testListsProvider() throws Exception {
+ for (int i = 0; i < mTests.size(); i++) {
+ Runnable test = mTests.get(i);
+
+ setTestName(test.getClass().getSimpleName());
+ // Disabled until database support lands
+ //ensureEmptyDatabase();
+ test.run();
+ }
+ }
+
+ abstract class Test implements Runnable {
+ @Override
+ public void run() {
+ try {
+ test();
+ } catch (Exception e) {
+ mAsserter.is(true, false, "Test " + this.getClass().getName() +
+ " threw exception: " + e);
+ }
+ }
+
+ public abstract void test() throws Exception;
+ }
+
+ class TestFakeItems extends Test {
+ @Override
+ public void test() throws Exception {
+ final long id = 1;
+ final String providerId = "fake-provider";
+ final String title = "Example";
+ final String url = "http://example.com";
+
+ final Cursor c = mProvider.query(Contract.CONTENT_FAKE_URI, null, null, null, null);
+ mAsserter.is(c.moveToFirst(), true, "Fake list item found");
+
+ mAsserter.is(c.getLong(c.getColumnIndex(Contract._ID)), id, "Fake list item has correct ID");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.PROVIDER_ID)), providerId, "Fake list item has correct provider ID");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.TITLE)), title, "Fake list item has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.URL)), url, "Fake list item has correct URL");
+
+ c.close();
+ }
+ }
+
+ class TestInsertItem extends Test {
+ @Override
+ public void test() throws Exception {
+ final String providerId = "{c77da387-4c80-0c45-9f22-70276c29b3ed}";
+ final String title = "Mozilla";
+ final String url = "https://mozilla.org";
+
+ // Insert a new list item with test values.
+ final ContentValues cv = new ContentValues();
+ cv.put(Contract.PROVIDER_ID, providerId);
+ cv.put(Contract.TITLE, title);
+ cv.put(Contract.URL, url);
+
+ final long id = ContentUris.parseId(mProvider.insert(Contract.CONTENT_URI, cv));
+
+ // Check that the item was inserted correctly.
+ final Cursor c = mProvider.query(Contract.CONTENT_URI, null, Contract._ID + " = ?", new String[] { String.valueOf(id) }, null);
+ mAsserter.is(c.moveToFirst(), true, "Inserted list item found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.PROVIDER_ID)), providerId, "Inserted list item has correct provider ID");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.TITLE)), title, "Inserted list item has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.URL)), url, "Inserted list item has correct URL");
+
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java
new file mode 100644
index 000000000..5cbbd1be9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java
@@ -0,0 +1,238 @@
+/* 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 android.graphics.Bitmap;
+
+import org.mozilla.gecko.icons.decoders.ICODecoder;
+import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
+import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+public class testICODecoder extends UITest {
+
+ private int mGolemNumIconDirEntries;
+
+ public void testICODecoder() throws IOException {
+ testMicrosoftFavicon();
+ testNvidiaFavicon();
+ testGolemFavicon();
+ testMissingHeader();
+ testCorruptIconDirectory();
+ }
+
+ /**
+ * Decode and verify a Microsoft favicon with six different sizes:
+ * 128x128, 72x72, 48x48, 32x32, 24x24, 16x16
+ * Each of the six BMPs supposedly has zero colour depth.
+ */
+ private void testMicrosoftFavicon() throws IOException {
+ byte[] icoBytes = readICO("microsoft_favicon.ico");
+ fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
+ icoBytes.length);
+ LoadFaviconResult result = decoder.decode();
+ fAssertNotNull("Expecting Microsoft favicon to not fail decoding.", result);
+
+ int largestBitmap = Integer.MAX_VALUE;
+
+ int[] possibleSizes = {16, 24, 32, 48, 72, 128};
+ for (int i = 0; i < possibleSizes.length; i++) {
+ if (possibleSizes[i] > decoder.getLargestFaviconSize()) {
+ largestBitmap = possibleSizes[i];
+
+ // Verify that all bitmaps but the smallest larger than Favicons.largestFaviconSize
+ // have been discarded.
+ for (int j = i + 1; j < possibleSizes.length; j++) {
+ Bitmap selectedBitmap = result.getBestBitmap(possibleSizes[j]);
+ fAssertNotNull("Expecting a best bitmap to be found for " +
+ possibleSizes[j] + "x" + possibleSizes[j], selectedBitmap);
+
+ fAssertEquals("Expecting best bitmap to have width " + possibleSizes[i],
+ possibleSizes[i], selectedBitmap.getWidth());
+ fAssertEquals("Expecting best bitmap to have height " + possibleSizes[i],
+ possibleSizes[i], selectedBitmap.getHeight());
+
+ // Reset the result's bitmap iterator.
+ result = decoder.decode();
+ }
+
+ break;
+ }
+ }
+
+ int[] expectedSizes = {
+ // If we request a 33x33 we should get a 48x48.
+ 33, 48,
+ // If we request a 24x24 we should get a 24x24.
+ 24, 24,
+ // If we request a 8x8 we should get a 16x16.
+ 8, 16,
+ };
+
+ for (int i = 0; i < expectedSizes.length - 1; i += 2) {
+ if (expectedSizes[i + 1] > largestBitmap) {
+ // This bitmap has been discarded.
+ continue;
+ }
+
+ Bitmap selectedBitmap = result.getBestBitmap(expectedSizes[i]);
+ fAssertNotNull("Expecting a best bitmap to have been found for " +
+ expectedSizes[i] + "x" + expectedSizes[i], selectedBitmap);
+
+ fAssertEquals("Expecting best bitmap to have width " + expectedSizes[i + 1],
+ expectedSizes[i + 1], selectedBitmap.getWidth());
+ fAssertEquals("Expecting best bitmap to have height " + expectedSizes[i + 1],
+ expectedSizes[i + 1], selectedBitmap.getHeight());
+
+ // Reset the result's bitmap iterator.
+ result = decoder.decode();
+ }
+ }
+
+ /**
+ * Decode and verify a NVIDIA favicon with three different colour depths,
+ * and three different sizes for each colour depth. All payloads are BMP.
+ */
+ private void testNvidiaFavicon() throws IOException {
+ byte[] icoBytes = readICO("nvidia_favicon.ico");
+ fAssertEquals("Expecting NVIDIA favicon to be 25214 bytes.", 25214, icoBytes.length);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
+ icoBytes.length);
+ fAssertNotNull("Expecting NVIDIA favicon to not fail decoding.", decoder.decode());
+
+ // Verify the best entry is correctly chosen for each width.
+ // We expect 32 bpp in all cases even if 32 bpp exceeds IconDirectoryEntry.maxBPP.
+ // This is okay because IconDirectoryEntry.maxBPP is a "desired bpp" not the absolute max.
+ // This was chosen because we think it gives better results to select a higher bpp and let
+ // Android downscale the bpp, rather than showing a bitmap of potentially significantly
+ // lower color depth.
+ IconDirectoryEntry[] expectedEntries = {
+ new IconDirectoryEntry(16, 16, 0, 32, 1128, 24086, false),
+ new IconDirectoryEntry(32, 32, 0, 32, 4264, 19822, false),
+ new IconDirectoryEntry(48, 48, 0, 32, 9640, 10182, false)
+ };
+
+ IconDirectoryEntry[] directory = decoder.getIconDirectory();
+ fAssertTrue("NVIDIA icon directory must contain at least one entry.", directory.length > 0);
+ for (int i = 0; i < directory.length; i++) {
+ if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) {
+ // This test-case has been discarded due to being over-sized. Next.
+ // All subsequent cases will be too.
+ fAssertTrue("At least one test-case should not have been discarded.", i > 0);
+ break;
+ }
+
+ // Verify the actual Icon Directory entry was as expected.
+ fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i],
+ 0, directory[i].compareTo(expectedEntries[i]));
+ }
+ }
+
+ /**
+ * Decode and verify a Golem.de favicon with five bitmaps: 256x256, 48x48, 32x32, 24x24, 16x16
+ * Only the 256x256 is a PNG payload. All others are BMP.
+ */
+ private void testGolemFavicon() throws IOException {
+ byte[] icoBytes = readICO("golem_favicon.ico");
+ fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
+ icoBytes.length);
+ fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode());
+
+ // Verify the five entries were correctly identified.
+ IconDirectoryEntry[] expectedEntries = {
+ new IconDirectoryEntry(16, 16, 0, 32, 1128, 39250, false),
+ new IconDirectoryEntry(24, 24, 0, 32, 2488, 37032, false),
+ new IconDirectoryEntry(32, 32, 0, 32, 4392, 32640, false),
+ new IconDirectoryEntry(48, 48, 0, 32, 9832, 22808, false),
+ new IconDirectoryEntry(256, 256, 0, 32, 22722, 86, true)
+ };
+
+ IconDirectoryEntry[] directory = decoder.getIconDirectory();
+ fAssertTrue("Golem icon directory must contain at least one entry.", directory.length > 0);
+ for (int i = 0; i < directory.length; i++) {
+ if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) {
+ // This test-case has been discarded due to being over-sized.
+ // All subsequent cases will be too.
+ fAssertTrue("At least one test-case should not have been discarded.", i > 0);
+ break;
+ }
+
+ // Verify the actual Icon Directory entry was as expected.
+ fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i],
+ 0, directory[i].compareTo(expectedEntries[i]));
+ }
+
+ // How many icon directory entries in the non-maimed favicon?
+ mGolemNumIconDirEntries = directory.length;
+ }
+
+ /**
+ * Verify that deleting the header will make decoding fail.
+ */
+ private void testMissingHeader() throws IOException {
+ byte[] icoBytes = readICO("microsoft_favicon.ico");
+ fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length);
+
+ int offsetNoHeader = ICODecoder.ICO_HEADER_LENGTH_BYTES;
+ int lenNoHeader = icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES;
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes,
+ offsetNoHeader, lenNoHeader);
+ fAssertNull("Expecting Microsoft favicon to fail decoding.", decoder.decode());
+ }
+
+ /**
+ * Verify that decoding does not fail if the number of icon directory entries is smaller than
+ * the number given in the header.
+ */
+ private void testCorruptIconDirectory() throws IOException {
+ byte[] icoBytes = readICO("golem_favicon.ico");
+ fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length);
+
+ byte[] icoMaimed = new byte[icoBytes.length - ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES];
+ // Copy the header and first four icon directory entries into icoMaimed.
+ System.arraycopy(icoBytes, 0, icoMaimed, 0,
+ ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+ // Skip the last icon directory entry.
+ System.arraycopy(icoBytes,
+ ICODecoder.ICO_HEADER_LENGTH_BYTES + 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES,
+ icoMaimed,
+ ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES,
+ icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES - 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoMaimed, 0,
+ icoMaimed.length);
+ fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode());
+ fAssertEquals("Expecting Golem favicon icon directory to contain one less bitmap.",
+ mGolemNumIconDirEntries - 1, decoder.getIconDirectory().length);
+ }
+
+ private byte[] readICO(String fileName) throws IOException {
+ String filePath = "ico_decoder_favicons" + File.separator + fileName;
+ InputStream icoStream = getInstrumentation().getContext().getAssets().open(filePath);
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream(icoStream.available());
+
+ int readByte;
+ while ((readByte = icoStream.read()) != -1) {
+ byteStream.write(readByte);
+ }
+
+ return byteStream.toByteArray();
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java
new file mode 100644
index 000000000..f9a6bcef7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java
@@ -0,0 +1,349 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.WaitHelper.waitFor;
+
+import org.mozilla.gecko.tests.components.GeckoViewComponent.InputConnectionTest;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import com.robotium.solo.Condition;
+
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Tests the proper operation of GeckoInputConnection
+ */
+public class testInputConnection extends JavascriptBridgeTest {
+
+ private static final String INITIAL_TEXT = "foo";
+
+ public void testInputConnection() throws InterruptedException {
+ GeckoHelper.blockForReady();
+
+ final String url = mStringHelper.ROBOCOP_INPUT_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ // First run tests inside the normal input field.
+ getJS().syncCall("focus_input", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the text area and rerun tests.
+ getJS().syncCall("focus_text_area", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the content editable and rerun tests.
+ getJS().syncCall("focus_content_editable", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the design mode document and rerun tests.
+ getJS().syncCall("focus_design_mode", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the resetting input field, and run tests there.
+ getJS().syncCall("focus_resetting_input", "");
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new ResettingInputConnectionTest());
+
+ // Then switch focus to the hiding input field, and run tests there.
+ getJS().syncCall("focus_hiding_input", "");
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new HidingInputConnectionTest());
+
+ getJS().syncCall("finish_test");
+ }
+
+ private class BasicInputConnectionTest extends InputConnectionTest {
+ @Override
+ public void test(final InputConnection ic, EditorInfo info) {
+ waitFor("focus change", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return INITIAL_TEXT.equals(getText(ic));
+ }
+ });
+
+ // Test setSelection
+ ic.setSelection(0, 3);
+ assertSelection("Can set selection to range", ic, 0, 3);
+ ic.setSelection(-3, 6);
+ // Test both forms of assert
+ assertTextAndSelection("Can handle invalid range", ic, INITIAL_TEXT, 0, 3);
+ ic.setSelection(3, 3);
+ assertSelectionAt("Can collapse selection", ic, 3);
+ ic.setSelection(4, 4);
+ assertTextAndSelectionAt("Can handle invalid cursor", ic, INITIAL_TEXT, 3);
+
+ // Test commitText
+ ic.commitText("", 10); // Selection past end of new text
+ assertTextAndSelectionAt("Can commit empty text", ic, "foo", 3);
+ ic.commitText("bar", 1); // Selection at end of new text
+ assertTextAndSelectionAt("Can commit text (select after)", ic, "foobar", 6);
+ ic.commitText("foo", -1); // Selection at start of new text
+ assertTextAndSelectionAt("Can commit text (select before)", ic, "foobarfoo", 5);
+
+ // Test deleteSurroundingText
+ ic.deleteSurroundingText(1, 0);
+ assertTextAndSelectionAt("Can delete text before", ic, "foobrfoo", 4);
+ ic.deleteSurroundingText(1, 1);
+ assertTextAndSelectionAt("Can delete text before/after", ic, "foofoo", 3);
+ ic.deleteSurroundingText(0, 10);
+ assertTextAndSelectionAt("Can delete text after", ic, "foo", 3);
+ ic.deleteSurroundingText(0, 0);
+ assertTextAndSelectionAt("Can delete empty text", ic, "foo", 3);
+
+ // Test setComposingText
+ ic.setComposingText("foo", 1);
+ assertTextAndSelectionAt("Can start composition", ic, "foofoo", 6);
+ ic.setComposingText("", 1);
+ assertTextAndSelectionAt("Can set empty composition", ic, "foo", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can update composition", ic, "foobar", 6);
+
+ // Test finishComposingText
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composition", ic, "foobar", 6);
+
+ // Test setComposingRegion
+ ic.setComposingRegion(0, 3);
+ assertTextAndSelectionAt("Can set composing region", ic, "foobar", 6);
+
+ ic.setComposingText("far", 1);
+ assertTextAndSelectionAt("Can set composing region text", ic, "farbar", 3);
+
+ ic.setComposingRegion(1, 4);
+ assertTextAndSelectionAt("Can set existing composing region", ic, "farbar", 3);
+
+ ic.setComposingText("rab", 3);
+ assertTextAndSelectionAt("Can set new composing region text", ic, "frabar", 6);
+
+ // Test getTextBeforeCursor
+ fAssertEquals("Can retrieve text before cursor", "bar", ic.getTextBeforeCursor(3, 0));
+
+ // Test getTextAfterCursor
+ fAssertEquals("Can retrieve text after cursor", "", ic.getTextAfterCursor(3, 0));
+
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composition", ic, "frabar", 6);
+
+ // Test sendKeyEvent
+ final KeyEvent shiftKey = new KeyEvent(KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT);
+ final KeyEvent leftKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
+ final KeyEvent tKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_T);
+
+ ic.sendKeyEvent(shiftKey);
+ ic.sendKeyEvent(leftKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(leftKey, KeyEvent.ACTION_UP));
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP));
+ assertTextAndSelection("Can select using key event", ic, "frabar", 6, 5);
+
+ ic.sendKeyEvent(tKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(tKey, KeyEvent.ACTION_UP));
+ assertTextAndSelectionAt("Can type using event", ic, "frabat", 6);
+
+ ic.deleteSurroundingText(6, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1133802, duplication when setting the same composing text more than once.
+ ic.setComposingText("foo", 1);
+ assertTextAndSelectionAt("Can set the composing text", ic, "foo", 3);
+ ic.setComposingText("foo", 1);
+ assertTextAndSelectionAt("Can set the same composing text", ic, "foo", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can set different composing text", ic, "bar", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can set the same composing text", ic, "bar", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can set the same composing text again", ic, "bar", 3);
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composing text", ic, "bar", 3);
+
+ ic.deleteSurroundingText(3, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1209465, cannot enter ideographic space character by itself (U+3000).
+ ic.commitText("\u3000", 1);
+ assertTextAndSelectionAt("Can commit ideographic space", ic, "\u3000", 1);
+
+ ic.deleteSurroundingText(1, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1051556, exception due to committing text changes during flushing.
+ ic.setComposingText("bad", 1);
+ assertTextAndSelectionAt("Can set the composing text", ic, "bad", 3);
+ getJS().asyncCall("test_reflush_changes");
+ // Wait for text change notifications to come in.
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can re-flush text changes", ic, "good", 4);
+ ic.setComposingText("done", 1);
+ assertTextAndSelectionAt("Can update composition after re-flushing", ic, "gooddone", 8);
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composing text", ic, "gooddone", 8);
+
+ ic.deleteSurroundingText(8, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1241558 - wrong selection due to ignoring selection notification.
+ ic.setComposingText("foobar", 1);
+ assertTextAndSelectionAt("Can set the composing text", ic, "foobar", 6);
+ getJS().asyncCall("test_set_selection");
+ // Wait for text change notifications to come in.
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can select after committing", ic, "foobar", 3);
+ ic.setComposingText("barfoo", 1);
+ assertTextAndSelectionAt("Can compose after selecting", ic, "barfoo", 6);
+ ic.beginBatchEdit();
+ ic.setSelection(3, 3);
+ ic.finishComposingText();
+ ic.deleteSurroundingText(1, 1);
+ ic.endBatchEdit();
+ assertTextAndSelectionAt("Can delete after committing", ic, "baoo", 2);
+
+ ic.deleteSurroundingText(2, 2);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1275371 - shift+backspace should not forward delete on Android.
+ final KeyEvent delKey = new KeyEvent(KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_DEL);
+
+ ic.beginBatchEdit();
+ ic.commitText("foo", 1);
+ ic.setSelection(1, 1);
+ ic.endBatchEdit();
+ assertTextAndSelectionAt("Can commit text", ic, "foo", 1);
+
+ ic.sendKeyEvent(shiftKey);
+ ic.sendKeyEvent(delKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(delKey, KeyEvent.ACTION_UP));
+ assertTextAndSelectionAt("Can backspace with shift+backspace", ic, "oo", 0);
+
+ ic.sendKeyEvent(delKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(delKey, KeyEvent.ACTION_UP));
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP));
+ assertTextAndSelectionAt("Cannot forward delete with shift+backspace", ic, "oo", 0);
+
+ ic.deleteSurroundingText(0, 2);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1123514 - exception due to incorrect text replacement offsets.
+ getJS().syncCall("test_bug1123514");
+ // Gecko will change text to 'abc' when we input 'b', potentially causing
+ // incorrect calculation of text replacement offsets.
+ ic.commitText("b", 1);
+ // We don't assert text here because this test only works for input/textarea,
+ // so an assertion would fail for contentEditable/designMode.
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ ic.deleteSurroundingText(2, 1);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Make sure we don't leave behind stale events for the following test.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ }
+ }
+
+ /**
+ * ResettingInputConnectionTest performs tests on the resetting input in
+ * robocop_input.html. Any test that uses the normal input should be put in
+ * BasicInputConnectionTest.
+ */
+ private class ResettingInputConnectionTest extends InputConnectionTest {
+ @Override
+ public void test(final InputConnection ic, EditorInfo info) {
+ waitFor("focus change", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return "".equals(getText(ic));
+ }
+ });
+
+ // Bug 1199658, duplication when page has JS that resets input field value.
+
+ ic.commitText("foo", 1);
+ assertTextAndSelectionAt("Can commit text (resetting)", ic, "foo", 3);
+
+ ic.setComposingRegion(0, 3);
+ // The bug appears after composition update events are processed. We only
+ // issue these events after some back-and-forth calls between the Gecko thread
+ // and the input connection thread. Therefore, to ensure these events are
+ // issued and to ensure the bug appears, we have to process all Gecko events,
+ // then all input connection events, and finally all Gecko events again.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can set composing region (resetting)", ic, "foo", 3);
+
+ ic.setComposingText("foobar", 1);
+ processGeckoEvents();
+ processInputConnectionEvents();
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can change composing text (resetting)", ic, "foobar", 6);
+
+ ic.setComposingText("baz", 1);
+ processGeckoEvents();
+ processInputConnectionEvents();
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can reset composing text (resetting)", ic, "baz", 3);
+
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composing text (resetting)", ic, "baz", 3);
+
+ ic.deleteSurroundingText(3, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Make sure we don't leave behind stale events for the following test.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ }
+ }
+
+ /**
+ * HidingInputConnectionTest performs tests on the hiding input in
+ * robocop_input.html. Any test that uses the normal input should be put in
+ * BasicInputConnectionTest.
+ */
+ private class HidingInputConnectionTest extends InputConnectionTest {
+ @Override
+ public void test(final InputConnection ic, EditorInfo info) {
+ waitFor("focus change", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return "".equals(getText(ic));
+ }
+ });
+
+ // Bug 1254629, crash when hiding input during input.
+ ic.commitText("foo", 1);
+ assertTextAndSelectionAt("Can commit text (hiding)", ic, "foo", 3);
+
+ ic.commitText("!", 1);
+ // The '!' key causes the input to hide in robocop_input.html,
+ // and there won't be a text/selection update as a result.
+ assertTextAndSelectionAt("Can handle hiding input", ic, "foo", 3);
+
+ // Make sure we don't leave behind stale events for the following test.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java
new file mode 100644
index 000000000..c12ccef98
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java
@@ -0,0 +1,136 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import android.widget.EditText;
+
+/**
+ * Basic test of text editing within the editing mode.
+ * - Enter some text, move the cursor around, and modifying some text.
+ * - Check that all edit entry text is selected after switching about:home tabs.
+ */
+public final class testInputUrlBar extends BaseTest {
+ private Element mUrlBarEditElement;
+ private EditText mUrlBarEditView;
+
+ public void testInputUrlBar() {
+ blockForGeckoReady();
+
+ startEditingMode();
+ assertUrlBarText("");
+
+ // Avoid any auto domain completion by using a prefix that matches
+ // nothing, including about: pages
+ mActions.sendKeys("zy");
+ assertUrlBarText("zy");
+
+ mActions.sendKeys("cd");
+ assertUrlBarText("zycd");
+
+ mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
+ mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
+
+ // Inserting "" should not do anything.
+ mActions.sendKeys("");
+ assertUrlBarText("zycd");
+
+ mActions.sendKeys("ef");
+ assertUrlBarText("zyefcd");
+
+ mActions.sendSpecialKey(Actions.SpecialKey.RIGHT);
+ mActions.sendKeys("gh");
+ assertUrlBarText("zyefcghd");
+
+ final EditText editText = mUrlBarEditView;
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ // Select "ef"
+ editText.setSelection(2);
+ }
+ });
+ mActions.sendKeys("op");
+ assertUrlBarText("zyopefcghd");
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ // Select "cg"
+ editText.setSelection(6, 8);
+ }
+ });
+ mActions.sendKeys("qr");
+ assertUrlBarText("zyopefqrhd");
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ // Select "op"
+ editText.setSelection(4,2);
+ }
+ });
+ mActions.sendKeys("st");
+ assertUrlBarText("zystefqrhd");
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ editText.selectAll();
+ }
+ });
+ mActions.sendKeys("uv");
+ assertUrlBarText("uv");
+
+ // Dismiss the VKB
+ mSolo.goBack();
+
+ // Dismiss editing mode
+ mSolo.goBack();
+
+ waitForText(mStringHelper.TITLE_PLACE_HOLDER);
+
+ // URL bar should have forgotten about "uv" text.
+ startEditingMode();
+ assertUrlBarText("");
+
+ int width = mDriver.getGeckoWidth() / 2;
+ int y = mDriver.getGeckoHeight() / 2;
+
+ // Slide to the right, force URL bar entry to lose input focus
+ mActions.drag(width, 0, y, y);
+
+ // Select text and replace the content
+ mSolo.clickOnView(mUrlBarEditView);
+ mActions.sendKeys("yz");
+
+ String yz = getUrlBarText();
+ mAsserter.ok("yz".equals(yz), "Is the URL bar text \"yz\"?", yz);
+ }
+
+ private void startEditingMode() {
+ focusUrlBar();
+
+ mUrlBarEditElement = mDriver.findElement(getActivity(), R.id.url_edit_text);
+ final int id = mUrlBarEditElement.getId();
+ mUrlBarEditView = (EditText) getActivity().findViewById(id);
+ }
+
+ private String getUrlBarText() {
+ final String elementText = mUrlBarEditElement.getText();
+ final String editText = mUrlBarEditView.getText().toString();
+ mAsserter.is(editText, elementText, "Does URL bar editText == elementText?");
+
+ return editText;
+ }
+
+ private void assertUrlBarText(String expectedText) {
+ String actualText = getUrlBarText();
+ mAsserter.is(actualText, expectedText, "Does URL bar actualText == expectedText?");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java
new file mode 100644
index 000000000..9310599d3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java
@@ -0,0 +1,70 @@
+/* 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.InputStream;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.GeckoJarReader;
+
+import android.content.Context;
+
+/**
+ * A basic jar reader test. Tests reading a png from fennec's apk, as well
+ * as loading some invalid jar urls.
+ */
+public class testJarReader extends BaseTest {
+ public void testJarReader() {
+ // Invalid characters are escaped.
+ final String s = GeckoJarReader.computeJarURI("some[1].apk", "something/else");
+ mAsserter.ok(!s.contains("["), "Illegal characters are escaped away.", null);
+ mAsserter.ok(!s.toLowerCase().contains("%2f"), "Path characters aren't escaped.", null);
+
+ final Context context = getInstrumentation().getTargetContext().getApplicationContext();
+ String appPath = getActivity().getApplication().getPackageResourcePath();
+ mAsserter.isnot(appPath, null, "getPackageResourcePath is non-null");
+
+ // Test reading a file from a jar url that looks correct.
+ String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+ InputStream stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.isnot(stream, null, "JarReader returned non-null for valid file in valid jar");
+
+ // Test looking for an non-existent file in a jar.
+ url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+ mAsserter.is(stream, null, "JarReader returned null for non-existent file in valid jar");
+
+ // Test looking for a file that doesn't exist in the APK.
+ url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.is(stream, null, "JarReader returned null for valid file in invalid jar file");
+
+ // Test looking for a file that doesn't exist in the APK.
+ // Bug 1174922, prefixed string / length error.
+ url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME + "BAD";
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.is(stream, null, "JarReader returned null for valid file in other invalid jar file");
+
+ // Test looking for an jar with an invalid url.
+ url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+ mAsserter.is(stream, null, "JarReader returned null for bad jar url");
+
+ // Test looking for a file that doesn't exist on disk.
+ url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.is(stream, null, "JarReader returned null for a non-existent APK");
+
+ // This test completes very quickly. If it completes too soon, the
+ // minidumps directory may not be created before the process is
+ // taken down, causing bug 722166.
+ blockForGeckoReady();
+ }
+
+ private String getData(InputStream stream) {
+ return new java.util.Scanner(stream).useDelimiter("\\A").next();
+ }
+
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java
new file mode 100644
index 000000000..724b6b4ab
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java
@@ -0,0 +1,69 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of JavascriptBridge and JavaBridge,
+ * which are used by tests for communication between Java and JS.
+ */
+public class testJavascriptBridge extends JavascriptBridgeTest {
+
+ private static final String TEST_JS = "testJavascriptBridge.js";
+
+ private boolean syncCallReceived;
+
+ public void testJavascriptBridge() {
+ blockForReadyAndLoadJS(TEST_JS);
+ getJS().syncCall("check_js_int_arg", 1);
+ }
+
+ public void checkJavaIntArg(final int int2) {
+ // Async call from JS
+ fAssertEquals("Integer argument matches", 2, int2);
+ getJS().syncCall("check_js_double_arg", 3.0D);
+ }
+
+ public void checkJavaDoubleArg(final double double4) {
+ // Async call from JS
+ fAssertEquals("Double argument matches", 4.0, double4);
+ getJS().syncCall("check_js_boolean_arg", false);
+ }
+
+ public void checkJavaBooleanArg(final boolean booltrue) {
+ // Async call from JS
+ fAssertEquals("Boolean argument matches", true, booltrue);
+ getJS().syncCall("check_js_string_arg", "foo");
+ }
+
+ public void checkJavaStringArg(final String stringbar) throws JSONException {
+ // Async call from JS
+ fAssertEquals("String argument matches", "bar", stringbar);
+ final JSONObject obj = new JSONObject();
+ obj.put("caller", "java");
+ getJS().syncCall("check_js_object_arg", (JSONObject) obj);
+ }
+
+ public void checkJavaObjectArg(final JSONObject obj) throws JSONException {
+ // Async call from JS
+ fAssertEquals("Object argument matches", "js", obj.getString("caller"));
+ getJS().syncCall("check_js_sync_call");
+ }
+
+ public void doJSSyncCall() {
+ // Sync call from JS
+ syncCallReceived = true;
+ getJS().asyncCall("respond_to_js_sync_call");
+ }
+
+ public void checkJSSyncCallReceived() {
+ fAssertTrue("Received sync call before end of test", syncCallReceived);
+ // End of test
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java
new file mode 100644
index 000000000..556ed0e07
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java
@@ -0,0 +1,37 @@
+/* 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;
+
+public class testLinkContextMenu extends ContentContextMenuTest {
+
+ // Test website strings
+ private static String LINK_PAGE_URL;
+ private static String BLANK_PAGE_URL;
+ private static final String LINK_PAGE_TITLE = "Big Link";
+
+ public void testLinkContextMenu() {
+ final String linkMenuItems [] = mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB;
+
+ blockForGeckoReady();
+
+ LINK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
+ BLANK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ loadUrlAndWait(LINK_PAGE_URL);
+ waitForText(LINK_PAGE_TITLE);
+
+ verifyContextMenuItems(linkMenuItems); // Verify context menu items are correct
+ openTabFromContextMenu(linkMenuItems[0],2); // Test the "Open in New Tab" option - expecting 2 tabs: the original and the new one
+ openTabFromContextMenu(linkMenuItems[1],2); // Test the "Open in Private Tab" option - expecting only 2 tabs in normal mode
+ verifyCopyOption(linkMenuItems[2], BLANK_PAGE_URL); // Test the "Copy Link" option
+ verifyShareOption(linkMenuItems[3], LINK_PAGE_TITLE); // Test the "Share Link" option
+ verifyBookmarkLinkOption(linkMenuItems[4], BLANK_PAGE_URL); // Test the "Bookmark Link" option
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDatabaseHelper.deleteBookmark(BLANK_PAGE_URL);
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java
new file mode 100644
index 000000000..e62bd7899
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java
@@ -0,0 +1,23 @@
+/* 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;
+
+/**
+ * A basic page load test.
+ * - loads a page
+ * - verifies it rendered properly
+ * - verifies the displayed url is correct
+ */
+public class testLoad extends PixelTest {
+ public void testLoad() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ blockForGeckoReady();
+
+ loadAndVerifyBoxes(url);
+
+ verifyUrl(url);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java
new file mode 100644
index 000000000..10dde28cd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java
@@ -0,0 +1,387 @@
+/* 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 android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.DeletedLogins;
+import org.mozilla.gecko.db.BrowserContract.Logins;
+import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts;
+import org.mozilla.gecko.db.LoginsProvider;
+
+import java.util.concurrent.Callable;
+
+import static org.mozilla.gecko.db.BrowserContract.CommonColumns._ID;
+import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
+import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS;
+import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
+
+public class testLoginsProvider extends ContentProviderTest {
+
+ private static final String DB_NAME = "browser.db";
+
+ private final TestCase[] TESTS_TO_RUN = {
+ new InsertLoginsTest(),
+ new UpdateLoginsTest(),
+ new DeleteLoginsTest(),
+ new InsertDeletedLoginsTest(),
+ new InsertDeletedLoginsFailureTest(),
+ new DisabledHostsInsertTest(),
+ new DisabledHostsInsertFailureTest(),
+ new InsertLoginsWithDefaultValuesTest(),
+ new InsertLoginsWithDuplicateGuidFailureTest(),
+ new DeleteLoginsByNonExistentGuidTest(),
+ };
+
+ /**
+ * Factory function that makes new LoginsProvider instances.
+ * <p>
+ * We want a fresh provider each test, so this should be invoked in
+ * <code>setUp</code> before each individual test.
+ */
+ private static final Callable<ContentProvider> sProviderFactory = new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new LoginsProvider();
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sProviderFactory, BrowserContract.LOGINS_AUTHORITY, DB_NAME);
+ for (TestCase test: TESTS_TO_RUN) {
+ mTests.add(test);
+ }
+ }
+
+ public void testLoginProviderTests() throws Exception {
+ for (Runnable test : mTests) {
+ final String testName = test.getClass().getSimpleName();
+ setTestName(testName);
+ ensureEmptyDatabase();
+ mAsserter.dumpLog("testLoginsProvider: Database empty - Starting " + testName + ".");
+ test.run();
+ }
+ }
+
+ /**
+ * Wipe DB.
+ */
+ private void ensureEmptyDatabase() {
+ getWritableDatabase(Logins.CONTENT_URI).delete(TABLE_LOGINS, null, null);
+ getWritableDatabase(DeletedLogins.CONTENT_URI).delete(TABLE_DELETED_LOGINS, null, null);
+ getWritableDatabase(LoginsDisabledHosts.CONTENT_URI).delete(TABLE_DISABLED_HOSTS, null, null);
+ }
+
+ private SQLiteDatabase getWritableDatabase(Uri uri) {
+ Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
+ DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider;
+ LoginsProvider loginsProvider = (LoginsProvider) delegateProvider.getTargetProvider();
+ return loginsProvider.getWritableDatabaseForTesting(testUri);
+ }
+
+ /**
+ * LoginsProvider insert logins test.
+ */
+ private class InsertLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+ Cursor cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid1" }, null);
+ verifyRowMatches(contentValues, cursor, "logins found");
+
+ // Empty ("") encrypted username and password are valid.
+ contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "", "", "guid2");
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+ cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid2" }, null);
+ verifyRowMatches(contentValues, cursor, "logins found");
+ }
+ }
+
+ /**
+ * LoginsProvider updates logins test.
+ */
+ private class UpdateLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String guid1 = "guid1";
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", guid1);
+ long timeBeforeCreated = System.currentTimeMillis();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ long timeAfterCreated = System.currentTimeMillis();
+ verifyLoginExists(contentValues, id);
+
+ Cursor cursor = getLoginById(id);
+ try {
+ mAsserter.ok(cursor.moveToFirst(), "cursor is not empty", "");
+ verifyBounded(timeBeforeCreated, cursor.getLong(cursor.getColumnIndexOrThrow(Logins.TIME_CREATED)), timeAfterCreated);
+ } finally {
+ cursor.close();
+ }
+
+ contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username2");
+ contentValues.put(Logins.ENCRYPTED_PASSWORD, "password2");
+
+ Uri updateUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ int numUpdated = mProvider.update(updateUri, contentValues, null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ verifyLoginExists(contentValues, id);
+
+ contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username1");
+ contentValues.put(Logins.ENCRYPTED_PASSWORD, "password1");
+
+ updateUri = Logins.CONTENT_URI;
+ numUpdated = mProvider.update(updateUri, contentValues, Logins.GUID + " = ?", new String[]{guid1});
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ verifyLoginExists(contentValues, id);
+ }
+ }
+
+ /**
+ * LoginsProvider deletion logins test.
+ * - inserts a new logins
+ * - deletes the logins and verify deleted-logins table has entry for deleted guid.
+ */
+ private class DeleteLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String guid1 = "guid1";
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", guid1);
+ long id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+
+ Uri deletedUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ int numDeleted = mProvider.delete(deletedUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ verifyNoRowExists(Logins.CONTENT_URI, "No login entry found");
+
+ contentValues = new ContentValues();
+ contentValues.put(DeletedLogins.GUID, guid1);
+ Cursor cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, null, null, null);
+ verifyRowMatches(contentValues, cursor, "deleted-login found");
+ cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, DeletedLogins.GUID + " = ?", new String[] { guid1 }, null);
+ verifyRowMatches(contentValues, cursor, "deleted-login found");
+ }
+ }
+
+ /**
+ * LoginsProvider re-insert logins test.
+ * - inserts a row into deleted-logins
+ * - insert the same login (matching guid) and verify deleted-logins table is empty.
+ */
+ private class InsertDeletedLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(DeletedLogins.GUID, "guid1");
+ long id = ContentUris.parseId(mProvider.insert(DeletedLogins.CONTENT_URI, contentValues));
+ final Uri insertedUri = DeletedLogins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
+ verifyRowMatches(contentValues, cursor, "deleted-login found");
+ verifyNoRowExists(BrowserContract.Logins.CONTENT_URI, "No login entry found");
+
+ contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
+ id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+ verifyNoRowExists(DeletedLogins.CONTENT_URI, "No deleted-login entry found");
+ }
+ }
+
+ /**
+ * LoginsProvider insert Deleted logins test.
+ * - inserts a row into deleted-login without GUID.
+ */
+ private class InsertDeletedLoginsFailureTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues contentValues = new ContentValues();
+ try {
+ mProvider.insert(DeletedLogins.CONTENT_URI, contentValues);
+ fail("Failed to throw IllegalArgumentException while missing GUID");
+ } catch (Exception e) {
+ mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid GUID");
+ }
+ }
+ }
+
+ /**
+ * LoginsProvider disabled host test.
+ * - inserts a disabled-host
+ * - delete the inserted disabled-host and verify disabled-hosts table is empty.
+ */
+ private class DisabledHostsInsertTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String hostname = "localhost";
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put(LoginsDisabledHosts.HOSTNAME, hostname);
+ mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
+ final Uri insertedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
+ final Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
+ verifyRowMatches(contentValues, cursor, "disabled-hosts found");
+
+ final Uri deletedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
+ final int numDeleted = mProvider.delete(deletedUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ verifyNoRowExists(LoginsDisabledHosts.CONTENT_URI, "No disabled-hosts entry found");
+ }
+ }
+
+ /**
+ * LoginsProvider disabled host insert failure testcase.
+ * - inserts a disabled-host without providing hostname
+ */
+ private class DisabledHostsInsertFailureTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String hostname = "localhost";
+ final ContentValues contentValues = new ContentValues();
+ try {
+ mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
+ fail("Failed to throw IllegalArgumentException while missing hostname");
+ } catch (Exception e) {
+ mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid hostname");
+ }
+ }
+ }
+
+ /**
+ * LoginsProvider login insertion with default values test.
+ * - insert a login missing GUID, FORM_SUBMIT_URL, HTTP_REALM and verify default values are set.
+ */
+ private class InsertLoginsWithDefaultValuesTest extends TestCase {
+ @Override
+ protected void test() throws Exception {
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", null);
+ // Remove GUID, HTTP_REALM, FORM_SUBMIT_URL from content values
+ contentValues.remove(Logins.GUID);
+ contentValues.remove(Logins.FORM_SUBMIT_URL);
+ contentValues.remove(Logins.HTTP_REALM);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ Cursor cursor = getLoginById(id);
+ assertNotNull(cursor);
+ cursor.moveToFirst();
+
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.GUID)), null, "GUID is not null");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.HTTP_REALM)), null, "HTTP_REALM is not null");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.FORM_SUBMIT_URL)), null, "FORM_SUBMIT_URL is not null");
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_LAST_USED)), null, "TIME_LAST_USED is not null");
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_CREATED)), null, "TIME_CREATED is not null");
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_PASSWORD_CHANGED)), null, "TIME_PASSWORD_CHANGED is not null");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.ENC_TYPE)), "0", "ENC_TYPE is 0");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.TIMES_USED)), "0", "TIMES_USED is 0");
+
+ // Verify other values.
+ verifyRowMatches(contentValues, cursor, "Updated login found");
+ }
+ }
+
+ /**
+ * LoginsProvider login insertion with duplicate GUID test.
+ * - insert two different logins with same GUID and verify that only one login exists.
+ */
+ private class InsertLoginsWithDuplicateGuidFailureTest extends TestCase {
+ @Override
+ protected void test() throws Exception {
+ final String guid = "guid1";
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", guid);
+ long id1 = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id1);
+
+ // Insert another login with duplicate GUID.
+ contentValues = createLogin("http://www.example2.com", "http://www.example2.com",
+ "http://www.example2.com", "username2", "password2", "username2", "password2", guid);
+ Uri insertUri = mProvider.insert(Logins.CONTENT_URI, contentValues);
+ mAsserter.is(insertUri, null, "Duplicate Guid insertion id1");
+
+ // Verify login with id1 still exists.
+ verifyLoginExists(contentValues, id1);
+ }
+ }
+
+ /**
+ * LoginsProvider deletion by non-existent GUID test.
+ * - delete a login with random GUID and verify that no entry was deleted.
+ */
+ private class DeleteLoginsByNonExistentGuidTest extends TestCase {
+ @Override
+ protected void test() throws Exception {
+ Uri deletedUri = Logins.CONTENT_URI;
+ int numDeleted = mProvider.delete(deletedUri, Logins.GUID + "= ?", new String[] { "guid1" });
+ mAsserter.is(0, numDeleted, "Correct number deleted");
+ }
+ }
+
+ private void verifyBounded(long left, long middle, long right) {
+ mAsserter.ok(left <= middle, "Left <= middle", left + " <= " + middle);
+ mAsserter.ok(middle <= right, "Middle <= right", middle + " <= " + right);
+ }
+
+ private Cursor getById(Uri uri, long id, String[] projection) {
+ return mProvider.query(uri, projection,
+ _ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ }
+
+ private Cursor getLoginById(long id) {
+ return getById(Logins.CONTENT_URI, id, null);
+ }
+
+ private void verifyLoginExists(ContentValues contentValues, long id) {
+ Cursor cursor = getLoginById(id);
+ verifyRowMatches(contentValues, cursor, "Updated login found");
+ }
+
+ private void verifyRowMatches(ContentValues contentValues, Cursor cursor, String name) {
+ try {
+ mAsserter.ok(cursor.moveToFirst(), name, "cursor is not empty");
+ CursorMatches(cursor, contentValues);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void verifyNoRowExists(Uri contentUri, String name) {
+ Cursor cursor = mProvider.query(contentUri, null, null, null, null);
+ try {
+ mAsserter.is(0, cursor.getCount(), name);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private ContentValues createLogin(String hostname, String httpRealm, String formSubmitUrl,
+ String usernameField, String passwordField, String encryptedUsername,
+ String encryptedPassword, String guid) {
+ final ContentValues values = new ContentValues();
+ values.put(Logins.HOSTNAME, hostname);
+ values.put(Logins.HTTP_REALM, httpRealm);
+ values.put(Logins.FORM_SUBMIT_URL, formSubmitUrl);
+ values.put(Logins.USERNAME_FIELD, usernameField);
+ values.put(Logins.PASSWORD_FIELD, passwordField);
+ values.put(Logins.ENCRYPTED_USERNAME, encryptedUsername);
+ values.put(Logins.ENCRYPTED_PASSWORD, encryptedPassword);
+ values.put(Logins.GUID, guid);
+ return values;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java
new file mode 100644
index 000000000..af674f441
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java
@@ -0,0 +1,26 @@
+/* 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;
+
+public class testMailToContextMenu extends ContentContextMenuTest {
+
+ // Test website strings
+ private static String MAILTO_PAGE_URL;
+ private static final String mailtoMenuItems [] = {"Copy Email Address", "Share Email Address"};
+
+ public void testMailToContextMenu() {
+ final String MAILTO_PAGE_TITLE = mStringHelper.ROBOCOP_BIG_MAILTO_TITLE;
+
+ blockForGeckoReady();
+
+ MAILTO_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_MAILTO_URL);
+ loadUrlAndWait(MAILTO_PAGE_URL);
+ waitForText(MAILTO_PAGE_TITLE);
+
+ verifyContextMenuItems(mailtoMenuItems);
+ verifyCopyOption(mailtoMenuItems[0], "foo.bar@example.com"); // Test the "Copy Email Address" option
+ verifyShareOption(mailtoMenuItems[1], MAILTO_PAGE_TITLE); // Test the "Share Email Address" option
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java
new file mode 100644
index 000000000..2ae2bb532
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java
@@ -0,0 +1,288 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertArrayEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.gecko.background.nativecode.NativeCrypto;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+import android.os.SystemClock;
+
+/**
+ * Tests the Java wrapper over native implementations of crypto code. Test vectors from:
+ * * PBKDF2SHA256:
+ * - <https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors>
+ - <https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c>
+ * SHA-1:
+ - <http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c>
+ */
+public class testNativeCrypto extends UITest {
+ private final static String LOGTAG = "testNativeCrypto";
+
+ /**
+ * Robocop supports only a single test function per test class. Therefore, we
+ * have a single top-level test function that dispatches to sub-tests,
+ * accepting that we might fail part way through the cycle. Proper JUnit 3
+ * testing can't land soon enough!
+ *
+ * @throws Exception
+ */
+ public void test() throws Exception {
+ // This test could complete very quickly. If it completes too soon, the
+ // minidumps directory may not be created before the process is
+ // taken down, causing bug 722166. But we can't run the test and then block
+ // for Gecko:Ready, since it may have arrived before we block. So we wait.
+ // Again, JUnit 3 can't land soon enough!
+ GeckoHelper.blockForReady();
+
+ _testPBKDF2SHA256A();
+ _testPBKDF2SHA256B();
+ _testPBKDF2SHA256C();
+ _testPBKDF2SHA256scryptA();
+ _testPBKDF2SHA256scryptB();
+ _testPBKDF2SHA256InvalidLenArg();
+
+ _testSHA1();
+ _testSHA1AgainstMessageDigest();
+
+ _testSHA256();
+ _testSHA256MultiPart();
+ _testSHA256AgainstMessageDigest();
+ _testSHA256WithMultipleUpdatesFromStream();
+ }
+
+ public void _testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "password";
+ final String s = "salt";
+ final int dkLen = 32;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b");
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a");
+ }
+
+ public void _testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "passwordPASSWORDpassword";
+ final String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt";
+ final int dkLen = 40;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9");
+ }
+
+ public void _testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "passwd";
+ final String s = "salt";
+ final int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783");
+ }
+
+ public void _testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "Password";
+ final String s = "NaCl";
+ final int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d");
+ }
+
+ public void _testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "pass\0word";
+ final String s = "sa\0lt";
+ final int dkLen = 16;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687");
+ }
+
+ public void _testPBKDF2SHA256InvalidLenArg() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "password";
+ final String s = "salt";
+ final int c = 1;
+ final int dkLen = -1; // Should always be positive.
+
+ try {
+ final byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ fFail("Expected sha256 to throw with negative dkLen argument.");
+ } catch (IllegalArgumentException e) { } // Expected.
+ }
+
+ private void _testSHA1() throws UnsupportedEncodingException {
+ final String[] inputs = new String[] {
+ "abc",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "" // To be filled in below.
+ };
+ final String baseStr = "01234567";
+ final int repetitions = 80;
+ final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
+ for (int i = 0; i < 80; ++i) {
+ builder.append(baseStr);
+ }
+ inputs[2] = builder.toString();
+
+ final String[] expecteds = new String[] {
+ "a9993e364706816aba3e25717850c26c9cd0d89d",
+ "84983e441c3bd26ebaae4aa1f95129e5e54670f1",
+ "dea356a2cddd90c7a7ecedc5ebb563934f460452"
+ };
+
+ for (int i = 0; i < inputs.length; ++i) {
+ final byte[] input = inputs[i].getBytes("US-ASCII");
+ final String expected = expecteds[i];
+
+ final byte[] actual = NativeCrypto.sha1(input);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+ }
+
+ /**
+ * Test to ensure the output of our SHA1 algo is the same as MessageDigest's. This is important
+ * because we intend to replace MessageDigest in FHR with this SHA-1 algo (bug 959652).
+ */
+ private void _testSHA1AgainstMessageDigest() throws UnsupportedEncodingException,
+ NoSuchAlgorithmException {
+ final String[] inputs = {
+ "password",
+ "saranghae",
+ "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!"
+ };
+
+ final MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ for (final String input : inputs) {
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+
+ final byte[] mdBytes = digest.digest(inputBytes);
+ final byte[] ourBytes = NativeCrypto.sha1(inputBytes);
+ fAssertArrayEquals("MessageDigest hash is the same as NativeCrypto SHA-1 hash", mdBytes, ourBytes);
+ }
+ }
+
+ private void _testSHA256() throws UnsupportedEncodingException {
+ final String[] inputs = new String[] {
+ "abc",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "" // To be filled in below.
+ };
+ final String baseStr = "01234567";
+ final int repetitions = 80;
+ final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
+ for (int i = 0; i < repetitions; ++i) {
+ builder.append(baseStr);
+ }
+ inputs[2] = builder.toString();
+
+ final String[] expecteds = new String[] {
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
+ "594847328451bdfa85056225462cc1d867d877fb388df0ce35f25ab5562bfbb5"
+ };
+
+ for (int i = 0; i < inputs.length; ++i) {
+ final byte[] input = inputs[i].getBytes("US-ASCII");
+ final String expected = expecteds[i];
+
+ final byte[] ctx = NativeCrypto.sha256init();
+ NativeCrypto.sha256update(ctx, input, input.length);
+ final byte[] actual = NativeCrypto.sha256finalize(ctx);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+ }
+
+ private void _testSHA256MultiPart() throws UnsupportedEncodingException {
+ final String input = "01234567";
+ final int repetitions = 80;
+ final String expected = "594847328451bdfa85056225462cc1d867d877fb388df0ce35f25ab5562bfbb5";
+
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+ final byte[] ctx = NativeCrypto.sha256init();
+ for (int i = 0; i < repetitions; ++i) {
+ NativeCrypto.sha256update(ctx, inputBytes, inputBytes.length);
+ }
+ final byte[] actual = NativeCrypto.sha256finalize(ctx);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+
+ private void _testSHA256AgainstMessageDigest() throws UnsupportedEncodingException,
+ NoSuchAlgorithmException {
+ final String[] inputs = {
+ "password",
+ "saranghae",
+ "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!"
+ };
+
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ for (final String input : inputs) {
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+
+ final byte[] mdBytes = digest.digest(inputBytes);
+
+ final byte[] ctx = NativeCrypto.sha256init();
+ NativeCrypto.sha256update(ctx, inputBytes, inputBytes.length);
+ final byte[] ourBytes = NativeCrypto.sha256finalize(ctx);
+ fAssertArrayEquals("MessageDigest hash is the same as NativeCrypto SHA-256 hash", mdBytes, ourBytes);
+ }
+ }
+
+ private void _testSHA256WithMultipleUpdatesFromStream() throws UnsupportedEncodingException {
+ final String input = "HelloWorldThisIsASuperLongStringThatIsReadAsAStreamOfBytes";
+ final ByteArrayInputStream stream = new ByteArrayInputStream(input.getBytes("UTF-8"));
+ final String expected = "8b5cb76b80f7eb6fb83ee138bfd31e2922e71dd245daa21a8d9876e8dee9eef5";
+
+ byte[] buffer = new byte[10];
+ final byte[] ctx = NativeCrypto.sha256init();
+ int c;
+
+ try {
+ while ((c = stream.read(buffer)) != -1) {
+ NativeCrypto.sha256update(ctx, buffer, c);
+ }
+ final byte[] actual = NativeCrypto.sha256finalize(ctx);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ } catch (IOException e) {
+ fFail("IOException while reading stream");
+ }
+ }
+
+ private void checkPBKDF2SHA256(String p, String s, int c, int dkLen, final String expectedStr)
+ throws GeneralSecurityException, UnsupportedEncodingException {
+ final long start = SystemClock.elapsedRealtime();
+
+ final byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ fAssertNotNull("Hash result is non-null", key);
+
+ final long end = SystemClock.elapsedRealtime();
+ dumpLog(LOGTAG, "SHA-256 " + c + " took " + (end - start) + "ms");
+
+ if (expectedStr == null) {
+ return;
+ }
+
+ fAssertEquals("Hash result is the appropriate length", dkLen,
+ Utils.hex2Byte(expectedStr).length);
+ assertExpectedBytes(expectedStr, key);
+ }
+
+ private void assertExpectedBytes(final String expectedStr, byte[] key) {
+ fAssertEquals("Expected string matches hash result", expectedStr, Utils.byte2Hex(key));
+ final byte[] expected = Utils.hex2Byte(expectedStr);
+
+ fAssertEquals("Expected byte array length matches key length", expected.length, key.length);
+ fAssertArrayEquals("Expected byte array matches key byte array", expected, key);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java
new file mode 100644
index 000000000..d9b014c1a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java
@@ -0,0 +1,65 @@
+/* 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 org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import android.app.Activity;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+/* A simple test that creates 2 new tabs and checks that the tab count increases. */
+public class testNewTab extends BaseTest {
+ private Element tabCount = null;
+ private Element tabs = null;
+ private final Element closeTab = null;
+ private int tabCountInt = 0;
+
+ public void testNewTab() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String url2 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ blockForGeckoReady();
+
+ Activity activity = getActivity();
+ tabCount = mDriver.findElement(activity, R.id.tabs_counter);
+ tabs = mDriver.findElement(activity, R.id.tabs);
+ mAsserter.ok(tabCount != null && tabs != null,
+ "Checking elements", "all elements present");
+
+ int expectedTabCount = 1;
+ getTabCount(expectedTabCount);
+ mAsserter.is(tabCountInt, expectedTabCount, "Initial number of tabs correct");
+
+ addTab(url);
+ expectedTabCount++;
+ getTabCount(expectedTabCount);
+ mAsserter.is(tabCountInt, expectedTabCount, "Number of tabs increased");
+
+ addTab(url2);
+ expectedTabCount++;
+ getTabCount(expectedTabCount);
+ mAsserter.is(tabCountInt, expectedTabCount, "Number of tabs increased");
+
+ // cleanup: close all opened tabs
+ closeAddedTabs();
+ }
+
+ private void getTabCount(final int expected) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ String newTabCountText = tabCount.getText();
+ tabCountInt = Integer.parseInt(newTabCountText);
+ if (tabCountInt == expected) {
+ return true;
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java
new file mode 100644
index 000000000..434594fee
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java
@@ -0,0 +1,137 @@
+/* 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.util.Locale;
+
+import org.mozilla.gecko.BrowserLocaleManager;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.PrefsHelper;
+
+import android.content.SharedPreferences;
+
+
+public class testOSLocale extends BaseTest {
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Clear per-profile SharedPreferences as a workaround for Bug 1069687.
+ // We're trying to exercise logic that only applies on first onCreate!
+ // We can't rely on this occurring prior to the first broadcast, though,
+ // so see the main test method for more logic.
+ final String profileName = getTestProfile().getName();
+ mAsserter.info("Setup", "Clearing pref in " + profileName + ".");
+ GeckoSharedPrefs.forProfileName(getActivity(), profileName)
+ .edit()
+ .remove("osLocale")
+ .apply();
+ }
+
+ public static class PrefState extends PrefsHelper.PrefHandlerBase {
+ private static final String PREF_LOCALE_OS = "intl.locale.os";
+ private static final String PREF_ACCEPT_LANG = "intl.accept_languages";
+
+ private static final String[] TO_FETCH = {PREF_LOCALE_OS, PREF_ACCEPT_LANG};
+
+ public volatile String osLocale;
+ public volatile String acceptLanguages;
+
+ private final Object waiter = new Object();
+
+ public void fetch() throws InterruptedException {
+ // Wait for any pending changes to have taken. Bug 1092580.
+ GeckoThread.waitOnGecko();
+ synchronized (waiter) {
+ PrefsHelper.getPrefs(TO_FETCH, this);
+ waiter.wait(MAX_WAIT_MS);
+ }
+ }
+
+ @Override
+ public void prefValue(String pref, String value) {
+ switch (pref) {
+ case PREF_LOCALE_OS:
+ osLocale = value;
+ return;
+ case PREF_ACCEPT_LANG:
+ acceptLanguages = value;
+ return;
+ }
+ }
+
+ @Override
+ public void finish() {
+ synchronized (waiter) {
+ waiter.notify();
+ }
+ }
+ }
+
+ public void testOSLocale() throws Exception {
+ blockForDelayedStartup();
+
+ final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getActivity());
+ final PrefState state = new PrefState();
+
+ state.fetch();
+
+ // We don't know at this point whether we were run against a dirty profile or not.
+ //
+ // If we cleared the pref above prior to BrowserApp's delayed init, or our Gecko
+ // profile has been used before, then we're already going to be set up for en-US.
+ //
+ // If we cleared the pref after the initial broadcast, and our Android-side profile
+ // has been used before but the Gecko profile is clean, then the Gecko prefs won't
+ // have been set.
+ //
+ // Instead, we always send a new locale code, and see what we get.
+ final Locale fr = Locales.parseLocaleCode("fr");
+ BrowserLocaleManager.storeAndNotifyOSLocale(prefs, fr);
+
+ state.fetch();
+
+ mAsserter.is(state.osLocale, "fr", "We're in fr.");
+
+ // Now we can see what the expected Accept-Languages header should be.
+ // The OS locale is 'fr', so we have our app locale (en-US),
+ // the OS locale (fr), then any remaining fallbacks from intl.properties.
+ mAsserter.is(state.acceptLanguages, "en-us,fr,en", "We have the default en-US+fr Accept-Languages.");
+
+ // Now set the app locale to be es-ES.
+ BrowserLocaleManager.getInstance().setSelectedLocale(getActivity(), "es-ES");
+
+ state.fetch();
+
+ mAsserter.is(state.osLocale, "fr", "We're still in fr.");
+
+ // The correct set here depends on whether the
+ // browser was built with multiple locales or not.
+ // This is exasperating, but hey.
+ final boolean isMultiLocaleBuild = false;
+
+ // This never changes.
+ final String SELECTED_LOCALES = "es-es,fr,";
+
+ // Expected, from es-ES's intl.properties:
+ final String EXPECTED = SELECTED_LOCALES +
+ (isMultiLocaleBuild ? "es,en-us,en" : // Expected, from es-ES's intl.properties.
+ "en-us,en"); // Expected, from en-US (the default).
+
+ mAsserter.is(state.acceptLanguages, EXPECTED, "We have the right es-ES+fr Accept-Languages for this build.");
+
+ // And back to en-US.
+ final Locale en_US = Locales.parseLocaleCode("en-US");
+ BrowserLocaleManager.storeAndNotifyOSLocale(prefs, en_US);
+ BrowserLocaleManager.getInstance().resetToSystemLocale(getActivity());
+
+ state.fetch();
+
+ mAsserter.is(state.osLocale, "en-US", "We're in en-US.");
+ mAsserter.is(state.acceptLanguages, "en-us,en", "We have the default processed en-US Accept-Languages.");
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java
new file mode 100644
index 000000000..e7d402607
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java
@@ -0,0 +1,49 @@
+/* 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 org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+/**
+ * A basic panning correctness test.
+ * - Loads a page and verifies it draws
+ * - drags page upwards by 100 pixels and verifies it draws
+ * - drags page leftwards by 100 pixels and verifies it draws
+ */
+public class testPanCorrectness extends PixelTest {
+ public void testPanCorrectness() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+
+ blockForGeckoReady();
+
+ // load page and check we're at 0,0
+ loadAndVerifyBoxes(url);
+
+ // drag page upwards by 100 pixels
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ meh.dragSync(10, 150, 10, 50);
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 100);
+ } finally {
+ painted.close();
+ }
+
+ // drag page leftwards by 100 pixels
+ paintExpecter = mActions.expectPaint();
+ meh.dragSync(150, 10, 50, 10);
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 100, 100);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java
new file mode 100644
index 000000000..65a4eaba6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java
@@ -0,0 +1,125 @@
+/* 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 org.json.JSONObject;
+import org.mozilla.gecko.NSSBridge;
+import org.mozilla.gecko.db.BrowserContract;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class testPasswordEncrypt extends BaseTest {
+ public void testPasswordEncrypt() {
+ Context context = (Context)getActivity();
+ ContentResolver cr = context.getContentResolver();
+ mAsserter.isnot(cr, null, "Found a content resolver");
+ ContentValues cvs = new ContentValues();
+
+ blockForGeckoReady();
+
+ File db = new File(mProfile, "signons.sqlite");
+ String dbPath = db.getPath();
+
+ Uri passwordUri;
+ cvs.put("hostname", "http://www.example.com");
+ cvs.put("encryptedUsername", "username");
+ cvs.put("encryptedPassword", "password");
+
+ // Attempt to insert into the db
+ passwordUri = BrowserContract.Passwords.CONTENT_URI;
+ Uri.Builder builder = passwordUri.buildUpon();
+ passwordUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ Uri uri = cr.insert(passwordUri, cvs);
+ Uri expectedUri = passwordUri.buildUpon().appendPath("1").build();
+ mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri");
+
+ Cursor list = mActions.querySql(dbPath, "SELECT encryptedUsername FROM moz_logins");
+ list.moveToFirst();
+ String decryptedU = null;
+ try {
+ decryptedU = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedU, "username", "Username was encrypted correctly when inserting");
+
+ list = mActions.querySql(dbPath, "SELECT encryptedPassword, encType FROM moz_logins");
+ list.moveToFirst();
+ String decryptedP = null;
+ try {
+ decryptedP = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedP, "password", "Password was encrypted correctly when inserting");
+ mAsserter.is(list.getInt(1), 1, "Password has correct encryption type");
+
+ cvs.put("encryptedUsername", "username2");
+ cvs.put("encryptedPassword", "password2");
+ cr.update(passwordUri, cvs, null, null);
+
+ list = mActions.querySql(dbPath, "SELECT encryptedUsername FROM moz_logins");
+ list.moveToFirst();
+ try {
+ decryptedU = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedU, "username2", "Username was encrypted when updating");
+
+ list = mActions.querySql(dbPath, "SELECT encryptedPassword FROM moz_logins");
+ list.moveToFirst();
+ try {
+ decryptedP = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedP, "password2", "Password was encrypted when updating");
+
+ // Trying to store a password while master password is enabled should throw,
+ // but because Android can't send Exceptions across processes
+ // it just results in a null uri/cursor being returned.
+ toggleMasterPassword("password");
+ try {
+ uri = cr.insert(passwordUri, cvs);
+ // TODO: restore this assertion -- see bug 764901
+ // mAsserter.is(uri, null, "Storing a password while MP was set should fail");
+
+ Cursor c = cr.query(passwordUri, null, null, null, null);
+ // TODO: restore this assertion -- see bug 764901
+ // mAsserter.is(c, null, "Querying passwords while MP was set should fail");
+ } catch (Exception ex) {
+ // Password provider currently can not throw across process
+ // so we should not catch this exception here
+ mAsserter.ok(false, "Caught exception", ex.toString());
+ }
+ toggleMasterPassword("password");
+ }
+
+ private void toggleMasterPassword(String passwd) {
+ setPreferenceAndWaitForChange("privacy.masterpassword.enabled", passwd);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // remove the entire signons.sqlite file
+ File profile = new File(mProfile);
+ File db = new File(profile, "signons.sqlite");
+ if (db.delete()) {
+ mAsserter.dumpLog("tearDown deleted "+db.toString());
+ } else {
+ mAsserter.dumpLog("tearDown did not delete "+db.toString());
+ }
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java
new file mode 100644
index 000000000..8a2cc357e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java
@@ -0,0 +1,104 @@
+/* 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 org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts;
+import org.mozilla.gecko.db.BrowserContract.Passwords;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * A basic password contentprovider test.
+ * - inserts a password when the database is not yet set up
+ * - inserts a password
+ * - updates a password
+ * - deletes a password
+ * - inserts a disabled host
+ * - queries for disabled host
+ */
+public class testPasswordProvider extends BaseTest {
+ private static final String DB_NAME = "signons.sqlite";
+
+ public void testPasswordProvider() {
+ Context context = (Context)getActivity();
+ ContentResolver cr = context.getContentResolver();
+ ContentValues[] cvs = new ContentValues[1];
+ cvs[0] = new ContentValues();
+
+ blockForGeckoReady();
+
+ cvs[0].put("hostname", "http://www.example.com");
+ cvs[0].put("httpRealm", "http://www.example.com");
+ cvs[0].put("formSubmitURL", "http://www.example.com");
+ cvs[0].put("usernameField", "usernameField");
+ cvs[0].put("passwordField", "passwordField");
+ cvs[0].put("encryptedUsername", "username");
+ cvs[0].put("encryptedPassword", "password");
+ cvs[0].put("encType", "1");
+
+ // Attempt to insert into the db
+ Uri passwordUri = Passwords.CONTENT_URI;
+ Uri.Builder builder = passwordUri.buildUpon();
+ passwordUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ Uri uri = cr.insert(passwordUri, cvs[0]);
+ Uri expectedUri = passwordUri.buildUpon().appendPath("1").build();
+ mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri");
+ Cursor c = cr.query(passwordUri, null, null, null, null);
+ SqliteCompare(c, cvs);
+
+ cvs[0].put("usernameField", "usernameField2");
+ cvs[0].put("passwordField", "passwordField2");
+
+ int numUpdated = cr.update(passwordUri, cvs[0], null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ c = cr.query(passwordUri, null, null, null, null);
+ SqliteCompare(c, cvs);
+
+ int numDeleted = cr.delete(passwordUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ cvs = new ContentValues[0];
+ c = cr.query(passwordUri, null, null, null, null);
+ SqliteCompare(c, cvs);
+
+ ContentValues values = new ContentValues();
+ values.put("hostname", "http://www.example.com");
+
+ // Attempt to insert into the db.
+ Uri disabledHostUri = GeckoDisabledHosts.CONTENT_URI;
+ builder = disabledHostUri.buildUpon();
+ disabledHostUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ uri = cr.insert(disabledHostUri, values);
+ expectedUri = disabledHostUri.buildUpon().appendPath("1").build();
+ mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri");
+ Cursor cursor = cr.query(disabledHostUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ CursorMatches(cursor, values);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // remove the entire signons.sqlite file
+ File profile = new File(mProfile);
+ File db = new File(profile, "signons.sqlite");
+ if (db.delete()) {
+ mAsserter.dumpLog("tearDown deleted "+db.toString());
+ } else {
+ mAsserter.dumpLog("tearDown did not delete "+db.toString());
+ }
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java
new file mode 100644
index 000000000..e4d997895
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java
@@ -0,0 +1,72 @@
+/* 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.util.ArrayList;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+import android.widget.CheckBox;
+
+public class testPermissions extends PixelTest {
+ public void testPermissions() {
+ blockForGeckoReady();
+
+ geolocationTest();
+ }
+
+ private void geolocationTest() {
+ Actions.RepeatedEventExpecter paintExpecter;
+
+ // Test geolocation notification
+ loadAndPaint(getAbsoluteUrl(mStringHelper.ROBOCOP_GEOLOCATION_URL));
+ waitForText("wants your location");
+
+ // Uncheck the "Don't ask again for this site" checkbox
+ ArrayList<CheckBox> checkBoxes = mSolo.getCurrentViews(CheckBox.class);
+ mAsserter.ok(checkBoxes.size() == 1, "checkbox count", "only one checkbox visible");
+ mAsserter.ok(mSolo.isCheckBoxChecked(0), "checkbox checked", "checkbox is checked");
+ mSolo.clickOnCheckBox(0);
+ mAsserter.ok(!mSolo.isCheckBoxChecked(0), "checkbox not checked", "checkbox is not checked");
+
+ // Test "Share" button functionality with unchecked checkbox
+ paintExpecter = mActions.expectPaint();
+ mSolo.clickOnText("Share");
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green");
+ } finally {
+ painted.close();
+ }
+
+ // Re-trigger geolocation notification
+ reloadAndPaint();
+ waitForText("wants your location");
+
+ // Make sure the checkbox is checked this time
+ mAsserter.ok(mSolo.isCheckBoxChecked(0), "checkbox checked", "checkbox is checked");
+
+ // Test "Share" button functionality with checked checkbox
+ paintExpecter = mActions.expectPaint();
+ mSolo.clickOnText("Share");
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green");
+ } finally {
+ painted.close();
+ }
+
+ // When we reload the page, location should be automatically shared
+ painted = reloadAndGetPainted();
+ try {
+ mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green");
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java
new file mode 100644
index 000000000..1461fd9be
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java
@@ -0,0 +1,52 @@
+/* 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;
+
+public class testPictureLinkContextMenu extends ContentContextMenuTest {
+
+ // Test website strings
+ private static String PICTURE_PAGE_URL;
+ private static String BLANK_PAGE_URL;
+ private static String PICTURE_URL;
+ private static final String tabs [] = { "Image", "Link" };
+ private static final String photoMenuItems [] = { "Copy Image Location", "Share Image", "View Image", "Set Image As", "Save Image" };
+ private static final String imageTitle = "^Image$";
+
+ public void testPictureLinkContextMenu() {
+ final String PICTURE_PAGE_TITLE = mStringHelper.ROBOCOP_PICTURE_LINK_TITLE;
+ final String linkMenuItems [] = mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB;
+
+ blockForGeckoReady();
+
+ PICTURE_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_PICTURE_LINK_URL);
+ BLANK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ PICTURE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_PICTURE_URL);
+ loadAndPaint(PICTURE_PAGE_URL);
+ verifyUrlInContentDescription(PICTURE_PAGE_URL);
+
+ switchTabs(imageTitle);
+ verifyContextMenuItems(photoMenuItems);
+ verifyTabs(tabs);
+ switchTabs(imageTitle);
+ verifyCopyOption(photoMenuItems[0], "Firefox.jpg"); // Test the "Copy Image Location" option
+ switchTabs(imageTitle);
+ verifyShareOption(photoMenuItems[1], PICTURE_PAGE_TITLE); // Test the "Share Image" option
+ switchTabs(imageTitle);
+ verifyViewImageOption(photoMenuItems[2], PICTURE_URL, PICTURE_PAGE_TITLE); // Test the "View Image" option
+
+ verifyContextMenuItems(linkMenuItems);
+ openTabFromContextMenu(linkMenuItems[0],2); // Test the "Open in New Tab" option - expecting 2 tabs: the original and the new one
+ openTabFromContextMenu(linkMenuItems[1],2); // Test the "Open in Private Tab" option - expecting only 2 tabs in normal mode
+ verifyCopyOption(linkMenuItems[2], BLANK_PAGE_URL); // Test the "Copy Link" option
+ verifyShareOption(linkMenuItems[3], PICTURE_PAGE_TITLE); // Test the "Share Link" option
+ verifyBookmarkLinkOption(linkMenuItems[4],BLANK_PAGE_URL); // Test the "Bookmark Link" option
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDatabaseHelper.deleteBookmark(BLANK_PAGE_URL);
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java
new file mode 100644
index 000000000..f63358d57
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java
@@ -0,0 +1,81 @@
+/* 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 org.mozilla.gecko.Actions;
+
+/**
+ * Basic test to check bounce-back from overscroll.
+ * - Load the page and verify it draws
+ * - Drag page downwards by 100 pixels into overscroll, verify it snaps back.
+ * - Drag page rightwards by 100 pixels into overscroll, verify it snaps back.
+ */
+public class testPrefsObserver extends BaseTest {
+ private static final String PREF_TEST_PREF = "robocop.tests.dummy";
+
+ private Actions.PrefWaiter prefWaiter;
+ private boolean prefValue;
+
+ public void setPref(boolean value) {
+ mAsserter.dumpLog("Setting pref");
+ mActions.setPref(PREF_TEST_PREF, value, /* flush */ false);
+ }
+
+ public void waitAndCheckPref(boolean value) {
+ mAsserter.dumpLog("Waiting to check pref");
+
+ mAsserter.isnot(prefWaiter, null, "Check pref waiter is not null");
+ prefWaiter.waitForFinish();
+
+ mAsserter.is(prefValue, value, "Check correct pref value");
+ }
+
+ public void verifyDisconnect() {
+ mAsserter.dumpLog("Checking pref observer is removed");
+
+ final boolean newValue = !prefValue;
+ setPreferenceAndWaitForChange(PREF_TEST_PREF, newValue);
+ mAsserter.isnot(prefValue, newValue, "Check pref value did not change");
+ }
+
+ public void observePref() {
+ mAsserter.dumpLog("Setting up pref observer");
+
+ // Setup the pref observer
+ mAsserter.is(prefWaiter, null, "Check pref waiter is null");
+ prefWaiter = mActions.addPrefsObserver(
+ new String[] { PREF_TEST_PREF }, new Actions.PrefHandlerBase() {
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean value) {
+ mAsserter.is(pref, PREF_TEST_PREF, "Check correct pref name");
+ prefValue = value;
+ }
+ });
+ }
+
+ public void removePrefObserver() {
+ mAsserter.dumpLog("Removing pref observer");
+
+ mActions.removePrefsObserver(prefWaiter);
+ }
+
+ public void testPrefsObserver() {
+ blockForGeckoReady();
+
+ setPref(false);
+ observePref();
+ waitAndCheckPref(false);
+
+ setPref(true);
+ waitAndCheckPref(true);
+
+ removePrefObserver();
+ verifyDisconnect();
+
+ // Removing again should be a no-op.
+ removePrefObserver();
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
new file mode 100644
index 000000000..461e95aa7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
@@ -0,0 +1,89 @@
+/* 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.util.ArrayList;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Tabs;
+
+/**
+ * The test loads a new private tab and loads a page with a big link on it
+ * Opens the link in a new private tab and checks that it is private
+ * Adds a new normal tab and loads a 3rd URL
+ * Checks that the bigLinkUrl loaded in the normal tab is present in the browsing history but the 2 urls opened in private tabs are not
+ */
+public class testPrivateBrowsing extends ContentContextMenuTest {
+
+ public void testPrivateBrowsing() {
+ String bigLinkUrl = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
+ String blank1Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String blank2Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ Tabs tabs = Tabs.getInstance();
+
+ blockForGeckoReady();
+
+ Actions.EventExpecter tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+ Actions.EventExpecter contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ tabs.loadUrl(bigLinkUrl, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
+ tabExpecter.blockForEvent();
+ tabExpecter.unregisterListener();
+ contentExpecter.blockForEvent();
+ contentExpecter.unregisterListener();
+ verifyTabCount(1);
+
+ // May intermittently get context menu for normal tab without additional wait
+ mSolo.sleep(5000);
+
+ // Open the link context menu and verify the options
+ verifyContextMenuItems(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB);
+
+ // Check that "Open Link in New Tab" is not in the menu
+ mAsserter.ok(!mSolo.searchText(mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]), "Checking that 'Open Link in New Tab' is not displayed in the context menu", "'Open Link in New Tab' is not displayed in the context menu");
+
+ // Open the link in a new private tab and check that it is private
+ tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB[0]);
+ String eventData = tabExpecter.blockForEventData();
+ tabExpecter.unregisterListener();
+ contentExpecter.blockForEvent();
+ contentExpecter.unregisterListener();
+ mAsserter.ok(isTabPrivate(eventData), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab");
+ verifyTabCount(2);
+
+ // Open a normal tab to check later that it was registered in the Firefox Browser History
+ tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ tabs.loadUrl(blank2Url, Tabs.LOADURL_NEW_TAB);
+ tabExpecter.blockForEvent();
+ tabExpecter.unregisterListener();
+ contentExpecter.blockForEvent();
+ contentExpecter.unregisterListener();
+ verifyTabCount(2);
+
+ // wait for history updates to complete
+ mSolo.sleep(3000);
+
+ // Get the history list and check that the links open in private browsing are not saved
+ final ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY);
+
+ mAsserter.ok(!firefoxHistory.contains(bigLinkUrl), "Check that the link opened in the first private tab was not saved", bigLinkUrl + " was not added to history");
+ mAsserter.ok(!firefoxHistory.contains(blank1Url), "Check that the link opened in the private tab from the context menu was not saved", blank1Url + " was not added to history");
+ mAsserter.ok(firefoxHistory.contains(blank2Url), "Check that the link opened in the normal tab was saved", blank2Url + " was added to history");
+ }
+
+ private boolean isTabPrivate(String eventData) {
+ try {
+ JSONObject data = new JSONObject(eventData);
+ return data.getBoolean("isPrivate");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "Error parsing the event data", e.toString());
+ return false;
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java
new file mode 100644
index 000000000..f645fe3be
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java
@@ -0,0 +1,47 @@
+/* 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;
+
+public class testPromptGridInput extends BaseTest {
+ protected int index = 1;
+ public void testPromptGridInput() {
+ blockForGeckoReady();
+
+ test(1);
+
+ testGridItem("Icon 1");
+ testGridItem("Icon 2");
+ testGridItem("Icon 3");
+ testGridItem("Icon 4");
+ testGridItem("Icon 5");
+ testGridItem("Icon 6");
+ testGridItem("Icon 7");
+ testGridItem("Icon 8");
+ testGridItem("Icon 9");
+ testGridItem("Icon 10");
+ testGridItem("Icon 11");
+
+ mSolo.clickOnText("Icon 11");
+ mSolo.clickOnText("OK");
+
+ mAsserter.ok(waitForText("PASS"), "test passed", "PASS");
+ mSolo.goBack();
+ }
+
+ public void testGridItem(String title) {
+ // Force the list to scroll if necessary
+ mSolo.waitForText(title, 1, 500, true);
+ mAsserter.ok(waitForText(title), "Found grid item", title);
+ }
+
+ public void test(final int num) {
+ // Load about:blank between each test to ensure we reset state
+ loadUrl(mStringHelper.ABOUT_BLANK_URL);
+ mAsserter.ok(waitForText(mStringHelper.ABOUT_BLANK_URL), "Loaded blank page",
+ mStringHelper.ABOUT_BLANK_URL);
+
+ loadUrl("chrome://roboextender/content/robocop_prompt_gridinput.html#test" + num);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java
new file mode 100644
index 000000000..6dbc70de5
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java
@@ -0,0 +1,62 @@
+/* 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 org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.BrowserDatabaseHelper;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+/**
+ * Tests that our readercache-migration works correctly.
+ *
+ * Our main concern is ensuring that the hashed path for a given url is the same in Java
+ * as it was in JS, or else our (Java-based) migration will lose track of valid cached items.
+ */
+public class testReaderCacheMigration extends JavascriptBridgeTest {
+
+ private final String[] TEST_DOMAINS = new String[] {
+ "",
+ "http://mozilla.org",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1234315#c41",
+ "http://www.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.com/"
+ };
+
+ private static final String TEST_JS = "testReaderCacheMigration.js";
+
+ /**
+ * We compute the path-name in Java, and pass this through to JS, which conducts the actual
+ * equality check. Our JavascriptBridge doesn't seem to support return values, so we need
+ * to instead pass the computed path-name in at least one direction.
+ */
+ private void checkPathMatches(final String pageURL, final File cacheDir) {
+ final String hashedName = BrowserDatabaseHelper.getReaderCacheFileNameForURL(pageURL);
+
+ final File cacheFile = new File(cacheDir, hashedName);
+
+ try {
+ // We have to use the canonical path to match what the JS side will use. We could
+ // instead just match on the file name, and not the path, but this helps
+ // ensure that we've not broken any of the path finding either.
+ getJS().syncCall("check_hashed_path_matches", pageURL, cacheFile.getCanonicalPath());
+ } catch (IOException e) {
+ fAssertTrue("Unable to getCanonicalPath(), this should never happen", false);
+ }
+
+ }
+
+ public void testReaderCacheMigration() {
+ blockForReadyAndLoadJS(TEST_JS);
+
+ final File cacheDir = new File(GeckoProfile.get(getActivity()).getDir(), "readercache");
+
+ for (final String URL : TEST_DOMAINS) {
+ checkPathMatches(URL, cacheDir);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java
new file mode 100644
index 000000000..31e012070
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java
@@ -0,0 +1,19 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * This tests ensures that the toolbar in reader mode displays the original page url.
+ */
+public class testReaderModeTitle extends UITest {
+ public void testReaderModeTitle() {
+ GeckoHelper.blockForReady();
+
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
+
+ mToolbar.pressReaderModeButton();
+
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java
new file mode 100644
index 000000000..2006bbbfc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java
@@ -0,0 +1,12 @@
+/* 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;
+
+
+public class testReadingListCache extends JavascriptTest {
+ public testReadingListCache() {
+ super("testReadingListCache.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java
new file mode 100644
index 000000000..dc181defc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java
@@ -0,0 +1,217 @@
+/* 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 android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoProfileDirectories;
+import org.mozilla.gecko.db.*;
+import org.mozilla.gecko.reader.SavedReaderViewHelper;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.mozilla.gecko.db.BrowserContract.*;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit.
+
+/**
+ * This test runs the 30 to 31 database upgrade, which moves reading-list INPUT_FILES from a separate
+ * reading-list folder into mobile bookmarks.
+ *
+ * It is based on testBrowserDatabaseHelperUpgrades. We load a v30 db containing two reading list
+ * INPUT_FILES, and test that these have successfully been converted into bookmarks.
+ */
+public class testReadingListToBookmarksMigration extends UITest {
+ private ArrayList<File> tempFiles;
+
+ // These names are generated by hashing the URLs, see INPUT_URLS below, and
+ // BrowserDatabaseHelper.getReaderCacheFileNameForURL()
+ private static final ArrayList<String> INPUT_FILES = new ArrayList<String>() {{
+ add("DWUP3U4ERC6TKJVSYXKJLHHEFY.json");
+ add("KWNV7PXD3JFOJBQJVFXI3CQKNE.json");
+ }};
+
+ // same ordering as in INPUT_FILES, although we don't rely on ordering in this test
+ private static final ArrayList<String> INPUT_URLS = new ArrayList<String>() {{
+ add("http://www.bbc.com/news/election-us-2016-35962179");
+ add("http://www.bbc.com/news/world-europe-35962670");
+ }};
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store
+ // this there. That being said, temporary files are still stored in the application directory so these temporary
+ // files will get cleaned up when the application is uninstalled or when data is cleared.
+ tempFiles = new ArrayList<>();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ for (final File file : tempFiles) {
+ file.delete();
+ }
+ }
+
+ private void walkRLPreMigration(SQLiteDatabase db) {
+ Set<String> urls = new HashSet<>(INPUT_URLS);
+
+ final Cursor c = db.rawQuery("SELECT * FROM " + ReadingListItems.TABLE_NAME, null);
+
+ fAssertNotNull("Cursor cannot be null", c);
+ try {
+ final boolean movedToFirst = c.moveToFirst();
+ fAssertTrue("Cursor must have data", movedToFirst);
+
+ int urlIndex = c.getColumnIndexOrThrow(ReadingListItems.URL);
+ do {
+ final String url = c.getString(urlIndex);
+
+ boolean removed = urls.remove(url);
+ fAssertTrue("Unexpected reading-list URL in database", removed);
+ } while (c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ fAssertTrue("All urls should have been removed from set", urls.isEmpty());
+ }
+
+ private void walkRLPostMigration(SQLiteDatabase db) {
+ Set<String> urls = new HashSet<>(INPUT_URLS);
+
+ final Cursor c = db.rawQuery("SELECT * FROM " +
+ Bookmarks.VIEW_WITH_ANNOTATIONS
+ + " WHERE " + BrowserContract.UrlAnnotations.KEY + " = ?",
+ new String[] {
+ BrowserContract.UrlAnnotations.Key.READER_VIEW.getDbValue()
+ });
+
+ fAssertNotNull("Cursor cannot be null", c);
+ try {
+ final boolean movedToFirst = c.moveToFirst();
+ fAssertTrue("Cursor must have data", movedToFirst);
+
+ int urlIndex = c.getColumnIndexOrThrow(Bookmarks.URL);
+ do {
+ final String url = c.getString(urlIndex);
+
+ boolean removed = urls.remove(url);
+ fAssertTrue("Unexpected reading-list URL in database", removed);
+ } while (c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ fAssertTrue("All urls should have been removed from set", urls.isEmpty());
+ }
+
+ /**
+ * @throws IOException if the database or input files cannot be copied.
+ */
+ public void testReadingListToBookmarksMigration() throws IOException {
+ final String tempDbPath = copyAssets();
+ final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0);
+
+ try {
+ // This initialises the helper, but does not open the DB.
+ BrowserDatabaseHelper dbHelper = new BrowserDatabaseHelper(getActivity(), tempDbPath);
+
+ walkRLPreMigration(db);
+
+ // Run just one upgrade - we don't know what future upgrades might do, whereas with one
+ // upgrade we can guarantee a given DB state.
+ dbHelper.onUpgrade(db, 30, 31);
+
+ // SavedReaderViewHelper writes annotations directly to the GeckoProfile DB (as opposed
+ // to our local DB copy). We aren't able to read this here (and the data isn't written
+ // to our own db), hence we can't test the DB content yet.
+// walkRLPostMigration(db);
+
+ SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getActivity());
+
+ fAssertEquals("All input files should have been migrated", INPUT_FILES.size(), rvh.size());
+ for (String url : INPUT_URLS) {
+ boolean isCached = rvh.isURLCached(url);
+ fAssertTrue("URL no longer in cache after migration", isCached);
+ }
+ } finally {
+ db.close();
+ }
+ }
+
+ private void copyAssetToFile(String inputPath, File destination) throws IOException {
+ final InputStream inputStream = openFileFromAssets(inputPath);
+ final OutputStream os = new BufferedOutputStream(new FileOutputStream(destination));
+ try {
+ final byte[] buffer = new byte[1024];
+ int len;
+ while ((len = inputStream.read(buffer)) > 0) {
+ os.write(buffer, 0, len);
+ }
+ os.flush();
+ } finally {
+ os.close();
+ inputStream.close();
+ }
+ }
+
+ /**
+ * Copies assets into the desired locations. We need to copy our DB into a temporary file,
+ * and readercache items into the profile directory.
+ *
+ * @throws IOException if reading the existing files or writing the temporary files fails
+ */
+ private String copyAssets() throws IOException {
+ final File profileDir = GeckoProfile.get(getActivity()).getDir();
+ final File cacheDir = new File(profileDir, "readercache");
+ cacheDir.mkdir();
+
+ for (String name : INPUT_FILES) {
+ final String path = "readercache" + File.separator + name;
+ final File destination = new File(cacheDir, name);
+ tempFiles.add(destination);
+
+ Log.d(LOGTAG, "Moving readerview cache file to " + destination.getPath());
+ copyAssetToFile(path, destination);
+ }
+
+ final File dbDestination = File.createTempFile("temporaryDB_", "db");
+ tempFiles.add(dbDestination);
+
+ Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath());
+ copyAssetToFile("browser.db", dbDestination);
+
+ return dbDestination.getPath();
+ }
+
+ private InputStream openFileFromAssets(final String name) throws IOException {
+ final String assetPath = String.format("reading_list_bookmarks_migration" + File.separator + name);
+ Log.d(LOGTAG, "Opening file from assets: " + assetPath);
+ try {
+ return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(assetPath));
+ } catch (final FileNotFoundException e) {
+ throw new IllegalStateException("Declared input files must be provided", e);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java
new file mode 100644
index 000000000..8977aa177
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java
@@ -0,0 +1,39 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.restrictions.Restrictable;
+import org.mozilla.gecko.restrictions.Restrictions;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+public class testRestrictions extends UITest {
+ public void testRestrictions() {
+ GeckoHelper.blockForReady();
+
+ // No restrictions should be enforced when using a normal profile
+ for (Restrictable restrictable : Restrictable.values()) {
+ if (restrictable == Restrictable.BLOCK_LIST) {
+ assertFeatureDisabled(restrictable);
+ } else {
+ assertFeatureEnabled(restrictable);
+ }
+ }
+ }
+
+ private void assertFeatureEnabled(Restrictable restrictable) {
+ fAssertTrue(String.format("Restrictable feature %s is enabled", restrictable.name),
+ Restrictions.isAllowed(getActivity(), restrictable)
+ );
+ }
+
+ private void assertFeatureDisabled(Restrictable restrictable) {
+ fAssertFalse(String.format("Restrictable feature %s is disabled", restrictable.name),
+ Restrictions.isAllowed(getActivity(), restrictable)
+ );
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
new file mode 100644
index 000000000..df192fc43
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
@@ -0,0 +1,48 @@
+/* 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 org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+public class testRuntimePermissionsAPI extends JavascriptTest implements NativeEventListener {
+ public testRuntimePermissionsAPI() {
+ super("testRuntimePermissionsAPI.js");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this, "RuntimePermissions:Prompt");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "RuntimePermissions:Prompt");
+ }
+
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ mAsserter.is(event, "RuntimePermissions:Prompt", "Received RuntimePermissions:Prompt event");
+
+ try {
+ String[] permissions = message.getStringArray("permissions");
+ mAsserter.is(3, permissions.length, "Received three permissions");
+
+ mAsserter.is("android.permission.CAMERA", permissions[0], "Received CAMERA permission");
+ mAsserter.is("android.permission.WRITE_EXTERNAL_STORAGE", permissions[1], "Received WRITE_EXTERNAL_STORAGE permission");
+ mAsserter.is("android.permission.RECORD_AUDIO", permissions[2], "Received RECORD_AUDIO permission");
+ } catch (Exception e) {
+ fFail("Event does not contain expected data: " + e.getMessage());
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java
new file mode 100644
index 000000000..3c22703bc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java
@@ -0,0 +1,379 @@
+/* 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.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+import org.mozilla.gecko.db.SearchHistoryProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class testSearchHistoryProvider extends ContentProviderTest {
+
+ // Translations of "United Kingdom" in several different languages
+ private static final String[] testStrings = {
+ "An Ríocht Aontaithe", // Irish
+ "Angli", // Albanian
+ "Britanniarum Regnum", // Latin
+ "Britio", // Esperanto
+ "Büyük Britanya", // Turkish
+ "Egyesült Királyság", // Hungarian
+ "Erresuma Batua", // Basque
+ "Inggris Raya", // Indonesian
+ "Ir-Renju Unit", // Maltese
+ "Iso-Britannia", // Finnish
+ "Jungtinė Karalystė", // Lithuanian
+ "Lielbritānija", // Latvian
+ "Regatul Unit", // Romanian
+ "Regne Unit", // Catalan, Valencian
+ "Regno Unito", // Italian
+ "Royaume-Uni", // French
+ "Spojené království", // Czech
+ "Spojené kráľovstvo", // Slovak
+ "Storbritannia", // Norwegian
+ "Storbritannien", // Danish
+ "Suurbritannia", // Estonian
+ "Ujedinjeno Kraljevstvo", // Bosnian
+ "United Alaeze", // Igbo
+ "United Kingdom", // English
+ "Vereinigtes Königreich", // German
+ "Verenigd Koninkrijk", // Dutch
+ "Verenigde Koninkryk", // Afrikaans
+ "Vương quốc Anh", // Vietnamese
+ "Wayòm Ini", // Haitian, Haitian Creole
+ "Y Deyrnas Unedig", // Welsh
+ "Združeno kraljestvo", // Slovene
+ "Zjednoczone Królestwo", // Polish
+ "Ηνωμένο Βασίλειο", // Greek (modern)
+ "Великобритания", // Russian
+ "Нэгдсэн Вант Улс", // Mongolian
+ "Обединетото Кралство", // Macedonian
+ "Уједињено Краљевство", // Serbian
+ "Միացյալ Թագավորություն", // Armenian
+ "בריטניה", // Hebrew (modern)
+ "פֿאַראייניקטע מלכות", // Yiddish
+ "المملكة المتحدة", // Arabic
+ "برطانیہ", // Urdu
+ "پادشاهی متحده", // Persian (Farsi)
+ "यूनाइटेड किंगडम", // Hindi
+ "संयुक्त राज्य", // Nepali
+ "যুক্তরাজ্য", // Bengali, Bangla
+ "યુનાઇટેડ કિંગડમ", // Gujarati
+ "ஐக்கிய ராஜ்யம்", // Tamil
+ "สหราชอาณาจักร", // Thai
+ "ສະ​ຫະ​ປະ​ຊາ​ຊະ​ອາ​ນາ​ຈັກ", // Lao
+ "გაერთიანებული სამეფო", // Georgian
+ "イギリス", // Japanese
+ "联合王国" // Chinese
+ };
+
+
+ private static final String DB_NAME = "searchhistory.db";
+
+ /**
+ * Boilerplate alert.
+ * <p/>
+ * Make sure this method is present and that it returns a new
+ * instance of your class.
+ */
+ private static final Callable<ContentProvider> sProviderFactory =
+ new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new SearchHistoryProvider();
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sProviderFactory, BrowserContract.SEARCH_HISTORY_AUTHORITY, DB_NAME);
+ mTests.add(new TestInsert());
+ mTests.add(new TestUnicodeQuery());
+ mTests.add(new TestTimestamp());
+ mTests.add(new TestLimit());
+ mTests.add(new TestDelete());
+ mTests.add(new TestIncrement());
+ }
+
+ public void testSearchHistory() throws Exception {
+ for (Runnable test : mTests) {
+ String testName = test.getClass().getSimpleName();
+ setTestName(testName);
+ mAsserter.dumpLog(
+ "testBrowserProvider: Database empty - Starting " + testName + ".");
+ // Clear the db
+ mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+ test.run();
+ }
+ }
+
+ /**
+ * Verify that we can pass a LIMIT clause using a query parameter.
+ */
+ private class TestLimit extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues cv;
+ for (int i = 0; i < testStrings.length; i++) {
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, testStrings[i]);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ }
+
+ final int limit = 5;
+
+ // Test 1: Handle proper input.
+
+ Uri uri = SearchHistory.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
+ .build();
+
+ Cursor c = mProvider.query(uri, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), limit,
+ String.format("Should have %d results", limit));
+ } finally {
+ c.close();
+ }
+
+ // Test 2: Empty input yields all results.
+
+ uri = SearchHistory.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, "")
+ .build();
+
+ c = mProvider.query(uri, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), testStrings.length, "Should have all results");
+ } finally {
+ c.close();
+ }
+
+ // Test 3: Illegal params.
+
+ String[] illegalParams = new String[] {"a", "-1"};
+ boolean success = true;
+
+ for (String param : illegalParams) {
+ success = true;
+
+ uri = SearchHistory.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, param)
+ .build();
+
+ try {
+ c = mProvider.query(uri, null, null, null, null);
+ success = false;
+ } catch(IllegalArgumentException e) {
+ // noop.
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ mAsserter.ok(success, "LIMIT", param + " should have been an invalid argument");
+ }
+
+ }
+ }
+
+ /**
+ * Verify that we can insert values into the DB, including unicode.
+ */
+ private class TestInsert extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues cv;
+ for (int i = 0; i < testStrings.length; i++) {
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, testStrings[i]);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ }
+
+ final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), testStrings.length,
+ "Should have one row for each insert");
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Verify that we can insert values into the DB, including unicode.
+ */
+ private class TestUnicodeQuery extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String selection = SearchHistory.QUERY + " = ?";
+
+ for (int i = 0; i < testStrings.length; i++) {
+ final ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, testStrings[i]);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, selection,
+ new String[]{ testStrings[i] }, null);
+ try {
+ mAsserter.is(c.getCount(), 1,
+ "Should have one row for insert of " + testStrings[i]);
+ } finally {
+ c.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Verify that timestamps are updated on insert.
+ */
+ private class TestTimestamp extends TestCase {
+ @Override
+ public void test() throws Exception {
+ String insertedTerm = "Courtside Seats";
+ long insertStart;
+ long insertFinish;
+ long t1Db;
+ long t2Db;
+
+ ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, insertedTerm);
+
+ // First check that the DB has a value that is close to the
+ // system time.
+ insertStart = System.currentTimeMillis();
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ insertFinish = System.currentTimeMillis();
+
+ final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ c1.moveToFirst();
+ t1Db = c1.getLong(c1.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+ } finally {
+ c1.close();
+ }
+
+ mAsserter.dumpLog("First insert:");
+ mAsserter.dumpLog(" insertStart " + insertStart);
+ mAsserter.dumpLog(" insertFinish " + insertFinish);
+ mAsserter.dumpLog(" t1Db " + t1Db);
+ mAsserter.ok(t1Db >= insertStart, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+ mAsserter.ok(t1Db <= insertFinish, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, insertedTerm);
+
+ insertStart = System.currentTimeMillis();
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ insertFinish = System.currentTimeMillis();
+
+ final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ c2.moveToFirst();
+ t2Db = c2.getLong(c2.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+ } finally {
+ c2.close();
+ }
+
+ mAsserter.dumpLog("Second insert:");
+ mAsserter.dumpLog(" insertStart " + insertStart);
+ mAsserter.dumpLog(" insertFinish " + insertFinish);
+ mAsserter.dumpLog(" t2Db " + t2Db);
+
+ mAsserter.ok(t2Db >= insertStart, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+ mAsserter.ok(t2Db <= insertFinish, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+ mAsserter.ok(t2Db >= t1Db, "DATE_LAST_VISITED",
+ "Date last visited should be updated on key increment.");
+ }
+ }
+
+ /**
+ * Verify that sending a delete command empties the database.
+ */
+ private class TestDelete extends TestCase {
+ @Override
+ public void test() throws Exception {
+ String insertedTerm = "Courtside Seats";
+
+ ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, insertedTerm);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c1.getCount(), 1, "Should have one value");
+ mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+ } finally {
+ c1.close();
+ }
+
+ final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c2.getCount(), 0, "Should be empty");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ } finally {
+ c2.close();
+ }
+
+ final Cursor c3 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c3.getCount(), 1, "Should have one value");
+ } finally {
+ c3.close();
+ }
+ }
+ }
+
+
+ /**
+ * Ensure that we only increment when the case matches.
+ */
+ private class TestIncrement extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, "omaha");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, "omaha");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ c.moveToFirst();
+ mAsserter.is(c.getCount(), 1, "Should have one result");
+ mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2,
+ "Counter should be 2");
+ } finally {
+ c.close();
+ }
+
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, "Omaha");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), 2, "Should have two results");
+ } finally {
+ c.close();
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java
new file mode 100644
index 000000000..6f82e5c51
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java
@@ -0,0 +1,115 @@
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SuggestClient;
+import org.mozilla.gecko.home.BrowserSearch;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test for search suggestions.
+ * Sends queries from AwesomeBar input and verifies that suggestions match
+ * expected values.
+ */
+public class testSearchSuggestions extends BaseTest {
+ private static final int SUGGESTION_MAX = 3;
+ private static final int SUGGESTION_TIMEOUT = 15000;
+
+ private static final String TEST_QUERY = "foo barz";
+ private static final String SUGGESTION_TEMPLATE = "/robocop/robocop_suggestions.sjs?query=__searchTerms__";
+
+ public void testSearchSuggestions() {
+ // Mock the search system.
+ // The BrowserSearch UI only shows up once a non-empty
+ // search term is entered, but we swizzle in a new factory beforehand.
+ mockSuggestClientFactory();
+
+ blockForGeckoReady();
+
+ // Map of expected values. See robocop_suggestions.sjs.
+ final HashMap<String, ArrayList<String>> suggestMap = new HashMap<String, ArrayList<String>>();
+ buildSuggestMap(suggestMap);
+
+ focusUrlBar();
+
+ // At this point we rely on our swizzling having worked -- which relies
+ // on us not having previously run a search.
+ // The test will fail later if there's already a BrowserSearch object with a
+ // suggest client set, so fail here.
+ BrowserSearch browserSearch = (BrowserSearch) getBrowserSearch();
+ mAsserter.ok(browserSearch == null ||
+ browserSearch.mSuggestClient == null,
+ "There is no existing search client.", "");
+
+ // Now test the incremental suggestions.
+ for (int i = 0; i < TEST_QUERY.length(); i++) {
+ mActions.sendKeys(TEST_QUERY.substring(i, i+1));
+
+ final String query = TEST_QUERY.substring(0, i+1);
+ mSolo.waitForView(R.id.suggestion_text);
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ // Get the first suggestion row.
+ ViewGroup suggestionGroup = (ViewGroup) getActivity().findViewById(R.id.suggestion_layout);
+ if (suggestionGroup == null) {
+ mAsserter.dumpLog("Fail: suggestionGroup is null.");
+ return false;
+ }
+
+ final ArrayList<String> expected = suggestMap.get(query);
+ for (int i = 0; i < expected.size(); i++) {
+ View queryChild = suggestionGroup.getChildAt(i);
+ if (queryChild == null || queryChild.getVisibility() == View.GONE) {
+ mAsserter.dumpLog("Fail: queryChild is null or GONE.");
+ return false;
+ }
+
+ String suggestion = ((TextView) queryChild.findViewById(R.id.suggestion_text)).getText().toString();
+ if (!suggestion.equals(expected.get(i))) {
+ mAsserter.dumpLog("Suggestion '" + suggestion + "' not equal to expected '" + expected.get(i) + "'.");
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }, SUGGESTION_TIMEOUT);
+
+ mAsserter.is(success, true, "Results for query '" + query + "' matched expected suggestions");
+ }
+ }
+
+ private void buildSuggestMap(HashMap<String, ArrayList<String>> suggestMap) {
+ // these values assume SUGGESTION_MAX = 3
+ suggestMap.put("f", new ArrayList<String>() {{ add("f"); add("facebook"); add("fandango"); add("frys"); }});
+ suggestMap.put("fo", new ArrayList<String>() {{ add("fo"); add("forever 21"); add("food network"); add("fox news"); }});
+ suggestMap.put("foo", new ArrayList<String>() {{ add("foo"); add("food network"); add("foothill college"); add("foot locker"); }});
+ suggestMap.put("foo ", new ArrayList<String>() {{ add("foo "); add("foo fighters"); add("foo bar"); add("foo bat"); }});
+ suggestMap.put("foo b", new ArrayList<String>() {{ add("foo b"); add("foo bar"); add("foo bat"); add("foo bay"); }});
+ suggestMap.put("foo ba", new ArrayList<String>() {{ add("foo ba"); add("foo bar"); add("foo bat"); add("foo bay"); }});
+ suggestMap.put("foo bar", new ArrayList<String>() {{ add("foo bar"); }});
+ suggestMap.put("foo barz", new ArrayList<String>() {{ add("foo barz"); }});
+ }
+
+ private void mockSuggestClientFactory() {
+ BrowserSearch.sSuggestClientFactory = new BrowserSearch.SuggestClientFactory() {
+ @Override
+ public SuggestClient getSuggestClient(Context context, String template, int timeout, int max) {
+ final String suggestTemplate = getAbsoluteRawUrl(SUGGESTION_TEMPLATE);
+
+ // This one uses our template, and also doesn't check for network accessibility.
+ return new SuggestClient(context, suggestTemplate, SUGGESTION_TIMEOUT, Integer.MAX_VALUE, false);
+ }
+ };
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java
new file mode 100644
index 000000000..50d173461
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java
@@ -0,0 +1,37 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * Tests that navigating through session history (ex: forward, back) sets the correct UI state.
+ */
+public class testSessionHistory extends UITest {
+ public void testSessionHistory() {
+ GeckoHelper.blockForReady();
+
+ String url = mStringHelper.ROBOCOP_BLANK_PAGE_01_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ url = mStringHelper.ROBOCOP_BLANK_PAGE_02_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ url = mStringHelper.ROBOCOP_BLANK_PAGE_03_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ NavigationHelper.goBack();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ NavigationHelper.goBack();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ NavigationHelper.goForward();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ NavigationHelper.reload();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java
new file mode 100644
index 000000000..5646311b1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java
@@ -0,0 +1,54 @@
+package org.mozilla.gecko.tests;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+/**
+ * Tests session OOM restore behavior.
+ *
+ * Loads a session and tests that it is restored correctly.
+ */
+public class testSessionOOMRestore extends SessionTest {
+ private Session mSession;
+ private static final String PREFS_NAME = "GeckoApp";
+ private static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle";
+
+ @Override
+ public void setActivityIntent(Intent intent) {
+ PageInfo home = new PageInfo(StringHelper.STATIC_ABOUT_HOME_URL);
+ PageInfo page1 = new PageInfo("page1");
+ PageInfo page2 = new PageInfo("page2");
+ PageInfo page3 = new PageInfo("page3");
+ PageInfo page4 = new PageInfo("page4");
+ PageInfo page5 = new PageInfo("page5");
+ PageInfo page6 = new PageInfo("page6");
+
+ SessionTab tab1 = new SessionTab(0, home, page1, page2);
+ SessionTab tab2 = new SessionTab(1, home, page3, page4);
+ SessionTab tab3 = new SessionTab(2, home, page5, page6);
+
+ mSession = new Session(1, tab1, tab2, tab3);
+
+ String sessionString = buildSessionJSON(mSession);
+ writeProfileFile("sessionstore.js", sessionString);
+
+ // This feature is pref-protected to prevent other apps from injecting
+ // a state bundle, so enable it here.
+ SharedPreferences prefs = getInstrumentation().getTargetContext()
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putBoolean(PREFS_ALLOW_STATE_BUNDLE, true).commit();
+
+ Bundle bundle = new Bundle();
+ bundle.putString("privateSession", null);
+ intent.putExtra("stateBundle", bundle);
+
+ super.setActivityIntent(intent);
+ }
+
+ public void testSessionOOMRestore() throws Exception {
+ blockForGeckoReady();
+ verifySessionTabs(mSession);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java
new file mode 100644
index 000000000..f5e5ee099
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java
@@ -0,0 +1,87 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Tests session OOM save behavior.
+ *
+ * Builds a session and tests that the saved state is correct.
+ */
+public class testSessionOOMSave extends SessionTest {
+ private final static int SESSION_TIMEOUT = 25000;
+
+ public void testSessionOOMSave() {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+
+ PageInfo home = new PageInfo(mStringHelper.ABOUT_HOME_URL);
+ PageInfo page1 = new PageInfo("page1");
+ PageInfo page2 = new PageInfo("page2");
+ PageInfo page3 = new PageInfo("page3");
+ PageInfo page4 = new PageInfo("page4");
+ PageInfo page5 = new PageInfo("page5");
+ PageInfo page6 = new PageInfo("page6");
+
+ SessionTab tab1 = new SessionTab(0, home, page1, page2);
+ SessionTab tab2 = new SessionTab(1, home, page3, page4);
+ SessionTab tab3 = new SessionTab(2, home, page5, page6);
+
+ final Session session = new Session(1, tab1, tab2, tab3);
+
+ // Load the tabs into the browser
+ loadSessionTabs(session);
+
+ // Verify sessionstore.js written by Gecko. The session write is
+ // delayed for certain interactions (such as changing the selected
+ // tab), so the file is repeatedly read until it matches the expected
+ // output. Because of the delay, this part of the test takes ~9 seconds
+ // to pass.
+ VerifyJSONCondition verifyJSONCondition = new VerifyJSONCondition(session);
+ boolean success = waitForCondition(verifyJSONCondition, SESSION_TIMEOUT);
+ if (success) {
+ mAsserter.ok(true, "verified session JSON", null);
+ } else {
+ mAsserter.ok(false, "failed to verify session JSON",
+ getStackTraceString(verifyJSONCondition.getLastException()));
+ }
+ }
+
+ private class VerifyJSONCondition implements Condition {
+ private AssertException mLastException;
+ private final NonFatalAsserter mAsserter = new NonFatalAsserter();
+ private final Session mSession;
+
+ public VerifyJSONCondition(Session session) {
+ mSession = session;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ try {
+ String sessionString = readProfileFile("sessionstore.js");
+ if (sessionString == null) {
+ mLastException = new AssertException("Could not read sessionstore.js");
+ return false;
+ }
+
+ verifySessionJSON(mSession, sessionString, mAsserter);
+ } catch (AssertException e) {
+ mLastException = e;
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Gets the last AssertException thrown by verifySessionJSON().
+ *
+ * This is useful to get the stack trace if the test fails.
+ */
+ public AssertException getLastException() {
+ return mLastException;
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
new file mode 100644
index 000000000..0df786136
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
@@ -0,0 +1,265 @@
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.home.HomePager;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * This test covers the opening and content of the Share Link pop-up list
+ * The test opens the Share menu from the app menu, the URL bar, a link context menu and the Awesomescreen tabs
+ */
+public class testShareLink extends AboutHomeTest {
+ String url;
+ String urlTitle = mStringHelper.ROBOCOP_BIG_LINK_TITLE;
+
+ public void testShareLink() {
+ url = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
+ ArrayList<String> shareOptions;
+ blockForGeckoReady();
+
+ // FIXME: This is a temporary hack workaround for a permissions problem.
+ openAboutHomeTab(AboutHomeTabs.HISTORY);
+
+ inputAndLoadUrl(url);
+ verifyUrlBarTitle(url); // Waiting for page title to ensure the page is loaded
+
+ selectMenuItem(mStringHelper.SHARE_LABEL);
+ if (Build.VERSION.SDK_INT >= 14) {
+ // Check for our own sync in the submenu.
+ waitForText("Sync$");
+ } else {
+ waitForText("Share via");
+ }
+
+ // Get list of current available share activities and verify them
+ shareOptions = getShareOptions();
+ ArrayList<String> displayedOptions = getShareOptionsList();
+ for (String option:shareOptions) {
+ // Verify if the option is present in the list of displayed share options
+ mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option found", option);
+ }
+
+ // Test share from the urlbar context menu
+ mSolo.goBack(); // Close the share menu
+ mSolo.clickLongOnText(urlTitle);
+ verifySharePopup(shareOptions,"urlbar");
+
+ // The link has a 60px height, so let's try to hit the middle
+ float top = mDriver.getGeckoTop() + 30 * mDevice.density;
+ float left = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() / 2;
+ mSolo.clickLongOnScreen(left, top);
+ verifySharePopup("Share Link",shareOptions,"Link");
+
+ // Test the share popup in the Bookmarks page
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+
+ final ListView bookmarksList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ mAsserter.is(waitForNonEmptyListToLoad(bookmarksList), true, "list is properly loaded");
+
+ int headerViewsCount = bookmarksList.getHeaderViewsCount();
+ View bookmarksItem = bookmarksList.getChildAt(headerViewsCount);
+ if (bookmarksItem == null) {
+ mAsserter.dumpLog("no child at index " + headerViewsCount + "; waiting for one...");
+ Condition listWaitCondition = new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ if (bookmarksList.getChildAt(bookmarksList.getHeaderViewsCount()) == null)
+ return false;
+ return true;
+ }
+ };
+ waitForCondition(listWaitCondition, MAX_WAIT_MS);
+ headerViewsCount = bookmarksList.getHeaderViewsCount();
+ bookmarksItem = bookmarksList.getChildAt(headerViewsCount);
+ }
+
+ mSolo.clickLongOnView(bookmarksItem);
+ verifySharePopup(shareOptions,"bookmarks");
+
+ // Prepopulate top sites with history items to overflow tiles.
+ // We are trying to move away from using reflection and doing more black-box testing.
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_04_URL));
+ if (mDevice.type.equals("tablet")) {
+ // Tablets have more tile spaces to fill.
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_05_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_SEARCH_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_TEXT_PAGE_URL));
+ }
+
+ // Test the share popup in Top Sites.
+ openAboutHomeTab(AboutHomeTabs.TOP_SITES);
+
+ // Scroll down a bit so that the top sites list has more items on screen.
+ int width = mDriver.getGeckoWidth();
+ int height = mDriver.getGeckoHeight();
+ mActions.drag(width / 2, width / 2, height - 10, height / 2);
+
+ ListView topSitesList = findListViewWithTag(HomePager.LIST_TAG_TOP_SITES);
+ mAsserter.is(waitForNonEmptyListToLoad(topSitesList), true, "list is properly loaded");
+ View mostVisitedItem = topSitesList.getChildAt(topSitesList.getHeaderViewsCount());
+ mSolo.clickLongOnView(mostVisitedItem);
+ verifySharePopup(shareOptions,"top_sites");
+
+ // Test the share popup in the history tab
+ openAboutHomeTab(AboutHomeTabs.HISTORY);
+
+ ListView mostRecentList = findListViewWithTag(HomePager.LIST_TAG_HISTORY);
+ mAsserter.is(waitForNonEmptyListToLoad(mostRecentList), true, "list is properly loaded");
+
+ // Getting second child after header views because the first is the "Today" label
+ View mostRecentItem = mostRecentList.getChildAt(mostRecentList.getHeaderViewsCount() + 1);
+ mSolo.clickLongOnView(mostRecentItem);
+ verifySharePopup(shareOptions,"most recent");
+ }
+
+ public void verifySharePopup(ArrayList<String> shareOptions, String openedFrom) {
+ verifySharePopup("Share", shareOptions, openedFrom);
+ }
+
+ public void verifySharePopup(String shareItemText, ArrayList<String> shareOptions, String openedFrom) {
+ waitForText(shareItemText);
+ mSolo.clickOnText(shareItemText);
+ waitForText("Share via");
+ ArrayList<String> displayedOptions = getSharePopupOption();
+ for (String option:shareOptions) {
+ // Verify if the option is present in the list of displayed share options
+ mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option for " + openedFrom + (openedFrom.equals("urlbar") ? "" : " item") + " found", option);
+ }
+ mSolo.goBack();
+ /**
+ * Adding a wait for the page title to make sure the Awesomebar will be dismissed
+ * Because of Bug 712370 the Awesomescreen will be dismissed when the Share Menu is closed
+ * so there is no need for handling this different depending on where the share menu was invoked from
+ * TODO: Look more into why the delay is needed here now and it was working before
+ */
+ waitForText(urlTitle);
+ }
+
+ // Create a SEND intent and get the possible activities offered
+ public ArrayList<String> getShareOptions() {
+ ArrayList<String> shareOptions = new ArrayList<>();
+ Activity currentActivity = getActivity();
+ final Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, url);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Robocop Blank 01");
+ shareIntent.setType("text/plain");
+ PackageManager pm = currentActivity.getPackageManager();
+ List<ResolveInfo> activities = pm.queryIntentActivities(shareIntent, 0);
+ for (ResolveInfo activity : activities) {
+ shareOptions.add(activity.loadLabel(pm).toString());
+ }
+ return shareOptions;
+ }
+
+ // Traverse the group of views, adding strings from TextViews to the list.
+ private void getGroupTextViews(ViewGroup group, ArrayList<String> list) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View child = group.getChildAt(i);
+ if (child instanceof AbsListView) {
+ getGroupTextViews((AbsListView)child, list);
+ } else if (child instanceof ViewGroup) {
+ getGroupTextViews((ViewGroup)child, list);
+ } else if (child instanceof TextView) {
+ String viewText = ((TextView)child).getText().toString();
+ if (viewText != null && viewText.length() > 0) {
+ list.add(viewText);
+ }
+ }
+ }
+ }
+
+ // Traverse the group of views, adding strings from TextViews to the list.
+ // This override is for AbsListView, which has adapters. If adapters are
+ // available, it is better to use them so that child views that are not
+ // yet displayed can be examined.
+ private void getGroupTextViews(AbsListView group, ArrayList<String> list) {
+ for (int i = 0; i < group.getAdapter().getCount(); i++) {
+ View child = group.getAdapter().getView(i, null, group);
+ if (child instanceof AbsListView) {
+ getGroupTextViews((AbsListView)child, list);
+ } else if (child instanceof ViewGroup) {
+ getGroupTextViews((ViewGroup)child, list);
+ } else if (child instanceof TextView) {
+ String viewText = ((TextView)child).getText().toString();
+ if (viewText != null && viewText.length() > 0) {
+ list.add(viewText);
+ }
+ }
+ }
+ }
+
+ public ArrayList<String> getSharePopupOption() {
+ ArrayList<String> displayedOptions = new ArrayList<>();
+ AbsListView shareMenu = getDisplayedShareList();
+ getGroupTextViews(shareMenu, displayedOptions);
+ return displayedOptions;
+ }
+
+ public ArrayList<String> getShareSubMenuOption() {
+ ArrayList<String> displayedOptions = new ArrayList<>();
+ AbsListView shareMenu = getDisplayedShareList();
+ getGroupTextViews(shareMenu, displayedOptions);
+ return displayedOptions;
+ }
+
+ public ArrayList<String> getShareOptionsList() {
+ if (Build.VERSION.SDK_INT >= 14) {
+ return getShareSubMenuOption();
+ } else {
+ return getSharePopupOption();
+ }
+ }
+
+ private boolean optionDisplayed(String shareOption, ArrayList<String> displayedOptions) {
+ for (String displayedOption: displayedOptions) {
+ if (shareOption.equals(displayedOption)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private AbsListView mViewGroup;
+
+ private AbsListView getDisplayedShareList() {
+ mViewGroup = null;
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ ArrayList<View> views = mSolo.getCurrentViews();
+ for (View view : views) {
+ // List may be displayed in different view formats.
+ // On JB, GridView is common; on ICS-, ListView is common.
+ if (view instanceof ListView ||
+ view instanceof GridView) {
+ mViewGroup = (AbsListView)view;
+ return true;
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success,"Got the displayed share options?", "Got the share options view");
+ return mViewGroup;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
new file mode 100644
index 000000000..893f98a51
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
@@ -0,0 +1,52 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+
+public class testSnackbarAPI extends JavascriptTest implements NativeEventListener {
+ // Snackbar.LENGTH_INDEFINITE: To avoid tests depending on the android design support library
+ private static final int SNACKBAR_LENGTH_INDEFINITE = -2;
+
+ public testSnackbarAPI() {
+ super("testSnackbarAPI.js");
+ }
+
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ mAsserter.is(event, "Snackbar:Show", "Received Snackbar:Show event");
+
+ try {
+ mAsserter.is(message.getString("message"), "This is a Snackbar", "Snackbar message");
+ mAsserter.is(message.getInt("duration"), SNACKBAR_LENGTH_INDEFINITE, "Snackbar duration");
+
+ NativeJSObject action = message.getObject("action");
+
+ mAsserter.is(action.getString("label"), "Click me", "Snackbar action label");
+
+ } catch (Exception e) {
+ fFail("Event does not contain expected data: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this, "Snackbar:Show");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Snackbar:Show");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java
new file mode 100644
index 000000000..7f7b47450
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java
@@ -0,0 +1,40 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.GeckoClickHelper;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+/**
+ * This test ensures the back/forward state is correct when switching to loading pages
+ * to prevent regressions like Bug 1124190.
+ */
+public class testStateWhileLoading extends UITest {
+ public void testStateWhileLoading() {
+ if (!DeviceHelper.isTablet()) {
+ // This test case only covers tablets currently.
+ return;
+ }
+
+ GeckoHelper.blockForReady();
+
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_LINK_TO_SLOW_LOADING);
+
+ GeckoClickHelper.openCentralizedLinkInNewTab();
+
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ mTabStrip.switchToTab(1);
+
+ // Assert that the state of the back button is correct
+ // after switching to the new (still loading) tab.
+ mToolbar.assertBackButtonIsNotEnabled();
+ }
+ });
+
+ // Assert that the state of the back button is still correct after the page has loaded.
+ mToolbar.assertBackButtonIsNotEnabled();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java
new file mode 100644
index 000000000..ac551b97f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java
@@ -0,0 +1,90 @@
+/* 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.robotium.solo.Condition;
+
+/*
+ * This test enables (checkbox checked) the Fennec setting to contribute to MLS, then waits for
+ * a response Intent from the stumbler service to confirm it has started. Then, it disables the
+ * service in the setting, and waits for confirmation that the servie has stopped.
+ */
+public class testStumblerSetting extends BaseTest {
+ boolean mIsEnabled;
+
+ public void testStumblerSetting() {
+ if (!AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
+ mAsserter.info("Checking stumbler build config.", "Skipping test as Stumbler is not enabled in this build.");
+ return;
+ }
+
+ blockForGeckoReady();
+
+ selectMenuItem(mStringHelper.SETTINGS_LABEL);
+ mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL),
+ "The Settings menu did not load", mStringHelper.SETTINGS_LABEL);
+
+ String section = "^" + mStringHelper.MOZILLA_SECTION_LABEL + "$";
+ waitForEnabledText(section);
+ mSolo.clickOnText(section);
+
+ String itemTitle = "^" + mStringHelper.LOCATION_SERVICES_LABEL + "$";
+ boolean foundText = waitForPreferencesText(itemTitle);
+ mAsserter.ok(foundText, "Waiting for settings item " + itemTitle + " in section " + section,
+ "The " + itemTitle + " option is present in section " + section);
+
+ BroadcastReceiver enabledDisabledReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(AppGlobals.ACTION_TEST_SETTING_ENABLED)) {
+ mIsEnabled = true;
+ } else {
+ mIsEnabled = false;
+ }
+ }
+ };
+
+ Context context = getInstrumentation().getTargetContext();
+ IntentFilter intentFilter = new IntentFilter(AppGlobals.ACTION_TEST_SETTING_ENABLED);
+ intentFilter.addAction(AppGlobals.ACTION_TEST_SETTING_DISABLED);
+ context.registerReceiver(enabledDisabledReceiver, intentFilter);
+
+ boolean checked = mSolo.isCheckBoxChecked(itemTitle);
+ try {
+ mAsserter.ok(!checked, "Checking stumbler setting is unchecked.", "Unchecked as expected.");
+
+ waitForEnabledText(itemTitle);
+ mSolo.clickOnText(itemTitle);
+
+ mSolo.waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return mIsEnabled;
+ }
+ }, 15000);
+
+ mAsserter.ok(mIsEnabled, "Checking if stumbler became enabled.", "Stumbler is enabled.");
+ mSolo.clickOnText(itemTitle);
+
+ mSolo.waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !mIsEnabled;
+ }
+ }, 15000);
+
+ mAsserter.ok(!mIsEnabled, "Checking if stumbler became disabled.", "Stumbler is disabled.");
+ } finally {
+ context.unregisterReceiver(enabledDisabledReceiver);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java
new file mode 100644
index 000000000..6cb42f37c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java
@@ -0,0 +1,116 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.db.BrowserDB;
+
+import android.content.ContentResolver;
+import android.graphics.Color;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test for thumbnail updates.
+ * - loads 2 pages, each of which yield an HTTP 200
+ * - verifies thumbnails are updated for both pages
+ * - loads pages again; first page yields HTTP 200, second yields HTTP 404
+ * - verifies thumbnail is updated for HTTP 200, but not HTTP 404
+ * - finally, test that BrowserDB.removeThumbnails drops the thumbnails
+ */
+public class testThumbnails extends BaseTest {
+ public void testThumbnails() {
+ final String site1Url = getAbsoluteUrl("/robocop/robocop_404.sjs?type=changeColor");
+ final String site2Url = getAbsoluteUrl("/robocop/robocop_404.sjs?type=do404");
+ final String site1Title = "changeColor";
+ final String site2Title = "do404";
+
+ // the session snapshot runnable is run 500ms after document stop. a
+ // 3000ms delay gives us 2.5 seconds to take the screenshot, which
+ // should be plenty of time, even on slow devices
+ final int thumbnailDelay = 3000;
+
+ blockForGeckoReady();
+
+ // load sites; both will return HTTP 200 with a green background
+ inputAndLoadUrl(site1Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(site2Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ waitForCondition(new ThumbnailTest(site1Title, Color.GREEN), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.GREEN, "Top site thumbnail updated for HTTP 200");
+ waitForCondition(new ThumbnailTest(site2Title, Color.GREEN), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail updated for HTTP 200");
+
+ // load sites again; both will have red background, and do404 will return HTTP 404
+ inputAndLoadUrl(site1Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(site2Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ waitForCondition(new ThumbnailTest(site1Title, Color.RED), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.RED, "Top site thumbnail updated for HTTP 200");
+ waitForCondition(new ThumbnailTest(site2Title, Color.GREEN), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail not updated for HTTP 404");
+
+ // test dropping thumbnails
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter);
+ final BrowserDB db = helper.getProfileDB();
+
+ // check that the thumbnail is non-null
+ byte[] thumbnailData = db.getThumbnailForUrl(resolver, site1Url);
+ mAsserter.ok(thumbnailData != null && thumbnailData.length > 0, "Checking for thumbnail data", "No thumbnail data found");
+ // drop thumbnails
+ db.removeThumbnails(resolver);
+ // check that the thumbnail is now null
+ thumbnailData = db.getThumbnailForUrl(resolver, site1Url);
+ mAsserter.ok(thumbnailData == null || thumbnailData.length == 0, "Checking for thumbnail data", "Thumbnail data found");
+ }
+
+ private class ThumbnailTest implements Condition {
+ private final String mTitle;
+ private final int mColor;
+
+ public ThumbnailTest(String title, int color) {
+ mTitle = title;
+ mColor = color;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ return getTopSiteThumbnailColor(mTitle) == mColor;
+ }
+ }
+
+ private int getTopSiteThumbnailColor(String title) {
+ // This test is not currently run, so this just needs to compile.
+ return -1;
+// ViewGroup topSites = (ViewGroup) getActivity().findViewById(mTopSitesId);
+// if (topSites != null) {
+// final int childCount = topSites.getChildCount();
+// for (int i = 0; i < childCount; i++) {
+// View child = topSites.getChildAt(i);
+// if (child != null) {
+// TextView titleView = (TextView) child.findViewById(R.id.title);
+// if (titleView != null) {
+// if (titleView.getText().equals(title)) {
+// ImageView thumbnailView = (ImageView) child.findViewById(R.id.thumbnail);
+// if (thumbnailView != null) {
+// Bitmap thumbnail = ((BitmapDrawable) thumbnailView.getDrawable()).getBitmap();
+// return thumbnail.getPixel(0, 0);
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find mThumbnailId: "+R.id.thumbnail);
+// }
+// }
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find R.id.title: "+R.id.title);
+// }
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: skipped null child at index "+i);
+// }
+// }
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find mTopSitesId: " + mTopSitesId);
+// }
+// return -1;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java
new file mode 100644
index 000000000..c27ff0094
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java
@@ -0,0 +1,65 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class testTrackingProtection extends JavascriptTest implements GeckoEventListener {
+ private String mLastTracking;
+
+ public testTrackingProtection() {
+ super("testTrackingProtection.js");
+ }
+
+ @Override
+ public void handleMessage(String event, final JSONObject message) {
+ if (event.equals("Content:SecurityChange")) {
+ try {
+ JSONObject identity = message.getJSONObject("identity");
+ JSONObject mode = identity.getJSONObject("mode");
+ mLastTracking = mode.getString("tracking");
+ mAsserter.dumpLog("Security change (tracking): " + mLastTracking);
+ } catch (Exception e) {
+ fFail("Can't extract tracking state from JSON");
+ }
+ }
+
+ if (event.equals("Test:Expected")) {
+ try {
+ String expected = message.getString("expected");
+ mAsserter.is(mLastTracking, expected, "Tracking matched expectation");
+ mAsserter.dumpLog("Testing (tracking): " + mLastTracking + " = " + expected);
+ } catch (Exception e) {
+ fFail("Can't extract expected state from JSON");
+ }
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Content:SecurityChange",
+ "Test:Expected");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Content:SecurityChange",
+ "Test:Expected");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java
new file mode 100644
index 000000000..30d7c169c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java
@@ -0,0 +1,56 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract.Event;
+import org.mozilla.gecko.TelemetryContract.Method;
+import org.mozilla.gecko.TelemetryContract.Reason;
+import org.mozilla.gecko.TelemetryContract.Session;
+
+import android.util.Log;
+
+public class testUITelemetry extends JavascriptTest {
+ public testUITelemetry() {
+ super("testUITelemetry.js");
+ }
+
+ @Override
+ public void testJavascript() throws Exception {
+ blockForGeckoReady();
+
+ // We can't run these tests unless telemetry is turned on --
+ // the events will be dropped on the floor.
+ Log.i("GeckoTest", "Enabling telemetry.");
+ PrefsHelper.setPref(AppConstants.TELEMETRY_PREF_NAME, true);
+
+ Log.i("GeckoTest", "Adding telemetry events.");
+ try {
+ Telemetry.sendUIEvent(Event._TEST1, Method._TEST1);
+ Telemetry.startUISession(Session._TEST_STARTED_TWICE);
+ Telemetry.sendUIEvent(Event._TEST2, Method._TEST1);
+
+ // We can only start one session per name, so this call should be ignored.
+ Telemetry.startUISession(Session._TEST_STARTED_TWICE);
+
+ Telemetry.sendUIEvent(Event._TEST2, Method._TEST2);
+ Telemetry.startUISession(Session._TEST_STOPPED_TWICE);
+ Telemetry.sendUIEvent(Event._TEST3, Method._TEST1, "foobarextras");
+ Telemetry.stopUISession(Session._TEST_STARTED_TWICE, Reason._TEST1);
+ Telemetry.sendUIEvent(Event._TEST4, Method._TEST1, "barextras");
+ Telemetry.stopUISession(Session._TEST_STOPPED_TWICE, Reason._TEST2);
+
+ // This session is already stopped, so this call should be ignored.
+ Telemetry.stopUISession(Session._TEST_STOPPED_TWICE, Reason._TEST_IGNORED);
+
+ // Method defaults to Method.NONE
+ Telemetry.sendUIEvent(Event._TEST1);
+ } catch (Exception e) {
+ Log.e("GeckoTest", "Oops.", e);
+ }
+
+ Log.i("GeckoTest", "Running remaining JS test code.");
+ super.testJavascript();
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java
new file mode 100644
index 000000000..aaaded4c8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java
@@ -0,0 +1,265 @@
+/* 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 static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+public class testUnifiedTelemetryClientId extends JavascriptBridgeTest {
+ private static final String TEST_JS = "testUnifiedTelemetryClientId.js";
+
+ private static final String CLIENT_ID_PATH = "datareporting/state.json";
+ private static final String FHR_DIR_PATH = "healthreport/";
+ private static final String FHR_CLIENT_ID_PATH = FHR_DIR_PATH + "state.json";
+
+ private GeckoProfile profile;
+ private File profileDir;
+ private File[] filesToDeleteOnReset;
+
+ public void setUp() throws Exception {
+ super.setUp();
+ profile = getTestProfile();
+ profileDir = profile.getDir(); // Assumes getDir is tested.
+ filesToDeleteOnReset = new File[] {
+ getClientIdFile(),
+ getFHRClientIdFile(),
+ getFHRClientIdParentDir(),
+ };
+ }
+
+ public void tearDown() throws Exception {
+ // Don't clear cache because who knows what state Gecko is in.
+ deleteClientIDFiles();
+ super.tearDown();
+ }
+
+ private void deleteClientIDFiles() {
+ Log.d(LOGTAG, "deleteClientIDFiles: begin");
+
+ for (final File file : filesToDeleteOnReset) {
+ file.delete(); // can't check return value because the file may not exist before deletion.
+ fAssertFalse("Deleted file in reset does not exist", file.exists()); // sanity check.
+ }
+
+ Log.d(LOGTAG, "deleteClientIDFiles: end");
+ }
+
+ public void testUnifiedTelemetryClientId() throws Exception {
+ blockForReadyAndLoadJS(TEST_JS);
+ fAssertTrue("Profile directory exists", profileDir.exists());
+
+ // Important note: we cannot stop Gecko from running while we run this test and
+ // Gecko is capable of creating client ID files while we run this test. However,
+ // ClientID.jsm will not touch modify the client ID files on disk if its client
+ // ID cache is filled. As such, we prevent it from touching the disk by intentionally
+ // priming the cache & deleting the files it added now, and resetting the cache at the
+ // latest possible moment before we attempt to test the client ID file.
+ //
+ // This is fragile because it relies on the ClientID cache's implementation, however,
+ // some alternatives (e.g. changing file system permissions, file locking) are worse
+ // because they can fire error handling code, which is not currently under test.
+ //
+ // First, we delete the test files - we don't want the cache prime to fail which could happen if
+ // these files are around & corrupted from a previous test/install. Then we prime the cache,
+ // and delete the files the cache priming added, so the tests are ready to add their own version
+ // of these files.
+ deleteClientIDFiles();
+ primeJsClientIdCache();
+ deleteClientIDFiles();
+
+ // TODO: If these tests weren't so expensive to run in automation,
+ // this should be two separate tests to avoid storing state between tests.
+ testJavaCreatesClientId(); // leaves cache filled.
+ deleteClientIDFiles();
+ testJsCreatesClientId(); // leaves cache filled.
+ deleteClientIDFiles();
+ testJavaMigratesFromHealthReport(); // leaves cache filled.
+ deleteClientIDFiles();
+ testJsMigratesFromHealthReport(); // leaves cache filled.
+
+ getJS().syncCall("endTest");
+ }
+
+ /**
+ * Scenario: Java creates client ID:
+ * * Fennec starts on fresh profile
+ * * Java code creates the client ID in datareporting/state.json
+ * * Js accesses client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJavaCreatesClientId() throws Exception {
+ Log.d(LOGTAG, "testJavaCreatesClientId: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+
+ final String clientIdFromJava = getClientIdFromJava();
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ // allow for the case where gecko updates the client ID after the first get
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ fAssertTrue("Client ID from Java equals ID from JS",
+ clientIdFromJava.equals(clientIdFromJS) ||
+ clientIdFromJavaAgain.equals(clientIdFromJS));
+
+ final String clientIdFromJSCache = getClientIdFromJS();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from JS cache", clientIdFromJavaAgain, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", clientIdFromJavaAgain, clientIdFromJSFileAgain);
+ }
+
+ /**
+ * Scenario: JS creates client ID
+ * * Fennec starts on a fresh profile
+ * * Js creates the client ID in datareporting/state.json
+ * * Java access the client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJsCreatesClientId() throws Exception {
+ Log.d(LOGTAG, "testJsCreatesClientId: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ final String clientIdFromJava = getClientIdFromJava();
+ fAssertEquals("Client ID from JS equals ID from Java", clientIdFromJS, clientIdFromJava);
+
+ final String clientIdFromJSCache = getClientIdFromJS();
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from JS cache", clientIdFromJS, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", clientIdFromJS, clientIdFromJSFileAgain);
+ fAssertEquals("Same client ID retrieved from Java", clientIdFromJS, clientIdFromJavaAgain);
+ }
+
+ /**
+ * Scenario: Java migrates client ID from FHR client ID file.
+ * * FHR file already exists.
+ * * Fennec starts on fresh profile
+ * * Java code merges client ID to datareporting/state.json from healthreport/state.json
+ * * Js accesses client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJavaMigratesFromHealthReport() throws Exception {
+ Log.d(LOGTAG, "testJavaMigratesFromHealthReport: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+ fAssertFalse("Health report file does not exist yet", getFHRClientIdFile().exists());
+
+ final String expectedClientId = UUID.randomUUID().toString();
+ createFHRClientIdFile(expectedClientId);
+
+ final String clientIdFromJava = getClientIdFromJava();
+ fAssertEquals("Health report client ID merged by Java", expectedClientId, clientIdFromJava);
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ fAssertEquals("Merged client ID read by JS", expectedClientId, clientIdFromJS);
+
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ final String clientIdFromJSCache = getClientIdFromJS();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from Java", expectedClientId, clientIdFromJavaAgain);
+ fAssertEquals("Same client ID retrieved from JS cache", expectedClientId, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", expectedClientId, clientIdFromJSFileAgain);
+ }
+
+ /**
+ * Scenario: JS merges client ID from FHR client ID file.
+ * * FHR file already exists.
+ * * Fennec starts on a fresh profile
+ * * Js merges the client ID to datareporting/state.json from healthreport/state.json
+ * * Java access the client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJsMigratesFromHealthReport() throws Exception {
+ Log.d(LOGTAG, "testJsMigratesFromHealthReport: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+ fAssertFalse("Health report file does not exist yet", getFHRClientIdFile().exists());
+
+ final String expectedClientId = UUID.randomUUID().toString();
+ createFHRClientIdFile(expectedClientId);
+
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ fAssertEquals("Health report client ID merged by JS", expectedClientId, clientIdFromJS);
+ final String clientIdFromJava = getClientIdFromJava();
+ fAssertEquals("Merged client ID read by Java", expectedClientId, clientIdFromJava);
+
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ final String clientIdFromJSCache = getClientIdFromJS();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from Java", expectedClientId, clientIdFromJavaAgain);
+ fAssertEquals("Same client ID retrieved from JS cache", expectedClientId, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", expectedClientId, clientIdFromJSFileAgain);
+ }
+
+ private String getClientIdFromJava() throws IOException {
+ // This assumes implementation details: it assumes the client ID
+ // file is created when Java attempts to retrieve it if it does not exist.
+ final String clientId = profile.getClientId();
+ fAssertNotNull("Returned client ID is not null", clientId);
+ fAssertTrue("Client ID file exists after getClientId call", getClientIdFile().exists());
+ return clientId;
+ }
+
+ private String getClientIdFromJS() {
+ return getBlockingFromJsString("clientId");
+ }
+
+ /**
+ * Must be called after Gecko is loaded.
+ */
+ private void primeJsClientIdCache() {
+ // Not the cleanest way, but it works.
+ getClientIdFromJS();
+ }
+
+ /**
+ * Resets the client ID cache in ClientID.jsm. This method *must* be called after
+ * Gecko is loaded or else this method will hang.
+ *
+ * Note: we do this for very specific reasons - see the comment in the test method
+ * ({@link #testUnifiedTelemetryClientId()}) for more.
+ */
+ private void resetJSCache() {
+ // HACK: the backing JS method is a promise with no return value. Rather than writing a method
+ // to handle this (for time reasons), I call the get String method and don't access the return value.
+ getBlockingFromJsString("reset");
+ }
+
+ private File getClientIdFile() {
+ return new File(profileDir, CLIENT_ID_PATH);
+ }
+
+ private File getFHRClientIdParentDir() {
+ return new File(profileDir, FHR_DIR_PATH);
+ }
+
+ private File getFHRClientIdFile() {
+ return new File(profileDir, FHR_CLIENT_ID_PATH);
+ }
+
+ private void createFHRClientIdFile(final String clientId) throws JSONException {
+ fAssertTrue("FHR directory created", getFHRClientIdParentDir().mkdirs());
+
+ final JSONObject obj = new JSONObject();
+ obj.put("clientID", clientId);
+ profile.writeFile(FHR_CLIENT_ID_PATH, obj.toString());
+ fAssertTrue("FHR client ID file exists after writing", getFHRClientIdFile().exists());
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java
new file mode 100644
index 000000000..5164815c4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java
@@ -0,0 +1,9 @@
+package org.mozilla.gecko.tests;
+
+
+
+public class testVideoControls extends JavascriptTest {
+ public testVideoControls() {
+ super("testVideoControls.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java
new file mode 100644
index 000000000..f5a54a0e9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java
@@ -0,0 +1,105 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+import android.net.Uri;
+
+/**
+ * A test to ensure that when an input field is focused, it is not obscured by the VKB.
+ * - Loads a page with an input field past the bottom of the visible area.
+ * - scrolls down to make the input field visible at the bottom of the screen.
+ * - taps on the input field to bring up the VKB
+ * - verifies that the input field is still visible.
+ */
+public class testVkbOverlap extends PixelTest {
+ private static final int CURSOR_BLINK_PERIOD = 500;
+ private static final int LESS_THAN_CURSOR_BLINK_PERIOD = CURSOR_BLINK_PERIOD - 50;
+ private static final int PAGE_SETTLE_TIME = 5000;
+
+ public void testVkbOverlap() {
+ blockForGeckoReady();
+ testSetup("initial-scale=1.0, user-scalable=no", false);
+ testSetup("initial-scale=1.0", false);
+ testSetup("", "phone".equals(mDevice.type));
+ }
+
+ private void testSetup(String viewport, boolean shouldZoom) {
+ loadAndPaint(getAbsoluteUrl("/robocop/test_viewport.sjs?metadata=" + Uri.encode(viewport)));
+
+ // scroll to the bottom of the page and let it settle
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+ meh.dragSync(10, 150, 10, 50);
+
+ // the input field has a green background, so let's count the number of green pixels
+ int greenPixelCount = 0;
+
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ greenPixelCount = countGreenPixels(painted);
+ } finally {
+ painted.close();
+ }
+
+ mAsserter.ok(greenPixelCount > 0, "testInputVisible", "Found " + greenPixelCount + " green pixels after scrolling");
+
+ paintExpecter = mActions.expectPaint();
+ // the input field should be in the bottom-left corner, so tap thereabouts
+ meh.tap(5, mDriver.getGeckoHeight() - 5);
+
+ // After tapping in the input field, the page needs some time to do stuff, like draw and undraw the focus highlight
+ // on the input field, trigger the VKB, process any resulting events generated by the system, and scroll the page. So
+ // we give it a few seconds to do all that. We are sufficiently generous with our definition of "few seconds" to
+ // prevent intermittent test failures.
+ try {
+ Thread.sleep(PAGE_SETTLE_TIME);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+
+ // now that the focus is in the text field we will repaint every 500ms as the cursor blinks, so we need to use a smaller
+ // "no paints" threshold to consider the page painted
+ paintExpecter.blockUntilClear(LESS_THAN_CURSOR_BLINK_PERIOD);
+ paintExpecter.unregisterListener();
+ painted = mDriver.getPaintedSurface();
+ try {
+ // if the vkb scrolled into view as expected, then the number of green pixels now visible should be about the
+ // same as it was before, since the green pixels indicate the text input is in view. use a fudge factor of 0.9 to
+ // account for borders and such of the text input which might still be out of view.
+ int newCount = countGreenPixels(painted);
+
+ // if zooming is allowed, the number of green pixels visible should have increased substantially
+ if (shouldZoom) {
+ mAsserter.ok(newCount > greenPixelCount * 1.5, "testVkbOverlap", "Found " + newCount + " green pixels after tapping; expected " + greenPixelCount);
+ } else {
+ mAsserter.ok((Math.abs(greenPixelCount - newCount) / greenPixelCount < 0.1), "testVkbOverlap", "Found " + newCount + " green pixels after tapping; expected " + greenPixelCount);
+ }
+ } finally {
+ painted.close();
+ }
+ }
+
+ private int countGreenPixels(PaintedSurface painted) {
+ int count = 0;
+ for (int y = painted.getHeight() - 1; y >= 0; y--) {
+ for (int x = painted.getWidth() - 1; x >= 0; x--) {
+ int pixel = painted.getPixelAt(x, y);
+ int r = (pixel >> 16) & 0xFF;
+ int g = (pixel >> 8) & 0xFF;
+ int b = (pixel & 0xFF);
+ if (g > (r + 0x30) && g > (b + 0x30)) {
+ // there's more green in this pixel than red or blue, so count it.
+ // the reason this is so hacky-looking is because even though green is supposed to
+ // be (r,g,b) = (0x00, 0x80, 0x00), the GL readback ends up coming back quite
+ // different.
+ count++;
+ }
+ // uncomment for debugging:
+ // if (pixel != -1) mAsserter.dumpLog("Pixel at " + x + ", " + y + ": " + Integer.toString(pixel, 16));
+ }
+ }
+ return count;
+ }
+}