chromium/chrome/browser/back_press/android/java/src/org/chromium/chrome/browser/back_press/MinimizeAppAndCloseTabBackPressHandlerUnitTest.java

// Copyright 2022 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.back_press;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.os.Build.VERSION_CODES;

import androidx.activity.BackEventCompat;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.UserDataHost;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.browser.back_press.MinimizeAppAndCloseTabBackPressHandler.MinimizeAppAndCloseTabType;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabAssociatedApp;
import org.chromium.chrome.browser.tab.TabLaunchType;

import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;

/** Unit tests for {@link MinimizeAppAndCloseTabBackPressHandler}. */
@Batch(Batch.UNIT_TESTS)
@RunWith(BaseJUnit4ClassRunner.class)
@EnableFeatures({ChromeFeatureList.BACK_GESTURE_REFACTOR})
public class MinimizeAppAndCloseTabBackPressHandlerUnitTest {
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Rule public ExpectedException thrown = ExpectedException.none();

    @Mock private Callback<Tab> mSendToBackground;

    @Mock private Predicate<Tab> mShouldCloseTab;

    @Mock private Tab mTab;

    @Mock private Runnable mFinalCallback;

    private MinimizeAppAndCloseTabBackPressHandler mHandler;
    private ObservableSupplierImpl<Tab> mActivityTabSupplier;

    @Before
    public void setUp() {
        createBackPressHandler();
    }

    @Test
    @SmallTest
    public void testMinimizeAppAndCloseTab() throws ExecutionException {
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        MinimizeAppAndCloseTabBackPressHandler.HISTOGRAM,
                        MinimizeAppAndCloseTabType.MINIMIZE_APP_AND_CLOSE_TAB);
        Mockito.when(mShouldCloseTab.test(mTab)).thenReturn(true);
        UserDataHost userDataHost = ThreadUtils.runOnUiThreadBlocking(() -> new UserDataHost());
        Mockito.when(mTab.getUserDataHost()).thenReturn(userDataHost);
        ThreadUtils.runOnUiThreadBlocking(() -> TabAssociatedApp.from(mTab));
        Mockito.when(mTab.getLaunchType()).thenReturn(TabLaunchType.FROM_EXTERNAL_APP);
        ThreadUtils.runOnUiThreadBlocking(() -> mActivityTabSupplier.set(mTab));
        Assert.assertTrue(mHandler.getHandleBackPressChangedSupplier().get());
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mHandler.handleOnBackStarted(
                                new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT)));
        ThreadUtils.runOnUiThreadBlocking(mHandler::handleBackPress);
        verify(
                        mSendToBackground,
                        Mockito.description("App should be minimized with tab being closed"))
                .onResult(mTab);
        histogram.assertExpected();
        verify(mFinalCallback).run();
    }

    @Test
    @SmallTest
    public void testMinimizeAppAndCloseTab_SystemBack() throws ExecutionException {
        createBackPressHandler(true);
        // Expect no change.
        testMinimizeAppAndCloseTab();
    }

    @Test
    @SmallTest
    public void testCloseTab() throws ExecutionException {
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        MinimizeAppAndCloseTabBackPressHandler.HISTOGRAM,
                        MinimizeAppAndCloseTabType.CLOSE_TAB);
        Mockito.when(mShouldCloseTab.test(mTab)).thenReturn(true);
        UserDataHost userDataHost = ThreadUtils.runOnUiThreadBlocking(() -> new UserDataHost());
        Mockito.when(mTab.getUserDataHost()).thenReturn(userDataHost);
        ThreadUtils.runOnUiThreadBlocking(() -> TabAssociatedApp.from(mTab));
        Mockito.when(mTab.getLaunchType()).thenReturn(TabLaunchType.FROM_LONGPRESS_FOREGROUND);
        ThreadUtils.runOnUiThreadBlocking(() -> mActivityTabSupplier.set(mTab));
        Assert.assertTrue(mHandler.getHandleBackPressChangedSupplier().get());
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mHandler.handleOnBackStarted(
                                new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT)));
        ThreadUtils.runOnUiThreadBlocking(mHandler::handleBackPress);

        verify(
                        mSendToBackground,
                        Mockito.never()
                                .description("Tab should be closed without minimizing the app."))
                .onResult(mTab);
        histogram.assertExpected();
        verify(mFinalCallback).run();
    }

    @Test
    @SmallTest
    public void testCloseTab_SystemBack() throws ExecutionException {
        createBackPressHandler(true);
        // Expect no change.
        testCloseTab();
    }

    @Test
    @SmallTest
    @DisableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
    public void testMinimizeApp() {
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        MinimizeAppAndCloseTabBackPressHandler.HISTOGRAM,
                        MinimizeAppAndCloseTabType.MINIMIZE_APP);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivityTabSupplier.set(mTab);
                });
        Mockito.when(mShouldCloseTab.test(mTab)).thenReturn(false);
        Assert.assertTrue(mHandler.getHandleBackPressChangedSupplier().get());
        mHandler.handleBackPress();

        verify(
                        mSendToBackground,
                        Mockito.description("App should be minimized without closing any tab"))
                .onResult(null);
        histogram.assertExpected();
        verify(mFinalCallback).run();
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
    public void testMinimizeApp_SystemBack() {
        createBackPressHandler(true);

        var histogram =
                HistogramWatcher.newBuilder()
                        .expectNoRecords(MinimizeAppAndCloseTabBackPressHandler.HISTOGRAM)
                        .build();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivityTabSupplier.set(mTab);
                });
        Mockito.when(mShouldCloseTab.test(mTab)).thenReturn(false);

        Assert.assertFalse(
                "Back press should be handled by OS.",
                mHandler.getHandleBackPressChangedSupplier().get());
        histogram.assertExpected();
        verify(mFinalCallback, never()).run();
    }

    @Test
    @SmallTest
    @DisableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
    public void testMinimizeApp_NoValidTab() {
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        MinimizeAppAndCloseTabBackPressHandler.HISTOGRAM,
                        MinimizeAppAndCloseTabType.MINIMIZE_APP);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivityTabSupplier.set(null);
                });
        Assert.assertTrue(mHandler.getHandleBackPressChangedSupplier().get());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mHandler.handleBackPress();
                });

        verify(mSendToBackground).onResult(null);
        verify(
                        mSendToBackground,
                        Mockito.description("App should be minimized without closing any tab"))
                .onResult(null);
        histogram.assertExpected();
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
    public void testMinimizeApp_NoValidTab_SystemBack() {
        createBackPressHandler(true);

        var histogram =
                HistogramWatcher.newBuilder()
                        .expectNoRecords(MinimizeAppAndCloseTabBackPressHandler.HISTOGRAM)
                        .build();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivityTabSupplier.set(null);
                });

        Assert.assertFalse(
                "Back press should be handled by OS.",
                mHandler.getHandleBackPressChangedSupplier().get());

        thrown.expect(Matchers.instanceOf(AssertionError.class));
        thrown.expectMessage(
                "Should be disabled when there is no valid tab and back press is consumed.");

        mHandler.handleBackPress();
        histogram.assertExpected();
        verify(mSendToBackground, Mockito.never()).onResult(Mockito.any());
        verify(mShouldCloseTab, Mockito.never()).test(Mockito.any());
    }

    private void createBackPressHandler() {
        createBackPressHandler(false);
    }

    private void createBackPressHandler(boolean systemBack) {
        if (systemBack) {
            MinimizeAppAndCloseTabBackPressHandler.setVersionForTesting(VERSION_CODES.TIRAMISU);
        }
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivityTabSupplier = new ObservableSupplierImpl<>();
                });
        mHandler =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new MinimizeAppAndCloseTabBackPressHandler(
                                        mActivityTabSupplier,
                                        mShouldCloseTab,
                                        mSendToBackground,
                                        mFinalCallback,
                                        new OneshotSupplierImpl<>()));
    }
}