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

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;

import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;

import android.app.Activity;
import android.view.View;

import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Spy;

import org.chromium.base.ThreadUtils;
import org.chromium.base.Token;
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.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.chrome.test.util.RecentTabsPageTestUtils;
import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestUtil;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.policy.test.annotations.Policies;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;

/** Instrumentation tests for {@link RecentTabsPage}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
public class RecentTabsPageTest {
    private static final String EMAIL = "[email protected]";
    private static final String NAME = "Email Emailson";

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    // FakeAccountInfoService is required to create the ProfileDataCache entry with sync_off badge
    // for Sync promo.
    @Rule public final SigninTestRule mSigninTestRule = new SigninTestRule();

    @Rule
    public final ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setRevision(8)
                    .setBugComponent(ChromeRenderTestRule.Component.UI_BROWSER_MOBILE_RECENT_TABS)
                    .build();

    @Spy private FakeRecentlyClosedTabManager mManager = new FakeRecentlyClosedTabManager();
    private ChromeTabbedActivity mActivity;
    private Tab mTab;
    private TabModel mTabModel;
    private RecentTabsPage mPage;

    @Before
    public void setUp() throws Exception {
        initMocks(this);

        RecentTabsManager.setRecentlyClosedTabManagerForTests(mManager);
        mActivityTestRule.startMainActivityOnBlankPage();
        mActivity = mActivityTestRule.getActivity();
        mTabModel = mActivity.getTabModelSelector().getModel(false);
        mTab = mActivity.getActivityTab();
    }

    @After
    public void tearDown() {
        leaveRecentTabsPage();
        ChromeSharedPreferences.getInstance()
                .removeKey(ChromePreferenceKeys.SYNC_PROMO_TOTAL_SHOW_COUNT);
    }

    @Test
    @MediumTest
    @Feature({"RecentTabsPage"})
    public void testRecentlyClosedTabs() throws ExecutionException {
        mPage = loadRecentTabsPage();
        // Set a recently closed tab and confirm a view is rendered for it.
        final RecentlyClosedTab tab =
                new RecentlyClosedTab(
                        0, 0, "Tab Title", new GURL("https://www.example.com/"), null);
        setRecentlyClosedEntries(Collections.singletonList(tab));
        Assert.assertEquals(1, mManager.getRecentlyClosedEntries(1).size());
        final String title = tab.getTitle();
        final View view = waitForView(title);

        openContextMenuAndInvokeItem(
                mActivity, view, RecentTabsRowAdapter.RecentlyClosedTabsGroup.ID_OPEN_IN_NEW_TAB);
        verify(mManager, times(1))
                .openRecentlyClosedTab(mTabModel, tab, WindowOpenDisposition.NEW_BACKGROUND_TAB);

        final int groupIdx = !DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity) ? 0 : 1;
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPage.onChildClick(null, null, groupIdx, 0, 0);
                });
        verify(mManager, times(1))
                .openRecentlyClosedTab(mTabModel, tab, WindowOpenDisposition.CURRENT_TAB);

        // Clear the recently closed tabs with the context menu and confirm the view is gone.
        openContextMenuAndInvokeItem(
                mActivity, view, RecentTabsRowAdapter.RecentlyClosedTabsGroup.ID_REMOVE_ALL);
        Assert.assertEquals(0, mManager.getRecentlyClosedEntries(1).size());
        waitForViewToDisappear(title);
    }

    @Test
    @LargeTest
    @Feature({"RecentTabsPage", "RenderTest"})
    // Disable sign-in to suppress sign-in promo, as it's unrelated to this render test.
    @Policies.Add(@Policies.Item(key = "BrowserSignin", string = "0"))
    public void testRecentlyClosedGroup_WithTitle() throws Exception {
        mPage = loadRecentTabsPage();
        // Set a recently closed group and confirm a view is rendered for it.
        final RecentlyClosedGroup group = new RecentlyClosedGroup(2, 0, "Group Title");
        Token tabGroupId = new Token(27839L, 4789L);
        group.getTabs()
                .add(
                        new RecentlyClosedTab(
                                0,
                                0,
                                "Tab Title 0",
                                new GURL("https://www.example.com/url/0"),
                                tabGroupId));
        group.getTabs()
                .add(
                        new RecentlyClosedTab(
                                1,
                                0,
                                "Tab Title 1",
                                new GURL("https://www.example.com/url/1"),
                                tabGroupId));
        setRecentlyClosedEntries(Collections.singletonList(group));
        Assert.assertEquals(1, mManager.getRecentlyClosedEntries(1).size());
        final String groupString =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return mActivity
                                    .getResources()
                                    .getString(
                                            R.string.recent_tabs_group_closure_with_title,
                                            group.getTitle());
                        });
        final View view = waitForView(groupString);

        mRenderTestRule.render(mPage.getView(), "recently_closed_group_with_title");

        final int groupIdx = !DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity) ? 0 : 1;
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPage.onChildClick(null, null, groupIdx, 0, 0);
                });
        verify(mManager, times(1)).openRecentlyClosedEntry(mTabModel, group);

        // Clear the recently closed tabs with the context menu and confirm the view is gone.
        openContextMenuAndInvokeItem(
                mActivity, view, RecentTabsRowAdapter.RecentlyClosedTabsGroup.ID_REMOVE_ALL);
        Assert.assertEquals(0, mManager.getRecentlyClosedEntries(1).size());
        waitForViewToDisappear(groupString);
    }

    @Test
    @LargeTest
    @Feature({"RecentTabsPage", "RenderTest"})
    // Disable sign-in to suppress sign-in promo, as it's unrelated to this render test.
    @Policies.Add(@Policies.Item(key = "BrowserSignin", string = "0"))
    public void testRecentlyClosedGroup_WithoutTitle() throws Exception {
        mPage = loadRecentTabsPage();
        long time = 904881600000L;
        // Set a recently closed group and confirm a view is rendered for it.
        final RecentlyClosedGroup group = new RecentlyClosedGroup(2, time, null);
        Token tabGroupId = new Token(798L, 4389L);
        group.getTabs()
                .add(
                        new RecentlyClosedTab(
                                0,
                                time,
                                "Tab Title 0",
                                new GURL("https://www.example.com/url/0"),
                                tabGroupId));
        group.getTabs()
                .add(
                        new RecentlyClosedTab(
                                1,
                                time,
                                "Tab Title 1",
                                new GURL("https://www.example.com/url/1"),
                                tabGroupId));
        setRecentlyClosedEntries(Collections.singletonList(group));
        Assert.assertEquals(1, mManager.getRecentlyClosedEntries(1).size());
        final String groupString =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return mActivity
                                    .getResources()
                                    .getString(
                                            R.string.recent_tabs_group_closure_without_title,
                                            group.getTabs().size());
                        });
        final View view = waitForView(groupString);

        mRenderTestRule.render(mPage.getView(), "recently_closed_group_without_title");

        final int groupIdx = !DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity) ? 0 : 1;
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPage.onChildClick(null, null, groupIdx, 0, 0);
                });
        verify(mManager, times(1)).openRecentlyClosedEntry(mTabModel, group);

        // Clear the recently closed tabs with the context menu and confirm the view is gone.
        openContextMenuAndInvokeItem(
                mActivity, view, RecentTabsRowAdapter.RecentlyClosedTabsGroup.ID_REMOVE_ALL);
        Assert.assertEquals(0, mManager.getRecentlyClosedEntries(1).size());
        waitForViewToDisappear(groupString);
    }

    @Test
    @LargeTest
    @Feature({"RecentTabsPage", "RenderTest"})
    // Disable sign-in to suppress sign-in promo, as it's unrelated to this render test.
    @Policies.Add(@Policies.Item(key = "BrowserSignin", string = "0"))
    public void testRecentlyClosedBulkEvent() throws Exception {
        mPage = loadRecentTabsPage();
        long time = 904881600000L;
        // Set a recently closed bulk event and confirm a view is rendered for it.
        final RecentlyClosedBulkEvent event = new RecentlyClosedBulkEvent(3, time);
        Token tabGroupId = new Token(1L, 2L);
        event.getTabGroupIdToTitleMap().put(tabGroupId, "Group 1 Title");
        event.getTabs()
                .add(
                        new RecentlyClosedTab(
                                0,
                                time,
                                "Tab Title 0",
                                new GURL("https://www.example.com/url/0"),
                                tabGroupId));
        event.getTabs()
                .add(
                        new RecentlyClosedTab(
                                1,
                                time,
                                "Tab Title 1",
                                new GURL("https://www.example.com/url/1"),
                                tabGroupId));
        event.getTabs()
                .add(
                        new RecentlyClosedTab(
                                2,
                                time,
                                "Tab Title 2",
                                new GURL("https://www.example.com/url/2"),
                                null));
        setRecentlyClosedEntries(Collections.singletonList(event));
        Assert.assertEquals(1, mManager.getRecentlyClosedEntries(1).size());
        final int size = event.getTabs().size();
        final String eventString =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return mActivity
                                    .getResources()
                                    .getString(R.string.recent_tabs_bulk_closure, size);
                        });
        final View view = waitForView(eventString);

        mRenderTestRule.render(mPage.getView(), "recently_closed_bulk_event");

        final int groupIdx = !DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity) ? 0 : 1;
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPage.onChildClick(null, null, groupIdx, 0, 0);
                });
        verify(mManager, times(1)).openRecentlyClosedEntry(mTabModel, event);

        // Clear the recently closed tabs with the context menu and confirm the view is gone.
        openContextMenuAndInvokeItem(
                mActivity, view, RecentTabsRowAdapter.RecentlyClosedTabsGroup.ID_REMOVE_ALL);
        Assert.assertEquals(0, mManager.getRecentlyClosedEntries(1).size());
        waitForViewToDisappear(eventString);
    }

    @Test
    @MediumTest
    @Feature({"RecentTabsPage"})
    @DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testEmptyStateView_replaceSyncWithSignInDisabled() {
        // Sign in and enable sync.
        mSigninTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL);
        mSigninTestRule.waitForSignin(AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL);
        SigninTestUtil.signinAndEnableSync(
                AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL,
                SyncTestUtil.getSyncServiceForLastUsedProfile());

        // Open an empty recent tabs page and confirm empty view shows.
        mPage = loadRecentTabsPage();
        onView(
                        allOf(
                                withId(R.id.empty_state_container),
                                withParent(withId(R.id.legacy_sync_promo_view_frame_layout))))
                .check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    @Feature({"RecentTabsPage"})
    @EnableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testEmptyStateView() {
        mSigninTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL);
        SigninTestUtil.signinAndEnableHistorySync(
                AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL);

        // Open an empty recent tabs page and confirm empty view shows.
        mPage = loadRecentTabsPage();
        onView(
                        allOf(
                                withId(R.id.empty_state_container),
                                withParent(withId(R.id.legacy_sync_promo_view_frame_layout))))
                .check(matches(isDisplayed()));
    }

    @Test
    @SmallTest
    @DisableFeatures(ChromeFeatureList.TAB_STRIP_LAYOUT_OPTIMIZATION)
    public void testTabStripHeightChangeCallback() {
        mPage = loadRecentTabsPage();
        var tabStripHeightChangeCallback = mPage.getTabStripHeightChangeCallbackForTesting();
        int newTabStripHeight = 40;
        ThreadUtils.runOnUiThreadBlocking(
                () -> tabStripHeightChangeCallback.onResult(newTabStripHeight));
        assertEquals(
                "Top padding of page view should be updated when tab strip height changes.",
                newTabStripHeight,
                mPage.getView().getPaddingTop());
    }

    /**
     * Generates the specified number of {@link RecentlyClosedTab} instances and sets them on the
     * manager.
     */
    private void setRecentlyClosedEntries(List<RecentlyClosedEntry> entries) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mManager.setRecentlyClosedEntries(entries);
                });
    }

    private RecentTabsPage loadRecentTabsPage() {
        mActivityTestRule.loadUrl(UrlConstants.RECENT_TABS_URL);
        RecentTabsPageTestUtils.waitForRecentTabsPageLoaded(mTab);
        return (RecentTabsPage) mTab.getNativePage();
    }

    /**
     * Leaves and destroys the {@link RecentTabsPage} by navigating the tab to {@code about:blank}.
     */
    private void leaveRecentTabsPage() {
        mActivityTestRule.loadUrl(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "RecentTabsPage is still there",
                            mTab.getNativePage(),
                            Matchers.not(Matchers.instanceOf(RecentTabsPage.class)));
                });
    }

    /** Waits for the view with the specified text to appear. */
    private View waitForView(final String text) {
        final ArrayList<View> views = new ArrayList<>();
        CriteriaHelper.pollUiThread(
                () -> {
                    mPage.getView().findViewsWithText(views, text, View.FIND_VIEWS_WITH_TEXT);
                    Criteria.checkThat(
                            "Could not find view with this text: " + text,
                            views.size(),
                            Matchers.is(1));
                });
        return views.get(0);
    }

    /** Waits for the view with the specified text to disappear. */
    private void waitForViewToDisappear(final String text) {
        CriteriaHelper.pollUiThread(
                () -> {
                    ArrayList<View> views = new ArrayList<>();
                    mPage.getView().findViewsWithText(views, text, View.FIND_VIEWS_WITH_TEXT);
                    Criteria.checkThat(
                            "View with this text is still present: " + text,
                            views,
                            Matchers.empty());
                });
    }

    private static void openContextMenuAndInvokeItem(
            final Activity activity, final View view, final int itemId) {
        // IMPLEMENTATION NOTE: Instrumentation.invokeContextMenuAction would've been much simpler,
        // but it requires the View to be focused which is hard to achieve in touch mode.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    view.performLongClick();
                    activity.getWindow().performContextMenuIdentifierAction(itemId, 0);
                });
    }
}