chromium/components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/ModalDialogViewTest.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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withChild;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withTagValue;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import android.app.Activity;
import android.content.res.Resources;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import androidx.test.espresso.action.ViewActions;
import androidx.test.filters.MediumTest;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Assert;
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.FakeTimeTestRule;
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.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.components.browser_ui.modaldialog.test.R;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modaldialog.ModalDialogProperties.ModalDialogButtonSpec;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.BlankUiTestActivity;

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

    @Rule public FakeTimeTestRule mFakeTime = new FakeTimeTestRule();

    private static Activity sActivity;
    private static Resources sResources;
    private static FrameLayout sContentView;
    private ModalDialogView mModalDialogView;
    private TextView mCustomTextView1;
    private TextView mCustomTextView2;
    private PropertyModel.Builder mModelBuilder;
    private RelativeLayout mCustomButtonBar1;
    private RelativeLayout mCustomButtonBar2;

    @BeforeClass
    public static void setupSuite() {
        activityTestRule.launchActivity(null);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sActivity = activityTestRule.getActivity();
                    sResources = sActivity.getResources();
                    sContentView = new FrameLayout(sActivity);
                    sActivity.setContentView(sContentView);
                });
    }

    @Before
    public void setupTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sContentView.removeAllViews();
                    mModelBuilder = new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS);
                    mModalDialogView =
                            (ModalDialogView)
                                    LayoutInflater.from(
                                                    new ContextThemeWrapper(
                                                            sActivity,
                                                            R.style
                                                                    .ThemeOverlay_BrowserUI_ModalDialog_TextPrimaryButton))
                                            .inflate(R.layout.modal_dialog_view, null);
                    sContentView.addView(mModalDialogView, MATCH_PARENT, WRAP_CONTENT);

                    mCustomTextView1 = new TextView(sActivity);
                    mCustomTextView1.setId(R.id.test_view_one);
                    mCustomTextView2 = new TextView(sActivity);
                    mCustomTextView2.setId(R.id.test_view_two);

                    mCustomButtonBar1 = new RelativeLayout(sActivity);
                    mCustomButtonBar1.setId(R.id.test_button_bar_one);
                    mCustomButtonBar2 = new RelativeLayout(sActivity);
                    mCustomButtonBar2.setId(R.id.test_button_bar_two);
                    Button button1 = new Button(sActivity);
                    button1.setText(R.string.ok);
                    Button button2 = new Button(sActivity);
                    button2.setText(R.string.cancel);
                    RelativeLayout.LayoutParams params =
                            new RelativeLayout.LayoutParams(
                                    ViewGroup.LayoutParams.WRAP_CONTENT,
                                    ViewGroup.LayoutParams.WRAP_CONTENT);
                    params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
                    mCustomButtonBar1.addView(button1, params);
                    mCustomButtonBar2.addView(button2, params);
                });
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testInitialStates() {
        // Verify that the default states are correct when properties are not set.
        createModel(mModelBuilder);
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));
        onView(withId(R.id.message_paragraph_1)).check(matches(not(isDisplayed())));
        onView(withId(R.id.message_paragraph_2)).check(matches(not(isDisplayed())));
        onView(withId(R.id.custom_view_not_in_scrollable)).check(matches(not(isDisplayed())));
        onView(withId(R.id.button_bar)).check(matches(not(isDisplayed())));
        onView(withId(R.id.positive_button)).check(matches(allOf(not(isDisplayed()), isEnabled())));
        onView(withId(R.id.negative_button)).check(matches(allOf(not(isDisplayed()), isEnabled())));
        onView(withId(R.id.custom_button_bar))
                .check(matches(allOf(not(isDisplayed()), isEnabled())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTitle() {
        // Verify that the title set from builder is displayed.
        PropertyModel model =
                createModel(
                        mModelBuilder.with(
                                ModalDialogProperties.TITLE, sResources, R.string.title));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.title_container))))
                .check(matches(allOf(isDisplayed(), withText(R.string.title))));
        onView(withId(R.id.title_container)).check(matches(isDisplayed()));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));

        // Set an empty title and verify that title is not shown.
        ThreadUtils.runOnUiThreadBlocking(() -> model.set(ModalDialogProperties.TITLE, ""));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.title_container))))
                .check(matches(not(isDisplayed())));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));

        // Set a String title and verify that title is displayed.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.TITLE, "My Test Title"));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.title_container))))
                .check(matches(allOf(isDisplayed(), withText("My Test Title"))));
        onView(withId(R.id.title_container)).check(matches(isDisplayed()));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTitle_Scrollable() {
        // Verify that the title set from builder is displayed.
        PropertyModel model =
                createModel(
                        mModelBuilder
                                .with(ModalDialogProperties.TITLE, sResources, R.string.title)
                                .with(ModalDialogProperties.TITLE_SCROLLABLE, true));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.scrollable_title_container))))
                .check(matches(allOf(isDisplayed(), withText(R.string.title))));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(isDisplayed()));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(isDisplayed()));
        onView(withId(R.id.message_paragraph_1)).check(matches(not(isDisplayed())));

        // Set title to not scrollable and verify that non-scrollable title is displayed.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.TITLE_SCROLLABLE, false));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.title_container))))
                .check(matches(allOf(isDisplayed(), withText(R.string.title))));
        onView(withId(R.id.title_container)).check(matches(isDisplayed()));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));
        onView(withId(R.id.message_paragraph_1)).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTitleIcon() {
        // Verify that the icon set from builder is displayed.
        PropertyModel model =
                createModel(
                        mModelBuilder.with(
                                ModalDialogProperties.TITLE_ICON,
                                sActivity,
                                R.drawable.ic_business));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.title_container))))
                .check(matches(not(isDisplayed())));
        onView(allOf(withId(R.id.title_icon), withParent(withId(R.id.title_container))))
                .check(matches(isDisplayed()));
        onView(withId(R.id.title_container)).check(matches(isDisplayed()));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));

        // Set icon to null and verify that icon is not shown.
        ThreadUtils.runOnUiThreadBlocking(() -> model.set(ModalDialogProperties.TITLE_ICON, null));
        onView(allOf(withId(R.id.title), withParent(withId(R.id.title_container))))
                .check(matches(not(isDisplayed())));
        onView(allOf(withId(R.id.title_icon), withParent(withId(R.id.title_container))))
                .check(matches(not(isDisplayed())));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testMessageParagraph1() {
        // Verify that the message_paragraph_1 set from builder is displayed.
        String msg = sResources.getString(R.string.more);
        PropertyModel model =
                createModel(mModelBuilder.with(ModalDialogProperties.MESSAGE_PARAGRAPH_1, msg));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(isDisplayed()));
        onView(withId(R.id.message_paragraph_1))
                .check(matches(allOf(isDisplayed(), withText(R.string.more))));

        // Set an empty message_paragraph_1 and verify that message_paragraph_1 is not shown.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.MESSAGE_PARAGRAPH_1, ""));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));
        onView(withId(R.id.message_paragraph_1)).check(matches(not(isDisplayed())));

        // Use CharSequence for the message_paragraph_1.
        SpannableStringBuilder sb = new SpannableStringBuilder(msg);
        sb.setSpan(new ForegroundColorSpan(0xffff0000), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.MESSAGE_PARAGRAPH_1, sb));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(isDisplayed()));
        onView(withId(R.id.message_paragraph_1))
                .check(matches(allOf(isDisplayed(), withText(R.string.more))));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testMessageParagraph2() {
        // Verify that the message_paragraph_2 set from builder is displayed.
        String msg = "Incognito warning message";
        PropertyModel model =
                createModel(mModelBuilder.with(ModalDialogProperties.MESSAGE_PARAGRAPH_2, msg));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(isDisplayed()));
        onView(withId(R.id.message_paragraph_1)).check(matches(not(isDisplayed())));
        onView(withId(R.id.message_paragraph_2))
                .check(matches(allOf(isDisplayed(), withText(msg))));

        // Set an empty message_paragraph_2 and verify that it's not shown.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.MESSAGE_PARAGRAPH_2, ""));
        onView(withId(R.id.title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.scrollable_title_container)).check(matches(not(isDisplayed())));
        onView(withId(R.id.modal_dialog_title_scroll_view)).check(matches(not(isDisplayed())));
        onView(withId(R.id.message_paragraph_2)).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testCustomView() {
        // Verify custom view set from builder is displayed.
        PropertyModel model =
                createModel(
                        mModelBuilder.with(ModalDialogProperties.CUSTOM_VIEW, mCustomTextView1));
        onView(withId(R.id.custom_view_not_in_scrollable))
                .check(matches(allOf(isDisplayed(), withChild(withId(R.id.test_view_one)))));

        // Change custom view.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.CUSTOM_VIEW, mCustomTextView2));
        onView(withId(R.id.custom_view_not_in_scrollable))
                .check(
                        matches(
                                allOf(
                                        isDisplayed(),
                                        not(withChild(withId(R.id.test_view_one))),
                                        withChild(withId(R.id.test_view_two)))));

        // Set custom view to null.
        ThreadUtils.runOnUiThreadBlocking(() -> model.set(ModalDialogProperties.CUSTOM_VIEW, null));
        onView(withId(R.id.custom_view_not_in_scrollable))
                .check(
                        matches(
                                allOf(
                                        not(isDisplayed()),
                                        not(withChild(withId(R.id.test_view_one))),
                                        not(withChild(withId(R.id.test_view_two))))));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testScrollCustomView() {
        // Verify custom view set from builder is displayed.
        var scrollView = new ScrollView(activityTestRule.getActivity());
        var linearLayout = new LinearLayout(activityTestRule.getActivity());
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        createModel(mModelBuilder.with(ModalDialogProperties.CUSTOM_VIEW, scrollView));
        // Add content.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    for (int i = 0; i < 100; i++) {
                        var textView = new TextView(activityTestRule.getActivity());
                        textView.setText(String.valueOf(i));
                        linearLayout.addView(textView);
                    }
                    scrollView.addView(linearLayout);
                    scrollView.setFillViewport(true);
                });
        // Verify the first few elements are visible.
        onView(withText("1")).check(matches(isDisplayed()));
        scrollView.scrollTo(0, scrollView.getBottom());
        // Verify after scrolling, the few elements are not visible.
        onView(withText("1")).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testCustomButtonBarView() {
        // Verify custom button bar view set from builder is displayed.
        PropertyModel model =
                createModel(
                        mModelBuilder
                                .with(
                                        ModalDialogProperties.CUSTOM_BUTTON_BAR_VIEW,
                                        mCustomButtonBar1)
                                .with(
                                        ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                        sResources,
                                        R.string.ok)
                                .with(
                                        ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                        sResources,
                                        R.string.cancel));
        onView(withId(R.id.custom_button_bar))
                .check(matches(allOf(isDisplayed(), withChild(withId(R.id.test_button_bar_one)))));

        // There are no positive and negative buttons when the custom button bar is present.
        onView(withId(R.id.button_bar)).check(matches(not(isDisplayed())));
        onView(withId(R.id.positive_button)).check(matches(not(isDisplayed())));
        onView(withId(R.id.negative_button)).check(matches(not(isDisplayed())));

        // Change custom button bar view.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.CUSTOM_BUTTON_BAR_VIEW, mCustomButtonBar2));
        onView(withId(R.id.custom_button_bar))
                .check(matches(allOf(isDisplayed(), withChild(withId(R.id.test_button_bar_two)))));

        // Set custom button bar view to null.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.CUSTOM_BUTTON_BAR_VIEW, null));
        onView(withId(R.id.custom_button_bar)).check(matches(not(isDisplayed())));

        // The positive and negative buttons are back since the custom button bar is not there.
        onView(withId(R.id.button_bar)).check(matches(isDisplayed()));
        onView(withId(R.id.positive_button)).check(matches(isDisplayed()));
        onView(withId(R.id.negative_button)).check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testButtonBar() {
        // Set text for both positive button and negative button.
        PropertyModel model =
                createModel(
                        mModelBuilder
                                .with(
                                        ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                        sResources,
                                        R.string.ok)
                                .with(
                                        ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                        sResources,
                                        R.string.cancel));
        onView(withId(R.id.button_bar)).check(matches(isDisplayed()));
        onView(withId(R.id.positive_button))
                .check(matches(allOf(isDisplayed(), isEnabled(), withText(R.string.ok))));
        onView(withId(R.id.negative_button))
                .check(matches(allOf(isDisplayed(), isEnabled(), withText(R.string.cancel))));

        // Set positive button to be disabled state.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, true));
        onView(withId(R.id.button_bar)).check(matches(isDisplayed()));
        onView(withId(R.id.positive_button))
                .check(matches(allOf(isDisplayed(), not(isEnabled()), withText(R.string.ok))));
        onView(withId(R.id.negative_button))
                .check(matches(allOf(isDisplayed(), isEnabled(), withText(R.string.cancel))));

        // Set positive button text to empty.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.POSITIVE_BUTTON_TEXT, ""));
        onView(withId(R.id.button_bar)).check(matches(isDisplayed()));
        onView(withId(R.id.positive_button)).check(matches(not(isDisplayed())));
        onView(withId(R.id.negative_button))
                .check(matches(allOf(isDisplayed(), isEnabled(), withText(R.string.cancel))));

        // Set negative button to be disabled state.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.NEGATIVE_BUTTON_DISABLED, true));
        onView(withId(R.id.button_bar)).check(matches(isDisplayed()));
        onView(withId(R.id.positive_button)).check(matches(not(isDisplayed())));
        onView(withId(R.id.negative_button))
                .check(matches(allOf(isDisplayed(), not(isEnabled()), withText(R.string.cancel))));

        // Set negative button text to empty.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, ""));
        onView(withId(R.id.button_bar)).check(matches(not(isDisplayed())));
        onView(withId(R.id.positive_button)).check(matches(not(isDisplayed())));
        onView(withId(R.id.negative_button)).check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testButtonGroup() {
        createModel(
                mModelBuilder.with(
                        ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST,
                        new ModalDialogProperties.ModalDialogButtonSpec[] {
                            new ModalDialogProperties.ModalDialogButtonSpec(
                                    ModalDialogProperties.ButtonType.POSITIVE_EPHEMERAL,
                                    sResources.getString(R.string.ok)),
                            new ModalDialogProperties.ModalDialogButtonSpec(
                                    ModalDialogProperties.ButtonType.POSITIVE,
                                    sResources.getString(R.string.ok_got_it)),
                            new ModalDialogProperties.ModalDialogButtonSpec(
                                    ModalDialogProperties.ButtonType.NEGATIVE,
                                    sResources.getString(R.string.cancel))
                        }));

        onView((withId(R.id.button_group))).check(matches(isDisplayed()));

        onView(withText(R.string.ok)).check(matches(isDisplayed()));
        onView(withText(R.string.ok_got_it)).check(matches(isDisplayed()));
        onView(withText(R.string.cancel)).check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    @DisabledTest(message = "crbug.com/329163841")
    public void testButtonGroupIsScrollable() throws InterruptedException {
        ModalDialogProperties.ModalDialogButtonSpec[] button_spec_list =
                new ModalDialogButtonSpec[20];
        for (int i = 0; i < button_spec_list.length; i++) {
            button_spec_list[i] =
                    new ModalDialogProperties.ModalDialogButtonSpec(
                            1000 + i, // ModalDialogProperties.ButtonType defines a button enum.
                            // Choose values outside the defined range.
                            sResources.getString(R.string.ok));
        }

        createModel(
                mModelBuilder.with(
                        ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST, button_spec_list));

        // Check that the first button is visible.
        onView(
                        (withTagValue(
                                is(
                                        ModalDialogView.getTagForButtonType(
                                                button_spec_list[0].getButtonType())))))
                .check(matches(isDisplayed()));

        // Swipe up a few times.
        for (int i = 0; i < 3; i++) {
            onView(
                            withTagValue(
                                    is(
                                            ModalDialogView.getTagForButtonType(
                                                    button_spec_list[3].getButtonType()))))
                    .perform(ViewActions.swipeUp());
        }

        // Check that the first button is no longer visible.
        onView(
                        (withTagValue(
                                is(
                                        ModalDialogView.getTagForButtonType(
                                                button_spec_list[0].getButtonType())))))
                .check(matches(not(isDisplayed())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTouchFilter() {
        createModel(
                mModelBuilder
                        .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, sResources, R.string.ok)
                        .with(
                                ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                sResources,
                                R.string.cancel)
                        .with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true));
        onView(withId(R.id.positive_button)).check(matches(touchFilterEnabled()));
        onView(withId(R.id.negative_button)).check(matches(touchFilterEnabled()));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTouchFilterOnButtonGroup() {
        createModel(
                mModelBuilder
                        .with(
                                ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST,
                                new ModalDialogProperties.ModalDialogButtonSpec[] {
                                    new ModalDialogProperties.ModalDialogButtonSpec(
                                            ModalDialogProperties.ButtonType.POSITIVE_EPHEMERAL,
                                            sResources.getString(R.string.ok)),
                                    new ModalDialogProperties.ModalDialogButtonSpec(
                                            ModalDialogProperties.ButtonType.POSITIVE,
                                            sResources.getString(R.string.ok_got_it)),
                                    new ModalDialogProperties.ModalDialogButtonSpec(
                                            ModalDialogProperties.ButtonType.NEGATIVE,
                                            sResources.getString(R.string.cancel))
                                })
                        .with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true));
        onView(
                        allOf(
                                withTagValue(
                                        is(
                                                ModalDialogView.getTagForButtonType(
                                                        ModalDialogProperties.ButtonType
                                                                .POSITIVE_EPHEMERAL))),
                                isDisplayed()))
                .check(matches(touchFilterEnabled()));
        onView(
                        allOf(
                                withTagValue(
                                        is(
                                                ModalDialogView.getTagForButtonType(
                                                        ModalDialogProperties.ButtonType
                                                                .POSITIVE))),
                                isDisplayed()))
                .check(matches(touchFilterEnabled()));
        onView(
                        allOf(
                                withTagValue(
                                        is(
                                                ModalDialogView.getTagForButtonType(
                                                        ModalDialogProperties.ButtonType
                                                                .NEGATIVE))),
                                isDisplayed()))
                .check(matches(touchFilterEnabled()));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTouchFilterDisabled() {
                createModel(
                        mModelBuilder
                                .with(
                                        ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                        sResources,
                                        R.string.ok)
                                .with(
                                        ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                        sResources,
                                        R.string.cancel));
        onView(withId(R.id.positive_button)).check(matches(not(touchFilterEnabled())));
        onView(withId(R.id.negative_button)).check(matches(not(touchFilterEnabled())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testTouchFilterDisabledOnButtonGroup() {
        createModel(
                mModelBuilder.with(
                        ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST,
                        new ModalDialogProperties.ModalDialogButtonSpec[] {
                            new ModalDialogProperties.ModalDialogButtonSpec(
                                    ModalDialogProperties.ButtonType.POSITIVE_EPHEMERAL,
                                    sResources.getString(R.string.ok)),
                            new ModalDialogProperties.ModalDialogButtonSpec(
                                    ModalDialogProperties.ButtonType.POSITIVE,
                                    sResources.getString(R.string.ok_got_it)),
                            new ModalDialogProperties.ModalDialogButtonSpec(
                                    ModalDialogProperties.ButtonType.NEGATIVE,
                                    sResources.getString(R.string.cancel))
                        }));
        onView(
                        allOf(
                                withTagValue(
                                        is(
                                                ModalDialogView.getTagForButtonType(
                                                        ModalDialogProperties.ButtonType
                                                                .POSITIVE_EPHEMERAL))),
                                isDisplayed()))
                .check(matches(not(touchFilterEnabled())));
        onView(
                        allOf(
                                withTagValue(
                                        is(
                                                ModalDialogView.getTagForButtonType(
                                                        ModalDialogProperties.ButtonType
                                                                .POSITIVE))),
                                isDisplayed()))
                .check(matches(not(touchFilterEnabled())));
        onView(
                        allOf(
                                withTagValue(
                                        is(
                                                ModalDialogView.getTagForButtonType(
                                                        ModalDialogProperties.ButtonType
                                                                .NEGATIVE))),
                                isDisplayed()))
                .check(matches(not(touchFilterEnabled())));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testFooterMessage() {
        // Verify that the footer message set from builder is displayed.
        String msg = sResources.getString(R.string.more);
        PropertyModel model =
                createModel(mModelBuilder.with(ModalDialogProperties.FOOTER_MESSAGE, msg));
        onView(withId(R.id.footer)).check(matches(isDisplayed()));
        onView(withId(R.id.footer_message))
                .check(matches(allOf(isDisplayed(), withText(R.string.more))));

        // Set an empty footer message and verify that footer message is not shown.
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.FOOTER_MESSAGE, ""));
        onView(withId(R.id.footer)).check(matches(not(isDisplayed())));
        onView(withId(R.id.footer_message)).check(matches(not(isDisplayed())));

        // Use CharSequence for the footer message.
        SpannableStringBuilder sb = new SpannableStringBuilder(msg);
        sb.setSpan(new ForegroundColorSpan(0xffff0000), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ModalDialogProperties.FOOTER_MESSAGE, sb));
        onView(withId(R.id.footer)).check(matches(isDisplayed()));
        onView(withId(R.id.footer_message))
                .check(matches(allOf(isDisplayed(), withText(R.string.more))));
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testButtonTapProtection() {
        final var callbackHelper = new CallbackHelper();
        var controller =
                new ModalDialogProperties.Controller() {
                    @Override
                    public void onClick(PropertyModel model, int buttonType) {
                        callbackHelper.notifyCalled();
                    }

                    @Override
                    public void onDismiss(PropertyModel model, int dismissalCause) {}
                };
        createModel(
                mModelBuilder
                        .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, sResources, R.string.ok)
                        .with(
                                ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                sResources,
                                R.string.cancel)
                        .with(ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS, 100)
                        .with(ModalDialogProperties.CONTROLLER, controller));
        onView(withId(R.id.button_bar)).check(matches(isDisplayed()));
        mModalDialogView.onEnterAnimationStarted(0);
        onView(withId(R.id.positive_button)).perform(click());
        Assert.assertEquals(
                "Not accept click event when button is frozen.", 0, callbackHelper.getCallCount());
        mFakeTime.advanceMillis(200);
        onView(withId(R.id.positive_button)).perform(click());
        Assert.assertEquals(
                "Button is clickable after time elapses", 1, callbackHelper.getCallCount());
    }

    @Test
    @MediumTest
    @Feature({"ModalDialog"})
    public void testButtonTapProtectionForButtonGroup() {
        final var callbackHelper = new CallbackHelper();
        var controller =
                new ModalDialogProperties.Controller() {
                    @Override
                    public void onClick(PropertyModel model, int buttonType) {
                        callbackHelper.notifyCalled();
                    }

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

        createModel(
                mModelBuilder
                        .with(
                                ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST,
                                new ModalDialogProperties.ModalDialogButtonSpec[] {
                                    new ModalDialogProperties.ModalDialogButtonSpec(
                                            ModalDialogProperties.ButtonType.POSITIVE_EPHEMERAL,
                                            sResources.getString(R.string.ok))
                                })
                        .with(ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS, 100)
                        .with(ModalDialogProperties.CONTROLLER, controller));
        onView(withId(R.id.button_group)).check(matches(isDisplayed()));

        mModalDialogView.onEnterAnimationStarted(0);
        onView(withText(R.string.ok)).perform(click());
        Assert.assertEquals(
                "Not accept click event when button is frozen.", 0, callbackHelper.getCallCount());
        mFakeTime.advanceMillis(200);
        onView(withText(R.string.ok)).perform(click());
        Assert.assertEquals(
                "Button is clickable after time elapses", 1, callbackHelper.getCallCount());
    }

    private static Matcher<View> touchFilterEnabled() {
        return new TypeSafeMatcher<View>() {
            @Override
            public void describeTo(Description description) {
                description.appendText("Touch filtering enabled");
            }

            @Override
            public boolean matchesSafely(View view) {
                return view.getFilterTouchesWhenObscured();
            }
        };
    }

    private PropertyModel createModel(PropertyModel.Builder modelBuilder) {
        return ModalDialogTestUtils.createModel(modelBuilder, mModalDialogView);
    }
}