chromium/chrome/browser/paint_preview/android/javatests/src/org/chromium/chrome/browser/paint_preview/StartupPaintPreviewTest.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.paint_preview;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;

import static org.chromium.base.test.util.Batch.PER_CLASS;
import static org.chromium.chrome.browser.paint_preview.TabbedPaintPreviewTest.assertAttachedAndShown;

import android.view.View;
import android.view.ViewGroup;

import androidx.test.filters.MediumTest;

import org.hamcrest.Matchers;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.paint_preview.services.PaintPreviewTabService;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.components.paintpreview.player.PlayerManager;

import java.util.concurrent.ExecutionException;

/** Tests for the {@link TabbedPaintPreview} class. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(PER_CLASS)
public class StartupPaintPreviewTest {
    @ClassRule
    public static ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    // Tell R8 not to break the ability to mock the class.
    @Mock private static PaintPreviewTabService sUnused;

    @Rule
    public final BlankCTATabInitialStateRule mInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, true);

    private static final String TEST_URL = "/chrome/test/data/android/about.html";

    @BeforeClass
    public static void setUp() {
        PaintPreviewTabService mockService = Mockito.mock(PaintPreviewTabService.class);
        Mockito.doReturn(true).when(mockService).hasCaptureForTab(Mockito.anyInt());
        TabbedPaintPreview.overridePaintPreviewTabServiceForTesting(mockService);
        PlayerManager.overrideCompositorDelegateFactoryForTesting(
                new TabbedPaintPreviewTest.TestCompositorDelegateFactory());
    }

    @AfterClass
    public static void tearDown() {
        PlayerManager.overrideCompositorDelegateFactoryForTesting(null);
        TabbedPaintPreview.overridePaintPreviewTabServiceForTesting(null);
    }

    @Before
    public void setup() {
        sActivityTestRule.loadUrl(sActivityTestRule.getTestServer().getURL(TEST_URL));
    }

    /**
     * Tests that StartupPaintPreview is displayed correctly if a paint preview for the current tab
     * has been captured before.
     */
    @Test
    @MediumTest
    public void testDisplayedCorrectly() throws ExecutionException {
        Tab tab = sActivityTestRule.getActivity().getActivityTab();
        StartupPaintPreview startupPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new StartupPaintPreview(tab, null, null, null));
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        showAndWaitForInflation(startupPaintPreview, tabbedPaintPreview, null);
    }

    @Test
    @MediumTest
    public void testSnackbarShow() throws ExecutionException, InterruptedException {
        Tab tab = sActivityTestRule.getActivity().getActivityTab();
        StartupPaintPreview startupPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new StartupPaintPreview(tab, null, null, null));
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        showAndWaitForInflation(startupPaintPreview, tabbedPaintPreview, null);

        // Snackbar should appear on user frustration. It currently happens when users taps 3 times,
        // or when users longpress.
        SnackbarManager snackbarManager = sActivityTestRule.getActivity().getSnackbarManager();
        assertSnackbarVisibility(snackbarManager, false);
        View view = tabbedPaintPreview.getViewForTesting();

        // First tap.
        onView(Matchers.is(view)).perform(click());
        assertSnackbarVisibility(snackbarManager, false);
        // Second tap.
        onView(Matchers.is(view)).perform(click());
        assertSnackbarVisibility(snackbarManager, false);
        // Third tap.
        onView(Matchers.is(view)).perform(click());
        assertSnackbarVisibility(snackbarManager, true);

        ThreadUtils.runOnUiThreadBlocking(snackbarManager::dismissAllSnackbars);
        assertSnackbarVisibility(snackbarManager, false);

        // Simulate long press.
        onView(Matchers.is(view)).perform(longClick());
        assertSnackbarVisibility(snackbarManager, true);
    }

    /** Tests that the paint preview is removed when certain conditions are met. */
    @Test
    @MediumTest
    public void testRemoveOnFirstMeaningfulPaint() throws ExecutionException {
        Tab tab = sActivityTestRule.getActivity().getActivityTab();
        StartupPaintPreview startupPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new StartupPaintPreview(tab, null, null, null));
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        CallbackHelper dismissCallback = new CallbackHelper();

        // Should be removed on FMP signal.
        showAndWaitForInflation(startupPaintPreview, tabbedPaintPreview, dismissCallback);
        ThreadUtils.runOnUiThreadBlocking(
                () -> startupPaintPreview.onWebContentsFirstMeaningfulPaint(tab.getWebContents()));
        assertAttachedAndShown(tabbedPaintPreview, false, false);
        Assert.assertEquals(
                "Dismiss callback should have been called.", 1, dismissCallback.getCallCount());
    }

    /** Tests that the paint preview is removed when offline page is shown. */
    @Test
    @MediumTest
    public void testRemoveOnOfflinePage() throws ExecutionException {
        Tab tab = sActivityTestRule.getActivity().getActivityTab();
        StartupPaintPreview startupPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new StartupPaintPreview(tab, null, null, null));
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        // Offline page callback always returns true.
        startupPaintPreview.setIsOfflinePage(() -> true);
        CallbackHelper dismissCallback = new CallbackHelper();

        showAndWaitForInflation(startupPaintPreview, tabbedPaintPreview, dismissCallback);
        assertAttachedAndShown(tabbedPaintPreview, true, true);
        // Should be removed on PageLoadFinished signal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    startupPaintPreview.getTabObserverForTesting().onPageLoadFinished(tab, null);
                });
        assertAttachedAndShown(tabbedPaintPreview, false, false);
        Assert.assertEquals(
                "Dismiss callback should have been called.", 1, dismissCallback.getCallCount());
    }

    /** Tests that the paint preview is removed when certain conditions are met. */
    @Test
    @MediumTest
    public void testRemoveOnSnackbarClick() throws ExecutionException, InterruptedException {
        Tab tab = sActivityTestRule.getActivity().getActivityTab();
        StartupPaintPreview startupPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new StartupPaintPreview(tab, null, null, null));
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        CallbackHelper dismissCallback = new CallbackHelper();

        // Should be removed on SnackBar click.
        showAndWaitForInflation(startupPaintPreview, tabbedPaintPreview, dismissCallback);
        SnackbarManager snackbarManager = sActivityTestRule.getActivity().getSnackbarManager();
        View view = tabbedPaintPreview.getViewForTesting();
        onView(Matchers.is(view)).perform(longClick());
        assertSnackbarVisibility(snackbarManager, true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    snackbarManager.getCurrentSnackbarForTesting().getController().onAction(null);
                });
        assertAttachedAndShown(tabbedPaintPreview, false, false);
        Assert.assertEquals(
                "Dismiss callback should have been called.", 1, dismissCallback.getCallCount());
    }

    /** Tests that the paint preview is removed when certain conditions are met. */
    @Test
    @MediumTest
    public void testRemoveOnNavigation() throws ExecutionException {
        Tab tab = sActivityTestRule.getActivity().getActivityTab();
        StartupPaintPreview startupPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new StartupPaintPreview(tab, null, null, null));
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        CallbackHelper dismissCallback = new CallbackHelper();

        // Should be removed on navigation start.
        showAndWaitForInflation(startupPaintPreview, tabbedPaintPreview, dismissCallback);
        startupPaintPreview.getTabObserverForTesting().onRestoreStarted(tab);
        ThreadUtils.runOnUiThreadBlocking(tab::reload);
        assertAttachedAndShown(tabbedPaintPreview, false, false);
        Assert.assertEquals(
                "Dismiss callback should have been called.", 1, dismissCallback.getCallCount());
    }

    private void assertSnackbarVisibility(SnackbarManager snackbarManager, boolean visible) {
        String message =
                visible ? "Snackbar should be visible." : "Snackbar should not be visible.";
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(message, snackbarManager.isShowing(), Matchers.is(visible));
                });
    }

    private void showAndWaitForInflation(
            StartupPaintPreview startupPaintPreview,
            TabbedPaintPreview tabbedPaintPreview,
            CallbackHelper dismissCallback) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    startupPaintPreview.show(
                            dismissCallback == null ? null : dismissCallback::notifyCalled);
                });
        assertAttachedAndShown(tabbedPaintPreview, true, true);
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "TabbedPaintPreview has no view",
                            tabbedPaintPreview.getViewForTesting(),
                            Matchers.notNullValue());
                    Criteria.checkThat(
                            "TabbedPaintPreview has 0 children",
                            ((ViewGroup) tabbedPaintPreview.getViewForTesting()).getChildCount(),
                            Matchers.not(0));
                });
    }
}