chromium/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarTest.java

// Copyright 2016 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.ui.messages.snackbar;

import static org.junit.Assert.assertTrue;

import android.app.Activity;
import android.widget.FrameLayout;

import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.junit.AfterClass;
import org.junit.Assert;
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.supplier.Supplier;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.ui.messages.test.R;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.ui.accessibility.AccessibilityState;
import org.chromium.ui.test.util.BlankUiTestActivity;

import java.util.concurrent.TimeUnit;

/** Tests for {@link SnackbarManager}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class SnackbarTest {
    private SnackbarManager mManager;
    private SnackbarController mDefaultController =
            new SnackbarController() {
                @Override
                public void onDismissNoAction(Object actionData) {}

                @Override
                public void onAction(Object actionData) {}
            };

    private SnackbarController mDismissController =
            new SnackbarController() {
                @Override
                public void onDismissNoAction(Object actionData) {
                    mDismissed = true;
                }

                @Override
                public void onAction(Object actionData) {}
            };

    @ClassRule
    public static BaseActivityTestRule<BlankUiTestActivity> activityTestRule =
            new BaseActivityTestRule<>(BlankUiTestActivity.class);

    private static Activity sActivity;
    private static FrameLayout sMainParent;
    private static FrameLayout sAlternateParent1;
    private static FrameLayout sAlternateParent2;
    private boolean mDismissed;

    @BeforeClass
    public static void setupSuite() {
        BlankUiTestActivity.setTestLayout(R.layout.test_snackbar_manager_activity_layout);
        activityTestRule.launchActivity(null);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sActivity = activityTestRule.getActivity();
                    sMainParent = sActivity.findViewById(android.R.id.content);
                    sAlternateParent1 = sActivity.findViewById(R.id.alternate_parent_1);
                    sAlternateParent2 = sActivity.findViewById(R.id.alternate_parent_2);
                    SnackbarManager.setDurationForTesting(1000);
                });
    }

    @AfterClass
    public static void teardownSuite() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    SnackbarManager.resetDurationForTesting();
                    sActivity = null;
                    sMainParent = null;
                    sAlternateParent1 = null;
                    sAlternateParent2 = null;
                });
    }

    @Before
    public void setupTest() {
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager = new SnackbarManager(sActivity, sMainParent, null);
                    mManager.dismissAllSnackbars();
                    AccessibilityState.setIsPerformGesturesEnabledForTesting(false);
                });
    }

    @Test
    @MediumTest
    public void testStackQueuePersistentOrder() {
        final Snackbar stackbar =
                Snackbar.make(
                        "stack",
                        mDefaultController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        final Snackbar queuebar =
                Snackbar.make(
                        "queue",
                        mDefaultController,
                        Snackbar.TYPE_NOTIFICATION,
                        Snackbar.UMA_TEST_SNACKBAR);
        final Snackbar persistent =
                Snackbar.make(
                        "persistent",
                        mDefaultController,
                        Snackbar.TYPE_PERSISTENT,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(stackbar));
        pollSnackbarCondition(
                "First snackbar not shown",
                () -> mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == stackbar);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.showSnackbar(queuebar);
                    Assert.assertTrue("Snackbar not showing", mManager.isShowing());
                    Assert.assertEquals(
                            "Snackbars on stack should not be cancelled by snackbars on queue",
                            stackbar,
                            mManager.getCurrentSnackbarForTesting());
                });
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.showSnackbar(persistent);
                    Assert.assertTrue("Snackbar not showing", mManager.isShowing());
                    Assert.assertEquals(
                            "Snackbars on stack should not be cancelled by persistent snackbars",
                            stackbar,
                            mManager.getCurrentSnackbarForTesting());
                });
        pollSnackbarCondition(
                "Snackbar on queue not shown",
                () -> mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == queuebar);
        pollSnackbarCondition(
                "Snackbar on queue not timed out",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarForTesting() == persistent);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.onClick(null));
        pollSnackbarCondition(
                "Persistent snackbar did not get cleared", () -> !mManager.isShowing());
    }

    @Test
    @SmallTest
    public void testPersistentQueueStackOrder() {
        final Snackbar stackbar =
                Snackbar.make(
                        "stack",
                        mDefaultController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        final Snackbar queuebar =
                Snackbar.make(
                        "queue",
                        mDefaultController,
                        Snackbar.TYPE_NOTIFICATION,
                        Snackbar.UMA_TEST_SNACKBAR);
        final Snackbar persistent =
                Snackbar.make(
                        "persistent",
                        mDefaultController,
                        Snackbar.TYPE_PERSISTENT,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(persistent));
        pollSnackbarCondition(
                "First snackbar not shown",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarForTesting() == persistent);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(queuebar));
        pollSnackbarCondition(
                "Persistent snackbar was not cleared by queue snackbar",
                () -> mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == queuebar);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(stackbar));
        pollSnackbarCondition(
                "Snackbar on queue was not cleared by snackbar stack.",
                () -> mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == stackbar);
        pollSnackbarCondition(
                "Snackbar did not time out",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarForTesting() == persistent);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.onClick(null));
        pollSnackbarCondition(
                "Persistent snackbar did not get cleared", () -> !mManager.isShowing());
    }

    @Test
    @SmallTest
    public void testDismissSnackbar() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        mDismissed = false;
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(snackbar));
        pollSnackbarCondition(
                "Snackbar on queue was not cleared by snackbar stack.",
                () -> mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == snackbar);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.dismissSnackbars(mDismissController);
                    // Callers rely on onDismissNoAction being called synchronously.
                    assertTrue("onDismissNoAction not called", mDismissed);
                });
        pollSnackbarCondition(
                "Snackbar did not time out", () -> !mManager.isShowing() && mDismissed);
    }

    @Test
    @SmallTest
    public void testPersistentSnackbar() throws InterruptedException {
        int timeout = 100;
        SnackbarManager.setDurationForTesting(timeout);
        final Snackbar snackbar =
                Snackbar.make(
                        "persistent",
                        mDismissController,
                        Snackbar.TYPE_PERSISTENT,
                        Snackbar.UMA_TEST_SNACKBAR);
        mDismissed = false;
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(snackbar));
        pollSnackbarCondition(
                "Persistent Snackbar not shown.",
                () -> mManager.isShowing() && mManager.getCurrentSnackbarForTesting() == snackbar);
        TimeUnit.MILLISECONDS.sleep(timeout);
        pollSnackbarCondition(
                "Persistent snackbar timed out.", () -> mManager.isShowing() && !mDismissed);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.onClick(null));
        pollSnackbarCondition(
                "Persistent snackbar not removed on action.", () -> !mManager.isShowing());
    }

    @Test
    @SmallTest
    public void testSnackbarDuration() {
        final Snackbar snackbar =
                Snackbar.make(
                        "persistent",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    snackbar.setDuration(0);
                    Assert.assertEquals(
                            "Snackbar should use default duration when client sets duration to 0.",
                            SnackbarManager.getDefaultTypeActionSnackbarDuration(),
                            mManager.getDuration(snackbar));
                });

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    snackbar.setDuration(70000);
                    Assert.assertEquals(
                            "Snackbar should use set duration when no gesture performing a11y "
                                    + "services are running.",
                            70000,
                            mManager.getDuration(snackbar));
                });

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    snackbar.setDuration(
                            SnackbarManager.getDefaultTypeActionSnackbarDuration() / 2);
                    Assert.assertEquals(
                            "Snackbar should use default duration when client sets a very short"
                                    + " duration.",
                            SnackbarManager.getDefaultTypeActionSnackbarDuration(),
                            mManager.getDuration(snackbar));
                });

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    AccessibilityState.setIsPerformGesturesEnabledForTesting(true);
                    snackbar.setDuration(SnackbarManager.getDefaultA11yDurationForTesting() / 3);
                    Assert.assertEquals(
                            "Snackbar should use default a11y duration when set duration is less"
                                + " than default and a gesture performing a11y service is running.",
                            SnackbarManager.getDefaultA11yDurationForTesting(),
                            mManager.getDuration(snackbar));
                });

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    AccessibilityState.setIsPerformGesturesEnabledForTesting(true);
                    snackbar.setDuration(SnackbarManager.getDefaultA11yDurationForTesting() * 3);
                    Assert.assertTrue(
                            "Snackbar should use the recommended duration if it is more than "
                                    + "the default a11y duration.",
                            SnackbarManager.getDefaultA11yDurationForTesting()
                                    < mManager.getDuration(snackbar));
                });
    }

    @Test
    @SmallTest
    public void testOverrideParent_BeforeShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.overrideParent(sAlternateParent1);
                    mManager.showSnackbar(snackbar);
                });
        pollSnackbarCondition(
                "Snackbar's parent should not have been overridden, but was.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sMainParent);
    }

    @Test
    @SmallTest
    public void testOverrideParent_WhileShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.showSnackbar(snackbar);
                    mManager.overrideParent(sAlternateParent1);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sAlternateParent1);
    }

    @Test
    @SmallTest
    public void testPushParentViewToOverrideStack_BeforeShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.pushParentViewToOverrideStack(sAlternateParent1);
                    mManager.showSnackbar(snackbar);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sAlternateParent1);
    }

    @Test
    @SmallTest
    public void testPushParentViewToOverrideStack_AfterShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.showSnackbar(snackbar);
                    mManager.pushParentViewToOverrideStack(sAlternateParent1);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sAlternateParent1);
    }

    @Test
    @SmallTest
    public void testPushParentViewToOverrideStack_StackedParentOverrides() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.pushParentViewToOverrideStack(sAlternateParent1);
                    mManager.showSnackbar(snackbar);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sAlternateParent1);

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.pushParentViewToOverrideStack(sAlternateParent2);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden by the next stack item, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sAlternateParent2);
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.popParentViewFromOverrideStack(1);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden by the previous stacked parent"
                        + " override, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sAlternateParent1);

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    mManager.popParentViewFromOverrideStack(0);
                });
        pollSnackbarCondition(
                "Snackbar's parent should have been overridden by the original parent, but wasn't.",
                () ->
                        mManager.isShowing()
                                && mManager.getCurrentSnackbarViewForTesting().mParent
                                        == sMainParent);
    }

    @Test
    @SmallTest
    public void testSupplier_BeforeShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        pollSnackbarCondition(
                "Snackbar isShowing() and isShowingSupplier().get() values are not "
                        + "both false before showing snackbar.",
                () -> !mManager.isShowing() && !mManager.isShowingSupplier().get());
    }

    @Test
    @SmallTest
    public void testSupplier_WhileShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(snackbar));
        pollSnackbarCondition(
                "Snackbar isShowing() and isShowingSupplier().get() values are not "
                        + "both true while snackbar is showing.",
                () -> mManager.isShowing() && mManager.isShowingSupplier().get());
    }

    @Test
    @SmallTest
    public void testSupplier_AfterShowing() {
        final Snackbar snackbar =
                Snackbar.make(
                        "stack",
                        mDismissController,
                        Snackbar.TYPE_ACTION,
                        Snackbar.UMA_TEST_SNACKBAR);
        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mManager.showSnackbar(snackbar));
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT, () -> mManager.dismissSnackbars(mDismissController));
        pollSnackbarCondition(
                "Snackbar isShowing() and isShowingSupplier().get() values are not "
                        + "both false after dismissing snackbar.",
                () -> !mManager.isShowing() && !mManager.isShowingSupplier().get());
    }

    void pollSnackbarCondition(String message, Supplier<Boolean> condition) {
        CriteriaHelper.pollUiThread(condition::get, message);
    }
}