Bug 1395409: Add testActivityStreamPocketReferrer. r=liuche
MozReview-Commit-ID: FlcMG5IewRH
--- 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;
+}