chromium/chrome/browser/back_press/android/java/src/org/chromium/chrome/browser/back_press/BackPressManagerUnitTest.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 android.os.Build;

import androidx.activity.BackEventCompat;
import androidx.annotation.NonNull;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;

import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;

import java.util.concurrent.TimeoutException;

/** Unit tests for {@link BackPressManager}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@EnableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
public class BackPressManagerUnitTest {

    private class EmptyBackPressHandler implements BackPressHandler {
        private ObservableSupplierImpl<Boolean> mSupplier = new ObservableSupplierImpl<>();
        private CallbackHelper mCallbackHelper = new CallbackHelper();

        @Override
        public @BackPressResult int handleBackPress() {
            mCallbackHelper.notifyCalled();
            return BackPressResult.SUCCESS;
        }

        @Override
        public ObservableSupplierImpl<Boolean> getHandleBackPressChangedSupplier() {
            return mSupplier;
        }

        public CallbackHelper getCallbackHelper() {
            return mCallbackHelper;
        }

        @Override
        public void handleOnBackCancelled() {}

        @Override
        public void handleOnBackProgressed(@NonNull BackEventCompat backEvent) {}

        @Override
        public void handleOnBackStarted(@NonNull BackEventCompat backEvent) {}
    }

    @Test
    @Before
    public void setup() {
        MinimizeAppAndCloseTabBackPressHandler.setVersionForTesting(Build.VERSION_CODES.TIRAMISU);
    }

    @Test
    public void testBasic() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);

        Assert.assertFalse(
                "Callback should be disabled if none of handlers are enabled",
                manager.getCallback().isEnabled());
        try {
            manager.getCallback().handleOnBackPressed();
        } catch (AssertionError ignored) {
        }
        Assert.assertEquals(
                "Handler's callback should not be executed if it is disabled",
                0,
                h1.getCallbackHelper().getCallCount());

        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertEquals(
                "Should return the active handler", h1, manager.getEnabledBackPressHandler());
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled",
                manager.getCallback().isEnabled());
        manager.getCallback().handleOnBackPressed();
        Assert.assertEquals(
                "Handler's callback should be executed if it is enabled",
                1,
                h1.getCallbackHelper().getCallCount());

        h1.getHandleBackPressChangedSupplier().set(false);
        try {
            manager.getCallback().handleOnBackPressed();
        } catch (AssertionError ignored) {
        }
        Assert.assertEquals(
                "Handler's callback should not be executed if it is disabled",
                1,
                h1.getCallbackHelper().getCallCount());
    }

    @Test
    public void testMultipleHandlers() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);

        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled.",
                manager.getCallback().isEnabled());

        h1.getHandleBackPressChangedSupplier().set(false);
        Assert.assertFalse(
                "Callback should be disabled if none of handlers are enabled.",
                manager.getCallback().isEnabled());

        h2.getHandleBackPressChangedSupplier().set(true);
        Assert.assertEquals(
                "Should return the first active handler", h2, manager.getEnabledBackPressHandler());
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled.",
                manager.getCallback().isEnabled());

        manager.getCallback().handleOnBackPressed();
        Assert.assertEquals(
                "Handler's callback should not be triggered if it's disabled",
                0,
                h1.getCallbackHelper().getCallCount());
        Assert.assertEquals(
                "Handler's callback should be triggered if it's enabled",
                1,
                h2.getCallbackHelper().getCallCount());

        h1.getHandleBackPressChangedSupplier().set(true);
        manager.getCallback().handleOnBackPressed();
        Assert.assertEquals(
                "Handler's callback should be triggered if it's enabled",
                1,
                h1.getCallbackHelper().getCallCount());
        Assert.assertEquals(
                "Handler's callback should not be triggered if other earlier handler has already"
                        + " consumed the event",
                1,
                h2.getCallbackHelper().getCallCount());
    }

    @Test
    public void testHighPriority() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);

        h1.getHandleBackPressChangedSupplier().set(true);
        h2.getHandleBackPressChangedSupplier().set(true);
        manager.getCallback().handleOnBackPressed();
        Assert.assertEquals(
                "Should return the active handler of higher priority",
                h1,
                manager.getEnabledBackPressHandler());
        Assert.assertEquals(
                "Enabled handler of higher priority should intercept the back gesture",
                1,
                h1.getCallbackHelper().getCallCount());

        h1.getHandleBackPressChangedSupplier().set(false);
        h2.getHandleBackPressChangedSupplier().set(true);
        manager.getCallback().handleOnBackPressed();
        Assert.assertEquals(
                "Disabled handler should be unable to intercept the back gesture",
                1,
                h1.getCallbackHelper().getCallCount());
        Assert.assertEquals(
                "Enabled handler of lower priority should be able to intercept the back gesture if"
                        + " there is no other enabled handler of higher priority..",
                1,
                h2.getCallbackHelper().getCallCount());
    }

    @Test
    public void testRemoveHandler() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        Assert.assertEquals("Two handlers should be registered", 2, getHandlerCount(manager));

        manager.removeHandler(h2);
        Assert.assertEquals("One handler should be removed", 1, getHandlerCount(manager));
        Assert.assertFalse("One handler should be removed", manager.has(2));

        manager.removeHandler(h1);
        Assert.assertEquals("All handlers should have been removed", 0, getHandlerCount(manager));
        Assert.assertFalse("All handlers should have been removed", manager.has(1));
    }

    @Test
    public void testRemoveEnabledHandler() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        Assert.assertEquals("Two handlers should be registered", 2, getHandlerCount(manager));
        Assert.assertFalse(
                "Callback should be disabled if no handler is enabled.",
                manager.getCallback().isEnabled());

        h1.getHandleBackPressChangedSupplier().set(true);
        h2.getHandleBackPressChangedSupplier().set(true);
        Assert.assertTrue(
                "Callback should be enabled if any handler is enabled.",
                manager.getCallback().isEnabled());

        manager.removeHandler(h2);
        Assert.assertEquals("One handler should be removed", 1, getHandlerCount(manager));
        Assert.assertFalse("One handler should be removed", manager.has(2));
        Assert.assertTrue(
                "Callback should be enabled if any handler is enabled.",
                manager.getCallback().isEnabled());

        manager.removeHandler(h1);
        Assert.assertEquals("All handlers should have been removed", 0, getHandlerCount(manager));
        Assert.assertFalse("All handlers should have been removed", manager.has(1));
        Assert.assertFalse(
                "Callback should be disabled if no handler is registered.",
                manager.getCallback().isEnabled());
    }

    @Test
    public void testNoHandlerRegistered() {
        BackPressManager manager = new BackPressManager();
        Assert.assertFalse(
                "Callback should be disabled if no handler is registered",
                manager.getCallback().isEnabled());
    }

    @Test
    public void testDisabledHandlers() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);

        Assert.assertFalse(
                "Callback should be disabled if no value is provided by handler",
                manager.getCallback().isEnabled());
        try {
            manager.getCallback().handleOnBackPressed();
        } catch (AssertionError ignored) {
        }
        Assert.assertEquals(
                "Callback should be disabled if no value is provided by handler",
                0,
                h1.getCallbackHelper().getCallCount());
        Assert.assertEquals(
                "Callback should be disabled if no value is provided by handler",
                0,
                h2.getCallbackHelper().getCallCount());

        h1.getHandleBackPressChangedSupplier().set(false);
        Assert.assertFalse(
                "Callback should be disabled if handler is disabled",
                manager.getCallback().isEnabled());
        try {
            manager.getCallback().handleOnBackPressed();
        } catch (AssertionError ignored) {
        }
        Assert.assertEquals(
                "Callback should be disabled if handler is disabled",
                0,
                h1.getCallbackHelper().getCallCount());
        Assert.assertEquals(
                "Callback should be disabled if handler is disabled",
                0,
                h2.getCallbackHelper().getCallCount());

        h2.getHandleBackPressChangedSupplier().set(false);
        Assert.assertFalse(
                "Callback should be disabled if handler is disabled",
                manager.getCallback().isEnabled());
        try {
            manager.getCallback().handleOnBackPressed();
        } catch (AssertionError ignored) {
        }
        Assert.assertEquals(
                "Callback should be disabled if handler is disabled",
                0,
                h1.getCallbackHelper().getCallCount());
        Assert.assertEquals(
                "Callback should be disabled if handler is disabled",
                0,
                h2.getCallbackHelper().getCallCount());

        Assert.assertFalse(
                "Callback should be disabled if no value is provided by handler",
                manager.getCallback().isEnabled());
    }

    @Test
    public void testDestroy() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled",
                manager.getCallback().isEnabled());

        manager.destroy();
        Assert.assertFalse(
                "Callback should be disabled if manager class has been destroyed",
                manager.getCallback().isEnabled());
    }

    @Test
    public void testObservableSupplierNullValue() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled",
                manager.getCallback().isEnabled());

        h1.getHandleBackPressChangedSupplier().set(null);
        Assert.assertFalse(
                "Callback should be disabled if no handler is enabled",
                manager.getCallback().isEnabled());
    }

    @Test
    @DisableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
    public void testAlwaysEnabledCallback_TabbedActivity() {
        BackPressManager manager = new BackPressManager();
        manager.setHasSystemBackArm(true);
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled",
                manager.getCallback().isEnabled());
        h1.getHandleBackPressChangedSupplier().set(false);
        Assert.assertFalse("No handler is enabled", manager.shouldInterceptBackPress());
        Assert.assertTrue("Callback is always enabled", manager.getCallback().isEnabled());
    }

    @Test
    @DisableFeatures({ChromeFeatureList.BACK_TO_HOME_ANIMATION})
    public void testAlwaysEnabledCallback_NonTabbedActivity() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        EmptyBackPressHandler h2 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled",
                manager.getCallback().isEnabled());
        h1.getHandleBackPressChangedSupplier().set(false);
        Assert.assertFalse("No handler is enabled", manager.shouldInterceptBackPress());
        Assert.assertFalse(
                "Callback should not be always enabled on non tabbed activity",
                manager.getCallback().isEnabled());
    }

    @Test
    public void testOnBackPressProgressed() {
        BackPressManager manager = new BackPressManager();
        manager.setIsGestureNavEnabledSupplier(() -> true);
        EmptyBackPressHandler h1 = Mockito.spy(new EmptyBackPressHandler());
        EmptyBackPressHandler h2 = Mockito.spy(new EmptyBackPressHandler());
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        h1.getHandleBackPressChangedSupplier().set(false);
        h2.getHandleBackPressChangedSupplier().set(true);
        Assert.assertEquals(
                "Should return the active handler", h2, manager.getEnabledBackPressHandler());
        var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT);
        manager.getCallback().handleOnBackStarted(backEvent);
        Mockito.verify(h2).handleOnBackStarted(backEvent);

        backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_LEFT);
        manager.getCallback().handleOnBackProgressed(backEvent);
        Mockito.verify(h2).handleOnBackProgressed(backEvent);

        backEvent = new BackEventCompat(2, 0, 1, BackEventCompat.EDGE_LEFT);
        manager.getCallback().handleOnBackProgressed(backEvent);
        Mockito.verify(h2).handleOnBackProgressed(backEvent);

        manager.getCallback().handleOnBackPressed();
        Mockito.verify(h2).handleBackPress();

        Mockito.verify(
                        h2,
                        Mockito.never()
                                .description(
                                        "Cancelled should never be called if back is not"
                                                + " cancelled"))
                .handleOnBackCancelled();
    }

    @Test
    public void testOnBackPressCancelled() {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = Mockito.spy(new EmptyBackPressHandler());
        EmptyBackPressHandler h2 = Mockito.spy(new EmptyBackPressHandler());
        manager.addHandler(h1, 0);
        manager.addHandler(h2, 1);
        h1.getHandleBackPressChangedSupplier().set(false);
        h2.getHandleBackPressChangedSupplier().set(true);
        Assert.assertEquals(
                "Should return the active handler", h2, manager.getEnabledBackPressHandler());
        var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT);
        manager.getCallback().handleOnBackStarted(backEvent);
        Mockito.verify(h2).handleOnBackStarted(backEvent);

        backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_LEFT);
        manager.getCallback().handleOnBackProgressed(backEvent);
        Mockito.verify(h2).handleOnBackProgressed(backEvent);

        backEvent = new BackEventCompat(2, 0, 1, BackEventCompat.EDGE_LEFT);
        manager.getCallback().handleOnBackProgressed(backEvent);
        Mockito.verify(h2).handleOnBackProgressed(backEvent);

        manager.getCallback().handleOnBackCancelled();
        Mockito.verify(h2).handleOnBackCancelled();

        Mockito.verify(
                        h2,
                        Mockito.never()
                                .description(
                                        "handleBackPress should never be called if back is"
                                                + " cancelled"))
                .handleBackPress();
    }

    @Test
    public void testOnBackPressedCallback() throws TimeoutException {
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = new EmptyBackPressHandler();
        manager.addHandler(h1, 0);

        CallbackHelper callbackHelper = new CallbackHelper();
        manager.setOnBackPressedListener(callbackHelper::notifyCalled);

        h1.getHandleBackPressChangedSupplier().set(true);
        Assert.assertEquals(
                "Should return the active handler", h1, manager.getEnabledBackPressHandler());
        Assert.assertTrue(
                "Callback should be enabled if any of handlers are enabled",
                manager.getCallback().isEnabled());
        manager.getCallback().handleOnBackPressed();
        Assert.assertEquals(
                "Handler's callback should be executed if it is enabled",
                1,
                h1.getCallbackHelper().getCallCount());

        callbackHelper.waitForCallback(
                "Callback should be triggered when back button is pressed", 0);

        manager.getCallback().handleOnBackPressed();
        callbackHelper.waitForCallback("Callback should be called again", 1);
    }

    private int getHandlerCount(BackPressManager manager) {
        int count = 0;
        for (BackPressHandler handler : manager.getHandlersForTesting()) {
            if (handler != null) count++;
        }
        return count;
    }
}