Bug 1395409: Add testActivityStreamPocketReferrer. r=liuche
authorMichael Comella <michael.l.comella@gmail.com>
Wed, 30 Aug 2017 17:33:08 -0700
changeset 384558 ffd9345660e51ee97c0d753f6e73cbe4e51f8e41
parent 384557 9edb781a90587e4dbf15eb111b2c6a2faac3d5b3
child 384559 ee6479d783a60ad7ba4aad54a50bc8c1d77a894e
push id32631
push userarchaeopteryx@coole-files.de
push dateThu, 05 Oct 2017 08:51:33 +0000
treeherdermozilla-central@66042a706980 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche
bugs1395409
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1395409: Add testActivityStreamPocketReferrer. r=liuche MozReview-Commit-ID: FlcMG5IewRH
mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
mobile/android/tests/browser/robocop/robocop.ini
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamPocketReferrer.java
mobile/android/tests/browser/robocop/testActivityStreamPocketReferrer.js
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
@@ -3,27 +3,29 @@
  * 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.activitystream.homepanel.topstories;
 
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.AsyncTaskLoader;
 import android.text.TextUtils;
 import android.util.Log;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.activitystream.homepanel.StreamRecyclerAdapter;
 import org.mozilla.gecko.activitystream.homepanel.model.TopStory;
+import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.ProxySelector;
 
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.URI;
@@ -48,16 +50,23 @@ import java.util.concurrent.TimeUnit;
  * and include the Pocket API token in the token file.
  */
 
 public class PocketStoriesLoader extends AsyncTaskLoader<List<TopStory>> {
     public static String LOGTAG = "PocketStoriesLoader";
 
     public static final String POCKET_REFERRER_URI = "https://getpocket.com/recommendations";
 
+    @RobocopTarget
+    @VisibleForTesting public static final String PLACEHOLDER_TITLE = "Placeholder ";
+    private static final String DEFAULT_PLACEHOLDER_URL = "https://www.mozilla.org/#";
+    static {
+        setPlaceholderUrl(DEFAULT_PLACEHOLDER_URL);
+    }
+
     // Pocket SharedPreferences keys
     private static final String POCKET_PREFS_FILE = "PocketStories";
     private static final String CACHE_TIMESTAMP_MILLIS_PREFIX = "timestampMillis-";
     private static final String STORIES_CACHE_PREFIX = "storiesCache-";
 
     // Pocket API params and defaults
     private static final String GLOBAL_ENDPOINT = "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs";
     private static final String PARAM_APIKEY = "consumer_key";
@@ -67,29 +76,34 @@ public class PocketStoriesLoader extends
     private static final String PARAM_LOCALE = "locale_lang";
 
     private static final long REFRESH_INTERVAL_MILLIS = TimeUnit.HOURS.toMillis(1);
 
     private static final int BUFFER_SIZE = 2048;
     private static final int CONNECT_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(15);
     private static final int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(15);
 
+    private static boolean isTesting = false;
+
     private String localeLang;
     private final SharedPreferences sharedPreferences;
 
+    private static String placeholderUrl;
+
     public PocketStoriesLoader(Context context) {
         super(context);
 
         sharedPreferences = context.getSharedPreferences(POCKET_PREFS_FILE, Context.MODE_PRIVATE);
         localeLang = Locales.getLanguageTag(Locale.getDefault());
     }
 
     @Override
     protected void onStartLoading() {
-        if (APIKEY == null) {
+        // We don't want to hit the network if we're testing.
+        if (APIKEY == null || isTesting) {
             deliverResult(makePlaceholderStories());
             return;
         }
         // Check timestamp to determine if we have cached stories. This won't properly handle a client manually
         // changing clock times, but this is not a time-sensitive task.
         final long previousTime = sharedPreferences.getLong(CACHE_TIMESTAMP_MILLIS_PREFIX + localeLang, 0);
         if (System.currentTimeMillis() - previousTime > REFRESH_INTERVAL_MILLIS) {
             forceLoad();
@@ -182,16 +196,30 @@ public class PocketStoriesLoader extends
         } catch (JSONException e) {
             Log.e(LOGTAG, "Couldn't load Pocket response", e);
         }
         return topStories;
     }
 
     private static List<TopStory> makePlaceholderStories() {
         final List<TopStory> stories = new LinkedList<>();
-        final String TITLE_PREFIX = "Placeholder ";
         for (int i = 0; i < DEFAULT_COUNT; i++) {
             // Urls must be different for bookmark/pinning UI to work properly. Assume this is true for Pocket stories.
-            stories.add(new TopStory(TITLE_PREFIX + i, "https://www.mozilla.org/#" + i, null));
+            stories.add(new TopStory(PLACEHOLDER_TITLE + i, placeholderUrl + i, null));
         }
         return stories;
     }
+
+    private static void setPlaceholderUrl(String placeholderUrl) {
+        // See use of placeholderUrl for why suffix is necessary.
+        final String requiredSuffix = "#";
+        if (!placeholderUrl.endsWith(requiredSuffix)) {
+            placeholderUrl = placeholderUrl + requiredSuffix;
+        }
+        PocketStoriesLoader.placeholderUrl = placeholderUrl;
+    }
+
+    @RobocopTarget
+    @VisibleForTesting public static void configureForTesting(final String placeholderUrl) {
+        isTesting = true;
+        setPlaceholderUrl(placeholderUrl);
+    }
 }
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -103,16 +103,17 @@ skip-if = android_version == "18"
 # disabled on 4.3, bug 1098532
 skip-if = android_version == "18"
 [src/org/mozilla/gecko/tests/testAndroidCastDeviceProvider.java]
 
 # Using UITest
 [src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java]
 disabled=see bug 947550, bug 979038 and bug 977952
 [src/org/mozilla/gecko/tests/testAboutHomeVisibility.java]
+[src/org/mozilla/gecko/tests/testActivityStreamPocketReferrer.java]
 [src/org/mozilla/gecko/tests/testAppMenuPathways.java]
 [src/org/mozilla/gecko/tests/testBackButtonInEditMode.java]
 [src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java]
 [src/org/mozilla/gecko/tests/testEventDispatcher.java]
 [src/org/mozilla/gecko/tests/testInputConnection.java]
 [src/org/mozilla/gecko/tests/testJavascriptBridge.java]
 [src/org/mozilla/gecko/tests/testReaderCacheMigration.java]
 [src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java]
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamPocketReferrer.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 android.util.Log;
+import com.robotium.solo.Condition;
+import org.mozilla.gecko.activitystream.homepanel.topstories.PocketStoriesLoader;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.util.StringUtils;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+/**
+ * It's very important that suggestions from Pocket are opened with a Pocket URI in the referrer: this is
+ * what this test is for.
+ *
+ * We want to verify:
+ * - Clicking or long clicking a Pocket top story has a Pocket referrer.
+ * - Clicking or long clicking other items on top sites does not have a Pocket referrer.
+ *
+ * The ideal test is to set up a server that will assert that clicks that should have a Pocket referrer
+ * have one in the request headers and clicks that should not have a referrer do not have one. However,
+ * it's non-trivial to set up such a server in our harness so we instead intercept Tab:Load JS events
+ * and verify they have the referrer. This isn't ideal because we might drop the ball passing the referrer
+ * to Gecko, or Gecko might drop the ball, but this test should help regressions if we manually test the
+ * referrers are working.
+ */
+public class testActivityStreamPocketReferrer extends JavascriptBridgeTest {
+
+    private static final String LOGTAG =
+            StringUtils.safeSubstring(testActivityStreamPocketReferrer.class.getSimpleName(), 0, 23);
+
+    private static final String JS_FILE = "testActivityStreamPocketReferrer.js";
+
+    private boolean wasTabLoadReceived = false;
+    private boolean tabLoadContainsPocketReferrer = false;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // Override the default placeholder URL so we don't access the network during testing.
+        // Note: this actually only seems to take effect after we load a page and go back to about:home.
+        PocketStoriesLoader.configureForTesting(getAbsoluteHostnameUrl(StringHelper.get().ROBOCOP_BLANK_PAGE_01_URL));
+    }
+
+    public void testActivityStreamPocketReferrer() throws Exception {
+        blockForReadyAndLoadJS(JS_FILE);
+        NavigationHelper.goBack(); // to top sites.
+
+        checkReferrerInTopStories();
+        checkReferrerInTopStoriesContextMenu();
+
+        checkNoReferrerInTopSites(); // relies on changes to Top Sites from previous tests.
+
+        // Ideally, we'd also test that there is no referrer for highlights but it's more
+        // challenging to get an item to show up in highlights (bookmark the page) and to scroll
+        // to open it: to save time, I chose not to implement it.
+    }
+
+    private void checkReferrerInTopStories() {
+        Log.d(LOGTAG, "testReferrerInTopStories");
+
+        WaitHelper.waitForPageLoad(new Runnable() {
+            @Override
+            public void run() {
+                mSolo.clickOnText(PocketStoriesLoader.PLACEHOLDER_TITLE); // Click Top Story placeholder item.
+            }
+        });
+
+        assertTabLoadEventContainsPocketReferrer(true);
+        NavigationHelper.goBack(); // to top sites.
+    }
+
+    private void checkReferrerInTopStoriesContextMenu() throws Exception {
+        Log.d(LOGTAG, "testReferrerInTopStoriesContextMenu");
+
+        mSolo.clickLongOnText(PocketStoriesLoader.PLACEHOLDER_TITLE); // Open Top Story context menu.
+        mSolo.clickOnText(StringHelper.get().CONTEXT_MENU_OPEN_IN_NEW_TAB);
+        WaitHelper.waitFor("context menu to close after item selection.", new Condition() {
+            @Override
+            public boolean isSatisfied() {
+                return !mSolo.searchText(StringHelper.get().CONTEXT_MENU_OPEN_IN_NEW_TAB);
+            }
+        }, 5000);
+
+        // There's no simple way to block until a background page loads so instead, we sleep for 500ms.
+        // Our JS listener is attached the whole time so if the message is sent, we'll receive it and cache it
+        // while we're sleeping.
+        Thread.sleep(500);
+        assertTabLoadEventContainsPocketReferrer(true);
+    }
+
+    private void checkNoReferrerInTopSites() {
+        Log.d(LOGTAG, "testNoReferrerInTopSites");
+
+        WaitHelper.waitForPageLoad(new Runnable() {
+            @Override
+            public void run() {
+                // Through the previous tests, we've added a top site called "Browser Blank Page...".
+                // Only part of that label will be visible, however.
+                mSolo.clickOnText("Browser Bl"); // Click on a Top Site.
+            }
+        });
+
+        assertTabLoadEventContainsPocketReferrer(false);
+        NavigationHelper.goBack(); // to top sites.
+    }
+
+    private void assertTabLoadEventContainsPocketReferrer(final boolean expectedContainsReferrer) {
+        // We intercept the Tab:Load event in JS and, due to limitations in JavascriptBridge,
+        // store the data there until Java asks for it.
+        getJS().syncCall("copyTabLoadEventMetadataToJava"); // expected to call copyTabLoad...Receiver
+
+        fAssertTrue("Expected Tab:Load to be called", wasTabLoadReceived);
+        fAssertEquals("Checking for expected existence of pocket referrer from Tab:Load event in JS",
+                expectedContainsReferrer, tabLoadContainsPocketReferrer);
+    }
+
+    // JS methods.
+    public void copyTabLoadEventMetadataToJavaReceiver(final boolean wasTabLoadReceived, final boolean tabLoadContainsPocketReferrer) {
+        Log.d(LOGTAG, "setTabLoadContainsPocketReferrer called via JS: " + wasTabLoadReceived + ", " + tabLoadContainsPocketReferrer);
+        this.wasTabLoadReceived = wasTabLoadReceived;
+        this.tabLoadContainsPocketReferrer = tabLoadContainsPocketReferrer;
+    }
+
+    public void log(final String s) {
+        Log.d(LOGTAG, "jsLog: " + s);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testActivityStreamPocketReferrer.js
@@ -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/. */
+
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+let java = new JavaBridge(this);
+do_register_cleanup(() => {
+    EventDispatcher.instance.unregisterListener(listener);
+
+    java.disconnect();
+});
+do_test_pending();
+
+var wasTabLoadReceived = false;
+var tabLoadContainsPocketReferrer = false;
+
+let listener = {
+    onEvent: function(event, data, callback) {
+        java.asyncCall("log", "Tab:Load url: " + data.url);
+        java.asyncCall("log", "Tab:Load referrerURI: " + data.referrerURI);
+        if (event !== "Tab:Load" ||
+                data.url === "about:home") {
+            return;
+        }
+
+        wasTabLoadReceived = true;
+        if (data.referrerURI && data.referrerURI.search("pocket") > 0) {
+            tabLoadContainsPocketReferrer = true;
+        } else {
+            tabLoadContainsPocketReferrer = false;
+        }
+    }
+};
+
+let win = Services.wm.getMostRecentWindow("navigator:browser");
+EventDispatcher.for(win).registerListener(listener, ["Tab:Load"]);
+
+// Java functions.
+function copyTabLoadEventMetadataToJava() {
+    java.syncCall("copyTabLoadEventMetadataToJavaReceiver", wasTabLoadReceived, tabLoadContainsPocketReferrer);
+    wasTabLoadReceived = false;
+}