chromium/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabModalDialogTest.java

// Copyright 2023 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.customtabs;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static org.hamcrest.Matchers.is;

import android.content.Context;
import android.content.Intent;

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

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.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Features;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.test.util.UiUtils;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;

@Batch(Batch.PER_CLASS)
@RunWith(ChromeJUnit4ClassRunner.class)
@Features.EnableFeatures(ChromeFeatureList.CCT_TAB_MODAL_DIALOG)
public class CustomTabModalDialogTest {

    @Rule
    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();

    private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
    private static final String TEST_PAGE_2 = "/chrome/test/data/android/test.html";
    private String mTestPage;
    private String mTestPage2;
    private EmbeddedTestServer mTestServer;

    @Before
    public void setUp() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(true));
        Context appContext = getInstrumentation().getTargetContext().getApplicationContext();
        mTestServer = EmbeddedTestServer.createAndStartServer(appContext);
        mTestPage = mTestServer.getURL(TEST_PAGE);
        mTestPage2 = mTestServer.getURL(TEST_PAGE_2);
        LibraryLoader.getInstance().ensureInitialized();
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));

        // finish() is called on a non-UI thread by the testing harness. Must hide the menu
        // first, otherwise the UI is manipulated on a non-UI thread.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (getActivity() == null) return;
                    AppMenuCoordinator coordinator =
                            mCustomTabActivityTestRule.getAppMenuCoordinator();
                    // CCT doesn't always have a menu (ex. in the media viewer).
                    if (coordinator == null) return;
                    AppMenuHandler handler = coordinator.getAppMenuHandler();
                    if (handler != null) handler.hideAppMenu();
                });
    }

    private CustomTabActivity getActivity() {
        return mCustomTabActivityTestRule.getActivity();
    }

    @Test
    @SmallTest
    @DisabledTest(message = "https://crbug.com/1511082")
    public void testShowAndDismissTabModalDialog() throws InterruptedException {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        var visibilityDelegate =
                mCustomTabActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getAppBrowserControlsVisibilityDelegate();

        ModalDialogManager dialogManager =
                mCustomTabActivityTestRule.getActivity().getModalDialogManagerSupplier().get();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel dialog =
                            new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                                    .with(ModalDialogProperties.TITLE, "test")
                                    .with(
                                            ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                            context.getString(
                                                    org.chromium.chrome.test.R.string.delete))
                                    .with(
                                            ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                            context.getString(
                                                    org.chromium.chrome.test.R.string.cancel))
                                    .with(
                                            ModalDialogProperties.CONTROLLER,
                                            new ModalDialogProperties.Controller() {
                                                @Override
                                                public void onClick(
                                                        PropertyModel model, int buttonType) {}

                                                @Override
                                                public void onDismiss(
                                                        PropertyModel model, int dismissalCause) {}
                                            })
                                    .build();

                    dialogManager.showDialog(dialog, ModalDialogManager.ModalDialogType.TAB);
                });

        Assert.assertNotNull(visibilityDelegate.get());
        Assert.assertEquals(
                "Browser Control should be SHOWN when dialog is being displayed.",
                BrowserControlsState.SHOWN,
                (int) visibilityDelegate.get());

        ThreadUtils.runOnUiThreadBlocking(
                () -> dialogManager.dismissAllDialogs(DialogDismissalCause.DISMISSED_BY_NATIVE));

        UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
        Assert.assertEquals(
                "Browser Control State should be BOTH when dialog becomes hidden.",
                BrowserControlsState.BOTH,
                (int) visibilityDelegate.get());
    }

    @Test
    @SmallTest
    public void testNavigationDismissTabModalDialog() {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        final Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();

        ModalDialogManager dialogManager =
                mCustomTabActivityTestRule.getActivity().getModalDialogManagerSupplier().get();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel dialog =
                            new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                                    .with(ModalDialogProperties.TITLE, "test")
                                    .with(
                                            ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                            context.getString(
                                                    org.chromium.chrome.test.R.string.delete))
                                    .with(
                                            ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                            context.getString(
                                                    org.chromium.chrome.test.R.string.cancel))
                                    .with(
                                            ModalDialogProperties.CONTROLLER,
                                            new ModalDialogProperties.Controller() {
                                                @Override
                                                public void onClick(
                                                        PropertyModel model, int buttonType) {}

                                                @Override
                                                public void onDismiss(
                                                        PropertyModel model, int dismissalCause) {}
                                            })
                                    .build();

                    dialogManager.showDialog(dialog, ModalDialogManager.ModalDialogType.TAB);
                });

        CriteriaHelper.pollUiThread(() -> dialogManager.isShowing());

        ThreadUtils.runOnUiThreadBlocking(
                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);

        Assert.assertTrue(tab.canGoBack());
        Assert.assertFalse(tab.canGoForward());

        CriteriaHelper.pollUiThread(() -> !dialogManager.isShowing());
    }

    @Test
    @SmallTest
    @Features.DisableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
    public void testBackPressDismissTabModalDialog() {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        final Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();

        ModalDialogManager dialogManager =
                mCustomTabActivityTestRule.getActivity().getModalDialogManagerSupplier().get();

        ThreadUtils.runOnUiThreadBlocking(
                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel dialog =
                            new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                                    .with(ModalDialogProperties.TITLE, "test")
                                    .with(
                                            ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                            context.getString(
                                                    org.chromium.chrome.test.R.string.delete))
                                    .with(
                                            ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                            context.getString(
                                                    org.chromium.chrome.test.R.string.cancel))
                                    .with(
                                            ModalDialogProperties.CONTROLLER,
                                            new ModalDialogProperties.Controller() {
                                                @Override
                                                public void onClick(
                                                        PropertyModel model, int buttonType) {}

                                                @Override
                                                public void onDismiss(
                                                        PropertyModel model, int dismissalCause) {}
                                            })
                                    .build();

                    dialogManager.showDialog(dialog, ModalDialogManager.ModalDialogType.TAB);
                });
        CriteriaHelper.pollUiThread(() -> dialogManager.isShowing(), "Dialog should be displayed");

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.BackPress.Intercept",
                        BackPressManager.getHistogramValue(
                                BackPressHandler.Type.TAB_MODAL_HANDLER));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mCustomTabActivityTestRule
                            .getActivity()
                            .getOnBackPressedDispatcher()
                            .onBackPressed();
                });

        Assert.assertTrue("Should be able to navigate back after navigation", tab.canGoBack());
        Assert.assertFalse("Should be unable to navigate forward", tab.canGoForward());
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Tab should not be navigated when dialog is dismissed",
                            ChromeTabUtils.getUrlStringOnUiThread(getActivity().getActivityTab()),
                            is(mTestPage2));
                });

        histogramWatcher.assertExpected("Dialog should be dismissed by back press");
        CriteriaHelper.pollUiThread(
                () -> !dialogManager.isShowing(), "Dialog should be dismissed by back press");
    }

    @Test
    @SmallTest
    @Features.EnableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
    public void testBackPressDismissTabModalDialog_BackGestureRefactor() {
        testBackPressDismissTabModalDialog();
    }
}