chromium/components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/AppModalPresenterTest.java

// Copyright 2018 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.components.browser_ui.modaldialog;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;

import static org.chromium.components.browser_ui.modaldialog.ModalDialogTestUtils.checkCurrentPresenter;
import static org.chromium.components.browser_ui.modaldialog.ModalDialogTestUtils.checkDialogDismissalCause;
import static org.chromium.components.browser_ui.modaldialog.ModalDialogTestUtils.checkPendingSize;
import static org.chromium.components.browser_ui.modaldialog.ModalDialogTestUtils.createDialog;
import static org.chromium.components.browser_ui.modaldialog.ModalDialogTestUtils.createDialogWithDialogStyle;
import static org.chromium.components.browser_ui.modaldialog.ModalDialogTestUtils.showDialogInRoot;

import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.widget.Button;

import androidx.activity.OnBackPressedCallback;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.components.browser_ui.modaldialog.test.R;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.BlankUiTestActivity;

import java.util.concurrent.TimeoutException;

/** Tests for {@link AppModalPresenter}. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class AppModalPresenterTest {
    @ClassRule
    public static BaseActivityTestRule<BlankUiTestActivity> activityTestRule =
            new BaseActivityTestRule<>(BlankUiTestActivity.class);

    private class TestObserver implements ModalDialogTestUtils.TestDialogDismissedObserver {
        public final CallbackHelper onDialogDismissedCallback = new CallbackHelper();

        @Override
        public void onDialogDismissed(int dismissalCause) {
            onDialogDismissedCallback.notifyCalled();
            checkDialogDismissalCause(mExpectedDismissalCause, dismissalCause);
        }
    }

    private static Activity sActivity;
    private static ModalDialogManager sManager;
    private TestObserver mTestObserver;
    private Integer mExpectedDismissalCause;

    @BeforeClass
    public static void setupSuite() {
        activityTestRule.launchActivity(null);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sActivity = activityTestRule.getActivity();
                    sManager =
                            new ModalDialogManager(
                                    new AppModalPresenter(sActivity),
                                    ModalDialogManager.ModalDialogType.APP);
                });
    }

    @Before
    public void setupTest() {
        mTestObserver = new TestObserver();
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(sManager::destroy);
    }

    @Test
    @SmallTest
    @Feature({"ModalDialog"})
    public void testDismiss_BackPressed() throws Exception {
        PropertyModel dialog1 = createDialog(sActivity, sManager, "1", null);
        PropertyModel dialog2 = createDialog(sActivity, sManager, "2", null);

        // Initially there are no dialogs in the pending list. Browser controls are not restricted.
        checkPendingSize(sManager, ModalDialogType.APP, 0);
        checkCurrentPresenter(sManager, null);

        // Add three dialogs available for showing. The app modal dialog should be shown first.
        showDialogInRoot(sManager, dialog1, ModalDialogType.APP);
        showDialogInRoot(sManager, dialog2, ModalDialogType.APP);
        checkPendingSize(sManager, ModalDialogType.APP, 1);
        onView(withText("1")).check(matches(isDisplayed()));
        checkCurrentPresenter(sManager, ModalDialogType.APP);

        // Perform back press. The first app modal dialog should be dismissed, and the second one
        // should be shown.
        Espresso.pressBack();
        checkPendingSize(sManager, ModalDialogType.APP, 0);
        onView(withText("1")).check(doesNotExist());
        onView(withText("2")).check(matches(isDisplayed()));
        checkCurrentPresenter(sManager, ModalDialogType.APP);

        // Perform a second back press. The second app modal dialog should be dismissed.
        Espresso.pressBack();
        checkPendingSize(sManager, ModalDialogType.APP, 0);
        onView(withText("2")).check(doesNotExist());
        checkCurrentPresenter(sManager, null);
    }

    @Test
    @SmallTest
    @Feature({"ModalDialog"})
    public void testDismiss_DismissalCause_BackPressed() throws Exception {
        PropertyModel dialog = createDialog(sActivity, sManager, "title", mTestObserver);
        mExpectedDismissalCause = DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE;

        showDialogInRoot(sManager, dialog, ModalDialogType.APP);

        // Dismiss the tab modal dialog and verify dismissal cause.
        int callCount = mTestObserver.onDialogDismissedCallback.getCallCount();
        Espresso.pressBack();
        mTestObserver.onDialogDismissedCallback.waitForCallback(callCount);

        mExpectedDismissalCause = null;
    }

    @Test
    @SmallTest
    @Feature({"ModalDialog"})
    public void testBackPressedCallback_ModalDialogProperty_IsFired() throws TimeoutException {
        PropertyModel dialog = createDialog(sActivity, sManager, "title", null);
        CallbackHelper callbackHelper = new CallbackHelper();
        final OnBackPressedCallback onBackPressedCallback =
                new OnBackPressedCallback(true) {
                    @Override
                    public void handleOnBackPressed() {
                        callbackHelper.notifyCalled();
                    }
                };

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    dialog.set(
                            ModalDialogProperties.APP_MODAL_DIALOG_BACK_PRESS_HANDLER,
                            onBackPressedCallback);
                });

        showDialogInRoot(sManager, dialog, ModalDialogType.APP);

        Espresso.pressBack();
        callbackHelper.waitForCallback(0);
    }

    @Test
    @SmallTest
    @Feature({"ModalDialog"})
    public void testButton_negativeButtonFilled() throws Exception {
        PropertyModel dialog =
                createDialog(
                        sActivity,
                        sManager,
                        "title",
                        mTestObserver,
                        ModalDialogProperties.ButtonStyles.PRIMARY_OUTLINE_NEGATIVE_FILLED);
        showDialogInRoot(sManager, dialog, ModalDialogType.APP);
        onView(withText(R.string.cancel)).check(matches(hasCurrentTextColor(Color.WHITE)));
        onView(withText(R.string.ok)).check(matches(not(hasCurrentTextColor(Color.WHITE))));
    }

    @Test
    @SmallTest
    @Feature({"ModalDialog"})
    public void testButton_primaryButtonFilled() throws Exception {
        PropertyModel dialog =
                createDialog(
                        sActivity,
                        sManager,
                        "title",
                        mTestObserver,
                        ModalDialogProperties.ButtonStyles.PRIMARY_FILLED_NEGATIVE_OUTLINE);
        showDialogInRoot(sManager, dialog, ModalDialogType.APP);
        onView(withText(R.string.cancel)).check(matches(not(hasCurrentTextColor(Color.WHITE))));
        onView(withText(R.string.ok)).check(matches(hasCurrentTextColor(Color.WHITE)));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    public void testFullscreenDarkStyle() {
        PropertyModel dialog =
                createDialogWithDialogStyle(
                        sActivity,
                        sManager,
                        "title",
                        mTestObserver,
                        ModalDialogProperties.DialogStyles.FULLSCREEN_DARK_DIALOG);
        showDialogInRoot(sManager, dialog, ModalDialogType.APP);
        Window window = ((AppModalPresenter) sManager.getCurrentPresenterForTest()).getWindow();

        assertEquals(
                sActivity.getColor(R.color.toolbar_background_primary_dark),
                window.getStatusBarColor());
        assertEquals(
                sActivity.getColor(R.color.toolbar_background_primary_dark),
                window.getNavigationBarColor());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            assertEquals(
                    sActivity.getColor(R.color.bottom_system_nav_divider_color_light),
                    window.getNavigationBarDividerColor());
        }
    }

    private static Matcher<View> hasCurrentTextColor(int expected) {
        return new BoundedMatcher<View, Button>(Button.class) {
            private int mColor;

            @Override
            public boolean matchesSafely(Button button) {
                mColor = button.getCurrentTextColor();
                return expected == mColor;
            }

            @Override
            public void describeTo(final Description description) {
                description.appendText("Color did not match " + mColor);
            }
        };
    }
}