blob: 5e5724529f81ada823b7f57a9268470c4293e44d [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.offlinepages;
import android.content.Intent;
import android.support.test.filters.MediumTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.DeviceConditions;
import org.chromium.chrome.browser.offlinepages.AutoFetchNotifier.NotificationAction;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.util.UrlConstants;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.net.test.util.WebServer;
import org.chromium.net.test.util.WebServer.HTTPRequest;
import org.chromium.ui.base.PageTransition;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/** Unit tests for auto-fetch-on-net-error-page. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.
Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "enable-features=AutoFetchOnNetErrorPage"})
public class OfflinePageAutoFetchTest {
private static final String TAG = "AutoFetchTest";
private static final long WAIT_TIMEOUT_MS = 20000;
@Rule
public ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeActivity.class);
@Rule
public TestWatcher mTestWatcher = new TestWatcher() {
@Override
protected void failed(Throwable e, Description description) {
logAdditionalContext();
}
};
private Profile mProfile;
private OfflinePageBridge mOfflinePageBridge;
private CallbackHelper mPageAddedHelper = new CallbackHelper();
private OfflinePageItem mAddedPage;
private WebServer mWebServer;
private Map<String, Integer> mInitialHistograms;
private Intent mLastInProgressCancelButtonIntent;
private Intent mLastInProgressDeleteIntent;
private Intent mLastCompleteClickIntent;
private Intent mLastCompleteDeleteIntent;
private class NotifierHooks implements AutoFetchNotifier.TestHooks {
@Override
public void inProgressNotificationShown(Intent cancelButtonIntent, Intent deleteIntent) {
mLastInProgressCancelButtonIntent = cancelButtonIntent;
mLastInProgressDeleteIntent = deleteIntent;
}
@Override
public void completeNotificationShown(Intent clickIntent, Intent deleteIntent) {
mLastCompleteClickIntent = clickIntent;
mLastCompleteDeleteIntent = deleteIntent;
}
}
private static final String DEFAULT_BODY = "<html><title>MyTestPage</title>Hello World!</html>";
private void startWebServer() throws Exception {
Assert.assertTrue(mWebServer == null);
mWebServer = new WebServer(0, false);
useDefaultWebServerResponse();
}
private void useDefaultWebServerResponse() {
Assert.assertTrue(mWebServer != null);
mWebServer.setRequestHandler((HTTPRequest request, OutputStream stream) -> {
try {
WebServer.writeResponse(stream, WebServer.STATUS_OK, DEFAULT_BODY.getBytes());
} catch (IOException e) {
}
});
}
private void useAlternateWebServerResponse() {
Assert.assertTrue(mWebServer != null);
String body = "<html><title>A Different Page</title>Alternate page!</html>";
mWebServer.setRequestHandler((HTTPRequest request, OutputStream stream) -> {
try {
WebServer.writeResponse(stream, WebServer.STATUS_OK, body.getBytes());
} catch (IOException e) {
}
});
}
private void useRedirectWebServerResponse() {
Assert.assertTrue(mWebServer != null);
String redirectBody =
"<html><meta http-equiv=\"refresh\" content=\"0; url=/redirect_target\">"
+ "<title>RedirectingFromHere</title>redirect</html>";
mWebServer.setRequestHandler((HTTPRequest request, OutputStream stream) -> {
try {
String body =
request.getURI().endsWith("redirect_from") ? redirectBody : DEFAULT_BODY;
WebServer.writeResponse(stream, WebServer.STATUS_OK, body.getBytes());
} catch (IOException e) {
}
});
}
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
AutoFetchNotifier.mTestHooks = new NotifierHooks();
TestThreadUtils.runOnUiThreadBlocking(() -> {
mInitialHistograms = histogramSnapshot();
mProfile = activityTab().getProfile();
mOfflinePageBridge = OfflinePageBridge.getForProfile(mProfile);
if (!NetworkChangeNotifier.isInitialized()) {
NetworkChangeNotifier.init();
}
OfflinePageBridge.getForProfile(mProfile).addObserver(
new OfflinePageBridge.OfflinePageModelObserver() {
@Override
public void offlinePageAdded(OfflinePageItem addedPage) {
mAddedPage = addedPage;
mPageAddedHelper.notifyCalled();
}
});
});
forceConnectivityState(false);
}
@After
public void tearDown() {
OfflineTestUtil.clearIntercepts();
if (mWebServer != null) {
mWebServer.shutdown();
}
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchTriggersOnDNSErrorWhenOffline() {
attemptLoadPage("http://does.not.resolve.com");
waitForRequestCount(1);
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchDoesNotTriggerOnDNSErrorWhenOnline() {
forceConnectivityState(true);
attemptLoadPage("http://does.not.resolve.com");
waitForRequestCount(0);
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchOnDinoPage() throws Exception {
startWebServer();
final String testUrl = mWebServer.getBaseUrl();
// Make |testUrl| return an offline error and attempt to load the page.
// This should trigger an auto-fetch request.
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
// Navigate away from the page, so that the auto-fetch request is allowed to complete,
// and go back online.
attemptLoadPage(UrlConstants.ABOUT_URL);
// The tab no longer has the requested URL active, so the in-progress notification should
// appear.
waitForInProgressNotification();
OfflineTestUtil.clearIntercepts();
forceConnectivityState(true);
OfflineTestUtil.startRequestCoordinatorProcessing();
// Wait for the background request to complete.
waitForPageAdded();
Assert.assertTrue(mAddedPage != null);
waitForHistogram("OfflinePages.AutoFetch.CompleteNotificationAction:SHOWN", 1);
// Simulate click on the complete notification, and ensure the offline page loads by
// swapping out the live page contents.
useAlternateWebServerResponse();
sendBroadcast(mLastCompleteClickIntent);
waitForHistogram("OfflinePages.AutoFetch.CompleteNotificationAction:TAPPED", 1);
// A new tab should open, and it should load the offline page.
pollInstrumentationThread(() -> {
return getCurrentTabModel().getCount() == 2
&& getCurrentTab().getTitle().equals("MyTestPage");
});
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchWithRedirect() throws Exception {
startWebServer();
useRedirectWebServerResponse();
final String testUrl = mWebServer.getBaseUrl() + "/redirect_from";
// Make |testUrl| return an offline error and attempt to load the page.
// This should trigger an auto-fetch request.
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
// Navigate away from the page, so that the auto-fetch request is allowed to complete,
// and go back online.
attemptLoadPage(UrlConstants.ABOUT_URL);
// The tab no longer has the requested URL active, so the in-progress notification should
// appear.
waitForInProgressNotification();
OfflineTestUtil.clearIntercepts();
forceConnectivityState(true);
OfflineTestUtil.startRequestCoordinatorProcessing();
// Wait for the background request to complete.
waitForPageAdded();
Assert.assertTrue(mAddedPage != null);
waitForHistogram("OfflinePages.AutoFetch.CompleteNotificationAction:SHOWN", 1);
// Navigate back to testUrl, this time there is no redirect.
useDefaultWebServerResponse();
attemptLoadPage(testUrl);
// Simulate click on the complete notification, and ensure the offline page loads by
// swapping out the live page contents.
useAlternateWebServerResponse();
sendBroadcast(mLastCompleteClickIntent);
waitForHistogram("OfflinePages.AutoFetch.CompleteNotificationAction:TAPPED", 1);
pollInstrumentationThread(() -> {
// No new tab is opened, because the URL of the tab matches the original URL.
return getCurrentTabModel().getCount() == 1
// The title matches the original page, not the 'AlternativeWebServerResponse'.
&& getCurrentTab().getTitle().equals("MyTestPage");
});
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testSwipeAwayCompleteNotification() throws Exception {
// Standard setup to trigger auto-fetch.
startWebServer();
final String testUrl = mWebServer.getBaseUrl();
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
attemptLoadPage(UrlConstants.ABOUT_URL);
waitForInProgressNotification();
OfflineTestUtil.clearIntercepts();
forceConnectivityState(true);
OfflineTestUtil.startRequestCoordinatorProcessing();
// Wait for the background request to complete.
waitForPageAdded();
waitForHistogram("OfflinePages.AutoFetch.CompleteNotificationAction:SHOWN", 1);
// Simulate swiping away the complete notification and wait for UMA change.
sendBroadcast(mLastCompleteDeleteIntent);
waitForHistogram("OfflinePages.AutoFetch.CompleteNotificationAction:DISMISSED", 1);
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchCancelOnLoad() throws Exception {
startWebServer();
final String testUrl = mWebServer.getBaseUrl();
// Make |testUrl| return an offline error and attempt to load the page.
// This should trigger an auto-fetch request.
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
// Allow loading the page and try again.
OfflineTestUtil.clearIntercepts();
OfflineTestUtil.startRequestCoordinatorProcessing();
mActivityTestRule.loadUrl(testUrl);
// |testUrl| should successfully load and the auto-fetch request should be removed.
waitForRequestCount(0);
Assert.assertEquals(0, mPageAddedHelper.getCallCount());
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
@DisabledTest(message = "Flaky: https://crbug.com/883486#c20")
public void testAutoFetchRequestRetainedOnOtherTabClosed() throws Exception {
startWebServer();
final String testUrl = mWebServer.getBaseUrl();
// Make |testUrl| return an offline error and attempt to load the page.
// This should trigger an auto-fetch request.
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
// Attempt to load the same URL in a new tab, and then close the tab.
// This should not create a new request.
Tab newTab = attemptLoadPageInNewTab(testUrl);
closeTab(newTab);
Assert.assertEquals(1, OfflineTestUtil.getRequestsInQueue().length);
// The original request should remain. Allow the request to complete.
closeTab(activityTab());
waitForInProgressNotification();
OfflineTestUtil.clearIntercepts();
forceConnectivityState(true);
OfflineTestUtil.startRequestCoordinatorProcessing();
waitForPageAdded();
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchNotifyOnTabClose() throws Exception {
final String testUrl = "http://www.offline.com";
// Make |testUrl| return an offline error and attempt to load the page.
// This should trigger an auto-fetch request.
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
closeTab(activityTab());
waitForInProgressNotification();
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchSwipeInProgressNotification() throws Exception {
// Trigger an auto-fetch request, and then an in-progress notification.
final String testUrl = "http://www.offline.com";
OfflineTestUtil.interceptWithOfflineError(testUrl);
attemptLoadPage(testUrl);
waitForRequestCount(1);
closeTab(activityTab());
waitForInProgressNotification();
// Simulate swiping the notification by sending the delete intent. This should trigger
// deletion of the request.
sendBroadcast(mLastInProgressDeleteIntent);
waitForRequestCount(0);
waitForHistogram("OfflinePages.AutoFetch.InProgressNotificationAction:DISMISSED", 1);
}
@Test
@MediumTest
@Feature({"OfflineAutoFetch"})
public void testAutoFetchTwoRequestsCancel() throws Exception {
// Trigger two auto-fetch requests.
final String testUrl1 = "http://www.offline1.com";
OfflineTestUtil.interceptWithOfflineError(testUrl1);
attemptLoadPage(testUrl1);
waitForRequestCount(1);
OfflineTestUtil.clearIntercepts(); // Only one intercept works at a time.
final String testUrl2 = "http://www.offline2.com";
OfflineTestUtil.interceptWithOfflineError(testUrl2);
attemptLoadPage(testUrl2);
waitForRequestCount(2);
// Trigger the in-progress notification, and then simulate tapping 'cancel'. Note that the
// in-progress notification is triggered for both requests, but only fires a single
// notification.
closeTab(activityTab());
waitForInProgressNotification();
sendBroadcast(mLastInProgressCancelButtonIntent);
waitForRequestCount(0);
waitForHistogram("OfflinePages.AutoFetch.InProgressNotificationAction:CANCEL_PRESSED", 1);
// Ensure the cancellation preference is cleared.
Assert.assertEquals(false, AutoFetchNotifier.autoFetchInProgressNotificationCanceled());
}
private void waitForRequestCount(int requestCount) {
pollInstrumentationThread(
() -> OfflineTestUtil.getRequestsInQueue().length == requestCount);
}
// Wait until at least one auto-fetch request has shown an in-progress notification.
private void waitForInProgressNotification() {
waitForHistogram("OfflinePages.AutoFetch.InProgressNotificationAction:SHOWN", 1);
}
private void waitForPageAdded() throws Exception {
mPageAddedHelper.waitForCallback(0, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
private Tab activityTab() {
return mActivityTestRule.getActivity().getActivityTab();
}
// Attempt to load a page on the active tab. Does not assert that the page is loaded
// successfully.
private void attemptLoadPage(String url) {
Tab tab = activityTab();
TestThreadUtils.runOnUiThreadBlocking(() -> {
tab.loadUrl(
new LoadUrlParams(url, PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR));
});
}
// Attempts to create a new tab and load |url| in it.
private Tab attemptLoadPageInNewTab(String url) throws Exception {
ChromeActivity activity = mActivityTestRule.getActivity();
Tab tab = TestThreadUtils.runOnUiThreadBlocking(
() -> activity.getTabCreator(false).launchUrl(url, TabLaunchType.FROM_LINK));
ChromeTabUtils.waitForInteractable(tab);
return tab;
}
private boolean isErrorPage(final Tab tab) {
final AtomicReference<Boolean> result = new AtomicReference<Boolean>(false);
TestThreadUtils.runOnUiThreadBlocking(() -> result.set(tab.isShowingErrorPage()));
return result.get();
}
private void closeTab(Tab tab) {
final TabModel model =
mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
// Attempt to close the tab, which will delay closing until the undo timeout goes away.
TestThreadUtils.runOnUiThreadBlocking(
() -> { TabModelUtils.closeTabById(model, tab.getId(), true); });
}
private void forceConnectivityState(boolean connected) {
TestThreadUtils.runOnUiThreadBlocking(() -> {
NetworkChangeNotifier.forceConnectivityState(connected);
DeviceConditions.sForceNoConnectionForTesting = !connected;
});
OfflineTestUtil.waitForConnectivityState(connected);
}
private void waitForHistogram(String histogramAndEnum, int delta) {
pollInstrumentationThread(()
-> histogramSnapshot().get(histogramAndEnum)
- mInitialHistograms.get(histogramAndEnum)
>= delta);
}
private static String histogramDiff(
Map<String, Integer> oldValues, Map<String, Integer> newValues) {
StringBuilder result = new StringBuilder();
String[] keys = newValues.keySet().toArray(new String[] {});
Arrays.sort(keys);
for (String key : keys) {
int diff = newValues.get(key) - oldValues.get(key);
if (diff > 0) {
if (result.length() > 0) result.append("\n");
result.append(key);
result.append(" ");
result.append(diff);
}
}
return result.toString();
}
private static Map<String, Integer> histogramSnapshot() {
final Map<String, Integer> histograms = new HashMap<String, Integer>();
TestThreadUtils.runOnUiThreadBlocking(() -> {
Integer actions[] = new Integer[] {
NotificationAction.SHOWN,
NotificationAction.COMPLETE,
NotificationAction.CANCEL_PRESSED,
NotificationAction.DISMISSED,
NotificationAction.TAPPED,
};
String[] actionNames = new String[] {
"SHOWN",
"COMPLETE",
"CANCEL_PRESSED",
"DISMISSED",
"TAPPED",
};
for (String histogramName : new String[] {
"OfflinePages.AutoFetch.InProgressNotificationAction",
"OfflinePages.AutoFetch.CompleteNotificationAction",
}) {
for (@NotificationAction int i = 0; i < NotificationAction.NUM_ENTRIES; i++) {
int value = RecordHistogram.getHistogramValueCountForTesting(histogramName, i);
histograms.put(histogramName + ":" + actionNames[i], value);
}
}
});
return histograms;
}
private void sendBroadcast(Intent intent) {
TestThreadUtils.runOnUiThreadBlocking(
() -> { ContextUtils.getApplicationContext().sendBroadcast(intent); });
}
private TabModel getCurrentTabModel() {
return mActivityTestRule.getActivity().getCurrentTabModel();
}
private Tab getCurrentTab() {
return TabModelUtils.getCurrentTab(getCurrentTabModel());
}
private void logAdditionalContext() {
Log.d(TAG, "Logging additional context");
Log.d(TAG, "Histogram Diff: " + histogramDiff(mInitialHistograms, histogramSnapshot()));
TabModel tabModel = getCurrentTabModel();
int tabCount = tabModel.getCount();
Log.d(TAG, "Tab Count: " + tabCount);
for (int i = 0; i < tabCount; ++i) {
String title = tabModel.getTabAt(i).getTitle();
String current = tabModel.index() == i ? "*current" : "";
Log.d(TAG, "Tab " + String.valueOf(i) + " '" + title + "' " + current);
}
try {
Log.d(TAG,
"Request Coordinator state:" + OfflineTestUtil.dumpRequestCoordinatorState());
} catch (TimeoutException e) {
}
}
private void pollInstrumentationThread(final Callable<Boolean> criteria) {
CriteriaHelper.pollInstrumentationThread(
criteria, "Criteria not met", WAIT_TIMEOUT_MS, 100);
}
}