chromium/chrome/android/javatests/src/org/chromium/chrome/browser/tab/SadTabTest.java

// Copyright 2015 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.tab;

import android.widget.Button;

import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.fullscreen.FullscreenManagerTestUtils;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.net.test.util.TestWebServer;

/** Tests related to the sad tab logic. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class SadTabTest {
    private static final String LONG_HTML_TEST_PAGE =
            UrlUtils.encodeHtmlDataUri("<html><body style='height:100000px;'></body></html>");

    @ClassRule
    public static ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, false);

    private static boolean isShowingSadTab(Tab tab) {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        return ThreadUtils.runOnUiThreadBlocking(() -> SadTab.isShowing(tab));
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Tab tab = sActivityTestRule.getActivity().getActivityTab();
                    tab.show(TabSelectionType.FROM_USER, TabLoadIfNeededCaller.OTHER);
                    SadTab sadTab = SadTab.from(tab);
                    sadTab.removeIfPresent();
                });
    }

    /** Verify that the sad tab is shown when the renderer crashes. */
    @Test
    @SmallTest
    @Feature({"SadTab"})
    public void testSadTabShownWhenRendererProcessKilled() {
        final Tab tab = sActivityTestRule.getActivity().getActivityTab();

        Assert.assertFalse(isShowingSadTab(tab));
        simulateRendererKilled(tab, true);
        Assert.assertTrue(isShowingSadTab(tab));
    }

    /**
     * Verify that the sad tab is not shown when the renderer crashes in the background or the
     * renderer was killed by the OS out-of-memory killer.
     */
    @Test
    @SmallTest
    @Feature({"SadTab"})
    public void testSadTabNotShownWhenRendererProcessKilledInBackround() {
        final Tab tab = sActivityTestRule.getActivity().getActivityTab();

        Assert.assertFalse(isShowingSadTab(tab));
        simulateRendererKilled(tab, false);
        Assert.assertFalse(isShowingSadTab(tab));
    }

    /** Verify that a tab navigating to a page that is killed in the background is reloaded. */
    @Test
    @SmallTest
    @Feature({"SadTab"})
    public void testSadTabReloadAfterKill() throws Throwable {
        final Tab tab = sActivityTestRule.getActivity().getActivityTab();

        TestWebServer webServer = TestWebServer.start();
        try {
            final String url1 = webServer.setEmptyResponse("/page1.html");
            sActivityTestRule.loadUrl(url1);
            Assert.assertFalse(tab.needsReload());
            simulateRendererKilled(tab, false);
            Assert.assertTrue(tab.needsReload());
        } finally {
            webServer.shutdown();
        }
    }

    /** Verify that a tab killed in the background is not reloaded if another load has started. */
    @Test
    @SmallTest
    @Feature({"SadTab"})
    public void testSadTabNoReloadAfterLoad() throws Throwable {
        final Tab tab = sActivityTestRule.getActivity().getActivityTab();

        TestWebServer webServer = TestWebServer.start();
        try {
            final String url1 = webServer.setEmptyResponse("/page1.html");
            final String url2 = webServer.setEmptyResponse("/page2.html");
            sActivityTestRule.loadUrl(url1);
            Assert.assertFalse(tab.needsReload());
            simulateRendererKilled(tab, false);
            sActivityTestRule.loadUrl(url2);
            Assert.assertFalse(tab.needsReload());
        } finally {
            webServer.shutdown();
        }
    }

    /**
     * Confirm that after a successive refresh of a failed tab that failed to load, change the
     * button from "Reload" to "Send Feedback". If reloaded a third time and it is successful it
     * reverts from "Send Feedback" to "Reload".
     *
     * @throws IllegalArgumentException
     */
    @Test
    @SmallTest
    @Feature({"SadTab"})
    public void testSadTabPageButtonText() throws IllegalArgumentException {
        final Tab tab = sActivityTestRule.getActivity().getActivityTab();

        Assert.assertFalse(isShowingSadTab(tab));
        simulateRendererKilled(tab, true);
        Assert.assertTrue(isShowingSadTab(tab));
        String actualText = getSadTabButton(tab).getText().toString();
        Assert.assertEquals(
                "Expected the sad tab button to have the reload label",
                sActivityTestRule.getActivity().getString(R.string.sad_tab_reload_label),
                actualText);

        reloadSadTab(tab);
        Assert.assertTrue(isShowingSadTab(tab));
        actualText = getSadTabButton(tab).getText().toString();
        Assert.assertTrue(showSendFeedbackView(tab));
        Assert.assertEquals(
                "Expected the sad tab button to have the feedback label after the tab button "
                        + "crashes twice in a row.",
                sActivityTestRule.getActivity().getString(R.string.sad_tab_send_feedback_label),
                actualText);
        sActivityTestRule.loadUrl("about:blank");
        Assert.assertFalse(
                "Expected about:blank to destroy the sad tab however the sad tab is still in "
                        + "view",
                isShowingSadTab(tab));
        simulateRendererKilled(tab, true);
        actualText = getSadTabButton(tab).getText().toString();
        Assert.assertEquals(
                "Expected the sad tab button to have the reload label after a successful load",
                sActivityTestRule.getActivity().getString(R.string.sad_tab_reload_label),
                actualText);
    }

    @Test
    @MediumTest
    @Feature({"SadTab"})
    @DisabledTest(message = "https://crbug.com/1447840")
    public void testSadTabBrowserControlsVisibility() {
        ThreadUtils.runOnUiThreadBlocking(
                TabStateBrowserControlsVisibilityDelegate::disablePageLoadDelayForTests);
        FullscreenManagerTestUtils.disableBrowserOverrides();
        sActivityTestRule.loadUrl(LONG_HTML_TEST_PAGE);
        FullscreenManagerTestUtils.waitForBrowserControlsToBeMoveable(
                sActivityTestRule, sActivityTestRule.getActivity().getActivityTab());
        FullscreenManagerTestUtils.scrollBrowserControls(sActivityTestRule, false);
        simulateRendererKilled(sActivityTestRule.getActivity().getActivityTab(), true);
        FullscreenManagerTestUtils.waitForBrowserControlsPosition(sActivityTestRule, 0);
    }

    /** Helper method that kills the renderer on a UI thread. */
    private static void simulateRendererKilled(final Tab tab, final boolean visible) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (!visible) tab.hide(TabHidingType.CHANGED_TABS);
                    ChromeTabUtils.simulateRendererKilledForTesting(tab);
                });
    }

    /** Helper method that reloads a tab with a SadTabView currently displayed. */
    private static void reloadSadTab(final Tab tab) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    SadTab sadTab = SadTab.from(tab);
                    sadTab.removeIfPresent();
                    sadTab.show(tab.getContext(), () -> {}, () -> {});
                });
    }

    private static boolean showSendFeedbackView(final Tab tab) {
        return ThreadUtils.runOnUiThreadBlocking(() -> SadTab.from(tab).showSendFeedbackView());
    }

    /**
     * If there is a SadTabView, this method will get the button for the sad tab.
     *
     * @param tab The tab that needs to contain a SadTabView.
     * @return Returns the button that is on the SadTabView, null if SadTabView. doesn't exist.
     */
    private static Button getSadTabButton(Tab tab) {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        return ThreadUtils.runOnUiThreadBlocking(
                () -> tab.getView().findViewById(R.id.sad_tab_button));
    }
}