chromium/chrome/android/javatests/src/org/chromium/chrome/browser/dom_distiller/ReaderModeTest.java

// Copyright 2020 The Chromium Authors
// 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.dom_distiller;

import static com.google.common.truth.Truth.assertThat;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.dom_distiller.ReaderModeManager.DOM_DISTILLER_SCHEME;

import android.app.Activity;
import android.app.PendingIntent;

import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ApplicationStatus;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.transit.Condition;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.download.DownloadTestRule;
import org.chromium.chrome.browser.download.DownloadTestRule.CustomMainActivityStart;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.incognito.IncognitoNotificationServiceImpl;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.transit.dom_distiller.ReaderModeConditions.TabBackgroundColorCondition;
import org.chromium.chrome.test.transit.dom_distiller.ReaderModeConditions.TabFontSizeCondition;
import org.chromium.chrome.test.transit.dom_distiller.ReaderModePreferencesDialog;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.MenuUtils;
import org.chromium.components.dom_distiller.core.DistilledPagePrefs;
import org.chromium.components.dom_distiller.core.DomDistillerService;
import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageStateHandler;
import org.chromium.components.messages.MessagesTestHelper;
import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.test.util.UiRestriction;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/** End-to-end tests for Reader Mode (Simplified view). */
@RunWith(ChromeJUnit4ClassRunner.class)
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DisableFeatures(ChromeFeatureList.BROWSER_CONTROLS_IN_VIZ)
public class ReaderModeTest implements CustomMainActivityStart {
    @Rule public DownloadTestRule mDownloadTestRule = new DownloadTestRule(this);

    private static final String TEST_PAGE = "/chrome/test/data/dom_distiller/simple_article.html";
    // Suffix added to page titles, string is defined as IDS_DOM_DISTILLER_VIEWER_TITLE_SUFFIX in
    // dom_distiller_strings.grdp.
    private static final String TITLE_SUFFIX = " - Simplified View";
    private static final String PAGE_TITLE = "Test Page Title" + TITLE_SUFFIX;
    private static final String CONTENT = "Lorem ipsum";

    @SuppressWarnings("FieldCanBeLocal")
    private EmbeddedTestServer mTestServer;

    private String mURL;

    @Mock DistilledPagePrefs.Observer mTestObserver;

    @Override
    public void customMainActivityStart() {
        MockitoAnnotations.initMocks(this);
        mTestServer =
                EmbeddedTestServer.createAndStartServer(
                        ApplicationProvider.getApplicationContext());
        mURL = mTestServer.getURL(TEST_PAGE);
        mDownloadTestRule.startMainActivityWithURL(mURL);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "crbug.com/1402815")
    public void testReaderModePromptShown() {
        waitForReaderModeMessage();
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    public void testReaderModeInCCT() throws TimeoutException {
        Tab originalTab = mDownloadTestRule.getActivity().getActivityTab();
        String innerHtml = getInnerHtml(originalTab);
        assertThat(innerHtml).doesNotContain("article-header");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    originalTab
                            .getUserDataHost()
                            .getUserData(ReaderModeManager.USER_DATA_KEY)
                            .activateReaderMode();
                });
        CustomTabActivity customTabActivity = waitForCustomTabActivity();
        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(customTabActivity.getActivityTab(), notNullValue()));
        @NonNull
        Tab distillerViewerTab = Objects.requireNonNull(customTabActivity.getActivityTab());
        waitForDistillation(PAGE_TITLE, distillerViewerTab);
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    public void testReaderModeInCCT_Downloaded() throws TimeoutException {
        Tab originalTab = mDownloadTestRule.getActivity().getActivityTab();
        String innerHtml = getInnerHtml(originalTab);
        assertThat(innerHtml).doesNotContain("article-header");

        downloadAndOpenOfflinePage();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    originalTab
                            .getUserDataHost()
                            .getUserData(ReaderModeManager.USER_DATA_KEY)
                            .activateReaderMode();
                });
        CustomTabActivity customTabActivity = waitForCustomTabActivity();
        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(customTabActivity.getActivityTab(), notNullValue()));
        @NonNull
        Tab distillerViewerTab = Objects.requireNonNull(customTabActivity.getActivityTab());
        waitForDistillation(PAGE_TITLE, distillerViewerTab);
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    public void testReaderModeInCCT_Incognito() throws TimeoutException {
        openReaderModeInIncognitoCCT();
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    @DisabledTest(message = "https://crbug.com/1338273")
    public void testCloseAllIncognitoNotification_ClosesCCT()
            throws PendingIntent.CanceledException, TimeoutException {
        CustomTabActivity customTabActivity = openReaderModeInIncognitoCCT();

        // Click on "Close all Incognito tabs" notification.
        PendingIntent clearIntent =
                IncognitoNotificationServiceImpl.getRemoveAllIncognitoTabsIntent(
                                ApplicationProvider.getApplicationContext())
                        .getPendingIntent();
        clearIntent.send();

        // Verify the Incognito CCT is closed.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            customTabActivity.getTabModelSelector().getModel(true).getCount(),
                            Matchers.equalTo(0));
                });
    }

    private CustomTabActivity openReaderModeInIncognitoCCT() throws TimeoutException {
        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(),
                (ChromeTabbedActivity) mDownloadTestRule.getActivity(),
                mURL,
                true);

        Tab originalTab = mDownloadTestRule.getActivity().getActivityTab();
        assertTrue(originalTab.isIncognito());
        String innerHtml = getInnerHtml(originalTab);
        assertThat(innerHtml).doesNotContain("article-header");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    originalTab
                            .getUserDataHost()
                            .getUserData(ReaderModeManager.USER_DATA_KEY)
                            .activateReaderMode();
                });
        CustomTabActivity customTabActivity = waitForCustomTabActivity();
        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(customTabActivity.getActivityTab(), notNullValue()));
        @NonNull
        Tab distillerViewerTab = Objects.requireNonNull(customTabActivity.getActivityTab());
        waitForDistillation(PAGE_TITLE, distillerViewerTab);
        assertTrue(distillerViewerTab.isIncognito());

        return customTabActivity;
    }

    private void downloadAndOpenOfflinePage() {
        int callCount = mDownloadTestRule.getChromeDownloadCallCount();
        MenuUtils.invokeCustomMenuActionSync(
                InstrumentationRegistry.getInstrumentation(),
                mDownloadTestRule.getActivity(),
                R.id.offline_page_id);
        Assert.assertTrue(mDownloadTestRule.waitForChromeDownloadToFinish(callCount));

        // Stop the server and also disconnect the network.
        mTestServer.stopAndDestroyServer();
        mTestServer = null;
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    NetworkChangeNotifier.forceConnectivityState(false);
                });

        // Load the page that has an offline copy. The offline page should be shown.
        Tab tab = mDownloadTestRule.getActivity().getActivityTab();
        Assert.assertFalse(isOfflinePage(tab));
        mDownloadTestRule.loadUrl(ChromeTabUtils.getUrlOnUiThread(tab).getSpec());
        Assert.assertTrue(isOfflinePage(tab));
    }

    private static boolean isOfflinePage(final Tab tab) {
        AtomicBoolean isOffline = new AtomicBoolean();
        ThreadUtils.runOnUiThreadBlocking(() -> isOffline.set(OfflinePageUtils.isOfflinePage(tab)));
        return isOffline.get();
    }

    @Test
    @MediumTest
    @DisableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    public void testReaderModeInTab() throws TimeoutException {
        Tab tab = mDownloadTestRule.getActivity().getActivityTab();
        String innerHtml = getInnerHtml(tab);
        assertThat(innerHtml).doesNotContain("article-header");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.getUserDataHost()
                            .getUserData(ReaderModeManager.USER_DATA_KEY)
                            .activateReaderMode();
                });
        waitForDistillation(PAGE_TITLE, mDownloadTestRule.getActivity().getActivityTab());
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    public void testPreferenceInCCT() throws TimeoutException {
        Tab originalTab = mDownloadTestRule.getActivity().getActivityTab();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    originalTab
                            .getUserDataHost()
                            .getUserData(ReaderModeManager.USER_DATA_KEY)
                            .activateReaderMode();
                });
        CustomTabActivity customTabActivity = waitForCustomTabActivity();
        CriteriaHelper.pollUiThread(() -> customTabActivity.getActivityTab() != null);
        @NonNull
        Tab distillerViewerTab = Objects.requireNonNull(customTabActivity.getActivityTab());
        waitForDistillation(PAGE_TITLE, distillerViewerTab);

        doTestSettingPreferences(customTabActivity, distillerViewerTab);
    }

    @Test
    @MediumTest
    @DisableFeatures(ChromeFeatureList.READER_MODE_IN_CCT)
    public void testPreferenceInTab() throws TimeoutException {
        mDownloadTestRule.loadUrl(
                DomDistillerUrlUtils.getDistillerViewUrlFromUrl(
                        DOM_DISTILLER_SCHEME, mURL, PAGE_TITLE));

        Tab tab = mDownloadTestRule.getActivity().getActivityTab();
        waitForDistillation(PAGE_TITLE, tab);

        doTestSettingPreferences(mDownloadTestRule.getActivity(), tab);
    }

    /**
     * Wait until a {@link CustomTabActivity} shows up, and return it.
     *
     * @return a {@link CustomTabActivity}
     */
    private CustomTabActivity waitForCustomTabActivity() {
        AtomicReference<CustomTabActivity> activity = new AtomicReference<>();
        CriteriaHelper.pollUiThread(
                () -> {
                    for (Activity runningActivity : ApplicationStatus.getRunningActivities()) {
                        if (runningActivity instanceof CustomTabActivity) {
                            activity.set((CustomTabActivity) runningActivity);
                            return true;
                        }
                    }
                    return false;
                });
        return activity.get();
    }

    private DistilledPagePrefs getDistilledPagePrefs() {
        AtomicReference<DistilledPagePrefs> prefs = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    DomDistillerService domDistillerService =
                            DomDistillerServiceFactory.getForProfile(
                                    ProfileManager.getLastUsedRegularProfile());
                    prefs.set(domDistillerService.getDistilledPagePrefs());
                });
        return prefs.get();
    }

    private void doTestSettingPreferences(ChromeActivity activity, Tab tab) {
        DistilledPagePrefs prefs = getDistilledPagePrefs();
        prefs.addObserver(mTestObserver);

        Condition.runAndWaitFor(
                /* trigger= */ null,
                new TabBackgroundColorCondition(tab, "\"rgb(255, 255, 255)\""));

        ReaderModePreferencesDialog dialog = ReaderModePreferencesDialog.open(activity);

        // Test setting background color
        dialog.pickColorDark(new TabBackgroundColorCondition(tab, "\"rgb(32, 33, 36)\""));
        dialog.pickColorSepia(new TabBackgroundColorCondition(tab, "\"rgb(254, 247, 224)\""));
        dialog.pickColorLight(new TabBackgroundColorCondition(tab, "\"rgb(255, 255, 255)\""));
        verify(mTestObserver, times(3)).onChangeTheme(anyInt());

        // Test setting font size
        Condition.runAndWaitFor(/* trigger= */ null, new TabFontSizeCondition(tab, "\"14px\""));
        // Max is 200% font size.
        dialog.setFontSizeSliderToMax(new TabFontSizeCondition(tab, "\"28px\""));
        // Min is 50% font size.
        dialog.setFontSizeSliderToMin(new TabFontSizeCondition(tab, "\"7px\""));
        verify(mTestObserver, times(2)).onChangeFontScaling(anyFloat());

        // TODO(crbug.com/40125950): change font family as well.

        dialog.pressBackToClose();
    }

    /**
     * Run JavaScript on a certain {@link Tab}.
     *
     * @param tab The tab to be injected to.
     * @param javaScript The JavaScript code to be injected.
     * @return The result of the code.
     */
    private String runJavaScript(Tab tab, String javaScript) throws TimeoutException {
        TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper javascriptHelper =
                new TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper();
        javascriptHelper.evaluateJavaScriptForTests(tab.getWebContents(), javaScript);
        javascriptHelper.waitUntilHasValue();
        return javascriptHelper.getJsonResultAndClear();
    }

    /**
     * @param tab The tab to be inspected.
     * @return The inner HTML of a certain {@link Tab}.
     */
    private String getInnerHtml(Tab tab) throws TimeoutException {
        return runJavaScript(tab, "document.body.innerHTML");
    }

    /** Wait until a Reader Mode message shows up. */
    private void waitForReaderModeMessage() {
        CriteriaHelper.pollUiThread(
                () -> {
                    MessageDispatcher messageDispatcher =
                            ThreadUtils.runOnUiThreadBlocking(
                                    () ->
                                            MessageDispatcherProvider.from(
                                                    mDownloadTestRule
                                                            .getActivity()
                                                            .getWindowAndroid()));
                    List<MessageStateHandler> messages =
                            MessagesTestHelper.getEnqueuedMessages(
                                    messageDispatcher, MessageIdentifier.READER_MODE);
                    return messages.size() > 0;
                });
    }

    /**
     * Wait until the distilled content is shown on the {@link Tab}.
     *
     * @param expectedTitle The expected title of the distilled content
     * @param tab the tab to wait
     */
    private void waitForDistillation(
            @SuppressWarnings("SameParameterValue") String expectedTitle, Tab tab)
            throws TimeoutException {
        CriteriaHelper.pollUiThread(
                () ->
                        Criteria.checkThat(
                                ChromeTabUtils.getUrlOnUiThread(tab).getScheme(),
                                is("chrome-distiller")));
        ChromeTabUtils.waitForTabPageLoaded(tab, null);
        // Distiller Viewer load the content dynamically, so waitForTabPageLoaded() is not enough.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(tab.getTitle(), is(expectedTitle)));

        String innerHtml = getInnerHtml(tab);
        assertThat(innerHtml).contains("article-header");
        assertThat(innerHtml).contains(CONTENT);
    }
}