Bug 1395409: Add testActivityStreamPocketReferrer. r=liuche draft
authorMichael Comella <michael.l.comella@gmail.com>
Wed, 30 Aug 2017 17:33:08 -0700
changeset 675135 0d744d9f3d4ffa55a594da566f2eacea0f1a3a17
parent 673759 15f221f491f707b1e8e46da344b6dd5a394b1242
child 734525 2cfc90642a28b64a5eba061b366c31835ff5d262
push id83048
push usermichael.l.comella@gmail.com
push dateWed, 04 Oct 2017 21:29:36 +0000
reviewersliuche
bugs1395409
milestone58.0a1
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;
+}