chromium/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherLayoutPTTest.java

// Copyright 2024 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.tasks.tab_management;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import static org.chromium.base.GarbageCollectionTestUtils.canBeGarbageCollected;
import static org.chromium.base.test.transit.TransitAsserts.assertFinalDestination;

import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.widget.ImageView;

import androidx.test.filters.MediumTest;

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.chromium.base.ThreadUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.transit.CarryOn;
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.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.TestAnimations.EnableAnimations;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.transit.BlankCTATabInitialStatePublicTransitRule;
import org.chromium.chrome.test.transit.Journeys;
import org.chromium.chrome.test.transit.hub.IncognitoTabSwitcherStation;
import org.chromium.chrome.test.transit.hub.NewTabGroupDialogFacility;
import org.chromium.chrome.test.transit.hub.RegularTabSwitcherStation;
import org.chromium.chrome.test.transit.hub.TabSwitcherListEditorFacility;
import org.chromium.chrome.test.transit.ntp.RegularNewTabPageStation;
import org.chromium.chrome.test.transit.page.PageStation;
import org.chromium.chrome.test.transit.page.WebPageStation;
import org.chromium.chrome.test.transit.tabmodel.TabThumbnailsCapturedCarryOn;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.tab_groups.TabGroupColorId;
import org.chromium.net.test.EmbeddedTestServer;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;

/** Tests for the {@link TabSwitcherLayout}. */
@SuppressWarnings("ConstantConditions")
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DisableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
@Restriction({Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE})
@Batch(Batch.PER_CLASS)
public class TabSwitcherLayoutPTTest {

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

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

    @Rule
    public BlankCTATabInitialStatePublicTransitRule mInitialStateRule =
            new BlankCTATabInitialStatePublicTransitRule(sActivityTestRule);

    @Rule
    public ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setRevision(1)
                    .setBugComponent(ChromeRenderTestRule.Component.UI_BROWSER_MOBILE_HUB)
                    .build();

    private static EmbeddedTestServer sTestServer;

    private WebPageStation mStartPage;
    private WeakReference<Bitmap> mBitmap;

    @Before
    public void setUp() throws ExecutionException {
        // After setUp, Chrome is launched and has one NTP.
        mStartPage = mInitialStateRule.startOnBlankPage();

        sActivityTestRule
                .getActivity()
                .getTabContentManager()
                .setCaptureMinRequestTimeForTesting(0);
    }

    @BeforeClass
    public static void setUpClass() throws ExecutionException {
        sTestServer = sActivityTestRule.getTestServer();
    }

    /** Enters the regular Tab Switcher, making sure all tabs have a thumbnail. */
    private RegularTabSwitcherStation enterRegularHTSWithThumbnailChecking(
            PageStation currentStation) {
        RegularTabSwitcherStation tabSwitcherStation = currentStation.openRegularTabSwitcher();
        CarryOn.pickUp(
                new TabThumbnailsCapturedCarryOn(/* isIncognito= */ false), /* trigger= */ null);
        return tabSwitcherStation;
    }

    /** Enters the Incognito Tab Switcher, making sure all tabs have a thumbnail. */
    private IncognitoTabSwitcherStation enterIncognitoHTSWithThumbnailChecking(
            PageStation currentStation) {
        IncognitoTabSwitcherStation tabSwitcherStation = currentStation.openIncognitoTabSwitcher();
        CarryOn.pickUp(
                new TabThumbnailsCapturedCarryOn(/* isIncognito= */ true), /* trigger= */ null);
        return tabSwitcherStation;
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    @DisabledTest(message = "Test is flaky due to thumbnails not being reliably captured")
    public void testRenderGrid_10WebTabs() throws IOException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        WebPageStation pageStation =
                Journeys.prepareTabsWithThumbnails(
                        mStartPage, 10, 0, "about:blank", WebPageStation::newBuilder);
        // Make sure all thumbnails are there before switching tabs.
        RegularTabSwitcherStation tabSwitcherStation =
                enterRegularHTSWithThumbnailChecking(pageStation);
        pageStation = tabSwitcherStation.selectTabAtIndex(0, WebPageStation.newBuilder());

        tabSwitcherStation = pageStation.openRegularTabSwitcher();
        mRenderTestRule.render(cta.findViewById(R.id.pane_frame), "10_web_tabs");

        WebPageStation previousPage =
                tabSwitcherStation.leaveHubToPreviousTabViaBack(WebPageStation.newBuilder());
        assertFinalDestination(previousPage);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    @DisabledTest(message = "Test is flaky due to thumbnails not being reliably captured")
    public void testRenderGrid_10WebTabs_InitialScroll() throws IOException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        WebPageStation pageStation =
                Journeys.prepareTabsWithThumbnails(
                        mStartPage, 10, 0, "about:blank", WebPageStation::newBuilder);
        assertEquals(9, cta.getTabModelSelector().getCurrentModel().index());
        RegularTabSwitcherStation tabSwitcherStation =
                enterRegularHTSWithThumbnailChecking(pageStation);
        // Make sure the grid tab switcher is scrolled down to show the selected tab.
        mRenderTestRule.render(cta.findViewById(R.id.pane_frame), "10_web_tabs-select_last");

        WebPageStation previousPage =
                tabSwitcherStation.leaveHubToPreviousTabViaBack(WebPageStation.newBuilder());
        assertFinalDestination(previousPage);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    @DisabledTest(message = "Test is flaky due to thumbnails not being reliably captured")
    public void testRenderGrid_3WebTabs() throws IOException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        WebPageStation pageStation =
                Journeys.prepareTabsWithThumbnails(
                        mStartPage, 3, 0, sTestServer.getURL(TEST_URL), WebPageStation::newBuilder);
        // Make sure all thumbnails are there before switching tabs.
        RegularTabSwitcherStation tabSwitcherStation =
                enterRegularHTSWithThumbnailChecking(pageStation);
        pageStation = tabSwitcherStation.selectTabAtIndex(0, WebPageStation.newBuilder());

        tabSwitcherStation = pageStation.openRegularTabSwitcher();

        mRenderTestRule.render(cta.findViewById(R.id.pane_frame), "3_web_tabs");

        WebPageStation previousPage =
                tabSwitcherStation.leaveHubToPreviousTabViaBack(WebPageStation.newBuilder());
        assertFinalDestination(previousPage);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    @DisabledTest(message = "Test is flaky due to thumbnails not being reliably captured")
    public void testRenderGrid_3NativeTabs() throws IOException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        RegularNewTabPageStation pageStation =
                Journeys.prepareTabsWithThumbnails(
                        mStartPage,
                        3,
                        0,
                        UrlConstants.NTP_URL,
                        RegularNewTabPageStation::newBuilder);
        // Make sure all thumbnails are there before switching tabs.
        RegularTabSwitcherStation tabSwitcherStation =
                enterRegularHTSWithThumbnailChecking(pageStation);
        pageStation = tabSwitcherStation.selectTabAtIndex(0, RegularNewTabPageStation.newBuilder());

        tabSwitcherStation = pageStation.openRegularTabSwitcher();

        mRenderTestRule.render(cta.findViewById(R.id.pane_frame), "3_native_tabs");

        RegularNewTabPageStation previousPage =
                tabSwitcherStation.leaveHubToPreviousTabViaBack(
                        RegularNewTabPageStation.newBuilder());
        assertFinalDestination(previousPage);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    @DisabledTest(message = "Test is flaky due to thumbnails not being reliably captured")
    @RequiresRestart("Disable batching while re-enabling other tests.")
    public void testRenderGrid_Incognito() throws IOException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        // Prepare some incognito tabs and enter tab switcher.
        WebPageStation pageStation =
                Journeys.createTabsWithThumbnails(
                        mStartPage,
                        3,
                        "about:blank",
                        /* isIncognito= */ true,
                        WebPageStation::newBuilder);
        assertTrue(cta.getCurrentTabModel().isIncognito());
        // Make sure all thumbnails are there before switching tabs.
        IncognitoTabSwitcherStation tabSwitcherStation =
                enterIncognitoHTSWithThumbnailChecking(pageStation);
        pageStation = tabSwitcherStation.selectTabAtIndex(0, WebPageStation.newBuilder());
        tabSwitcherStation = pageStation.openIncognitoTabSwitcher();
        ChromeRenderTestRule.sanitize(cta.findViewById(R.id.pane_frame));
        mRenderTestRule.render(cta.findViewById(R.id.pane_frame), "3_incognito_web_tabs");

        WebPageStation previousPage =
                tabSwitcherStation.leaveHubToPreviousTabViaBack(WebPageStation.newBuilder());
        assertFinalDestination(previousPage);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    @EnableFeatures(ChromeFeatureList.TAB_GROUP_PARITY_ANDROID)
    @DisabledTest(message = "Test is flaky due to thumbnails not being reliably captured")
    public void testRenderGrid_1TabGroup_ColorIcon() throws IOException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();

        WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
        int firstTabId = firstPage.getLoadedTab().getId();
        RegularNewTabPageStation secondPage = firstPage.openRegularTabAppMenu().openNewTab();
        int secondTabId = secondPage.getLoadedTab().getId();
        // Make sure all thumbnails are there before switching tabs.
        RegularTabSwitcherStation tabSwitcher = enterRegularHTSWithThumbnailChecking(secondPage);
        TabSwitcherListEditorFacility editor = tabSwitcher.openAppMenu().clickSelectTabs();
        editor = editor.addTabToSelection(0, firstTabId);
        editor = editor.addTabToSelection(1, secondTabId);

        NewTabGroupDialogFacility dialog =
                editor.openAppMenuWithEditor().groupTabsWithParityEnabled();
        dialog = dialog.inputName("test_tab_group_name");
        dialog = dialog.pickColor(TabGroupColorId.RED);
        dialog.pressDone();

        ChromeRenderTestRule.sanitize(cta.findViewById(R.id.pane_frame));
        mRenderTestRule.render(
                cta.findViewById(R.id.pane_frame), "1_tab_group_GTS_card_item_color_icon");

        WebPageStation previousPage =
                tabSwitcher.leaveHubToPreviousTabViaBack(WebPageStation.newBuilder());
        assertFinalDestination(previousPage);
    }

    @Test
    @MediumTest
    @EnableAnimations
    public void testTabToGridAndBack_NoReset() {
        WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
        WebPageStation page =
                roundtripToHTSWithThumbnailChecks(
                        firstPage,
                        WebPageStation::newBuilder,
                        () -> {},
                        /* canGarbageCollectBitmaps= */ false);
        assertFinalDestination(page);
    }

    @Test
    @MediumTest
    @EnableAnimations
    public void testTabToGridAndBack_SoftCleanup() {
        WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        Runnable resetHTSStateOnUiThread =
                () -> {
                    var tabSwitcherPane =
                            (TabSwitcherPaneBase)
                                    cta.getHubManagerSupplierForTesting()
                                            .get()
                                            .getPaneManager()
                                            .getFocusedPaneSupplier()
                                            .get();
                    tabSwitcherPane.softCleanupForTesting();
                };
        WebPageStation page =
                roundtripToHTSWithThumbnailChecks(
                        firstPage,
                        WebPageStation::newBuilder,
                        resetHTSStateOnUiThread,
                        /* canGarbageCollectBitmaps= */ true);
        assertFinalDestination(page);
    }

    @Test
    @MediumTest
    @EnableAnimations
    public void testTabToGridAndBack_SoftCleanup_Ntp() {
        WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
        RegularNewTabPageStation ntp = firstPage.openRegularTabAppMenu().openNewTab();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        Runnable resetHTSStateOnUiThread =
                () -> {
                    var tabSwitcherPane =
                            (TabSwitcherPaneBase)
                                    cta.getHubManagerSupplierForTesting()
                                            .get()
                                            .getPaneManager()
                                            .getFocusedPaneSupplier()
                                            .get();
                    tabSwitcherPane.softCleanupForTesting();
                };
        ntp =
                roundtripToHTSWithThumbnailChecks(
                        ntp,
                        RegularNewTabPageStation::newBuilder,
                        resetHTSStateOnUiThread,
                        /* canGarbageCollectBitmaps= */ true);
        assertFinalDestination(ntp);
    }

    @Test
    @MediumTest
    @EnableAnimations
    public void testTabToGridAndBack_HardCleanup() {
        WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        Runnable resetHTSStateOnUiThread =
                () -> {
                    var tabSwitcherPane =
                            (TabSwitcherPaneBase)
                                    cta.getHubManagerSupplierForTesting()
                                            .get()
                                            .getPaneManager()
                                            .getFocusedPaneSupplier()
                                            .get();
                    tabSwitcherPane.softCleanupForTesting();
                    tabSwitcherPane.hardCleanupForTesting();
                };
        WebPageStation page =
                roundtripToHTSWithThumbnailChecks(
                        firstPage,
                        WebPageStation::newBuilder,
                        resetHTSStateOnUiThread,
                        /* canGarbageCollectBitmaps= */ true);
        assertFinalDestination(page);
    }

    @Test
    @MediumTest
    @EnableAnimations
    public void testTabToGridAndBack_NoCoordinator() {
        WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        Runnable resetHTSStateOnUiThread =
                () -> {
                    var tabSwitcherPane =
                            (TabSwitcherPaneBase)
                                    cta.getHubManagerSupplierForTesting()
                                            .get()
                                            .getPaneManager()
                                            .getFocusedPaneSupplier()
                                            .get();
                    tabSwitcherPane.softCleanupForTesting();
                    tabSwitcherPane.hardCleanupForTesting();
                    tabSwitcherPane.destroyCoordinatorForTesting();
                };
        WebPageStation page =
                roundtripToHTSWithThumbnailChecks(
                        firstPage,
                        WebPageStation::newBuilder,
                        resetHTSStateOnUiThread,
                        /* canGarbageCollectBitmaps= */ true);
        assertFinalDestination(page);
    }

    private <T extends PageStation> T roundtripToHTSWithThumbnailChecks(
            T page,
            Supplier<PageStation.Builder<T>> destinationBuiderFactory,
            Runnable resetHTSStateOnUiThread,
            boolean canGarbageCollectBitmaps) {
        RegularTabSwitcherStation tabSwitcher = enterRegularHTSWithThumbnailChecking(page);

        // TODO(crbug.com/324919909): Migrate this to a HubTabSwitcherCardFacility with a tab
        // thumbnail as a view element.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ImageView view =
                            (ImageView)
                                    sActivityTestRule
                                            .getActivity()
                                            .findViewById(R.id.tab_thumbnail);
                    mBitmap =
                            new WeakReference<>(((BitmapDrawable) view.getDrawable()).getBitmap());
                    assertNotNull(mBitmap.get());
                });

        page = tabSwitcher.leaveHubToPreviousTabViaBack(destinationBuiderFactory.get());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    resetHTSStateOnUiThread.run();
                });

        if (canGarbageCollectBitmaps) {
            assertTrue(canBeGarbageCollected(mBitmap));
        } else {
            assertFalse(canBeGarbageCollected(mBitmap));
        }

        tabSwitcher = enterRegularHTSWithThumbnailChecking(page);
        return tabSwitcher.leaveHubToPreviousTabViaBack(destinationBuiderFactory.get());
    }
}