chromium/chrome/browser/back_press/android/java/src/org/chromium/chrome/browser/back_press/BackPressManagerTest.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 androidx.activity.BackEventCompat;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.ObservableSupplierImpl;
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.HistogramWatcher;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;

import java.util.concurrent.TimeoutException;

/** Tests for {@link BackPressManager}. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class BackPressManagerTest {

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

        @Override
        public @BackPressResult int handleBackPress() {
            return BackPressResult.UNKNOWN;
        }

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

    private class FailedBackPressHandler extends EmptyBackPressHandler {
        @Override
        public @BackPressResult int handleBackPress() {
            return BackPressResult.FAILURE;
        }
    }

    @Test
    @SmallTest
    public void testBasic() {
        var histogramWatcher =
                HistogramWatcher.newBuilder().expectNoRecords(BackPressManager.HISTOGRAM).build();

        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(h1, BackPressHandler.Type.FIND_TOOLBAR);
                });

        triggerBackPressWithoutAssertionError(manager);

        histogramWatcher.assertExpected(
                "Handler's histogram should be not recorded if it is not executed");

        histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        BackPressManager.HISTOGRAM, 16); // 16 is FIND_TOOLBAR
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    h1.getHandleBackPressChangedSupplier().set(true);
                    manager.getCallback().handleOnBackPressed();
                });
        histogramWatcher.assertExpected("Handler's histogram should be recorded if it is executed");

        histogramWatcher =
                HistogramWatcher.newBuilder().expectNoRecords(BackPressManager.HISTOGRAM).build();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    h1.getHandleBackPressChangedSupplier().set(false);
                });
        triggerBackPressWithoutAssertionError(manager);
        histogramWatcher.assertExpected(
                "Handler's histogram should be not recorded if it is not executed");
    }

    @Test
    @SmallTest
    public void testMultipleHandlers() {
        var histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        BackPressManager.HISTOGRAM, 18); // 18 is XR_DELEGATE
        BackPressManager manager = new BackPressManager();
        EmptyBackPressHandler h1 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);
        EmptyBackPressHandler h2 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(h1, BackPressHandler.Type.TEXT_BUBBLE);
                    manager.addHandler(h2, BackPressHandler.Type.XR_DELEGATE);
                    h1.getHandleBackPressChangedSupplier().set(false);
                    h2.getHandleBackPressChangedSupplier().set(true);
                    manager.getCallback().handleOnBackPressed();
                });

        histogramWatcher.assertExpected(
                "Only record to handler's histogram should have value 18 (XR_DELEGATE).");

        histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        BackPressManager.HISTOGRAM,
                        BackPressManager.getHistogramValue(BackPressHandler.Type.TEXT_BUBBLE));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    h1.getHandleBackPressChangedSupplier().set(true);
                    manager.getCallback().handleOnBackPressed();
                });
        histogramWatcher.assertExpected(
                "Only record to handler's histogram should have value 0 (TEXT_BUBBLE).");
    }

    @Test
    @SmallTest
    public void testFailedHandlers() {
        BackPressManager manager = new BackPressManager();
        var textBubbleFailedHandler =
                ThreadUtils.runOnUiThreadBlocking(FailedBackPressHandler::new);
        var arSuccessHandler = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(textBubbleFailedHandler, BackPressHandler.Type.TEXT_BUBBLE);
                    manager.addHandler(arSuccessHandler, BackPressHandler.Type.XR_DELEGATE);
                    textBubbleFailedHandler.getHandleBackPressChangedSupplier().set(true);
                    arSuccessHandler.getHandleBackPressChangedSupplier().set(true);
                });

        var watcher =
                HistogramWatcher.newBuilder()
                        .expectIntRecord(
                                BackPressManager.FAILURE_HISTOGRAM,
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.TEXT_BUBBLE))
                        .expectIntRecord(
                                BackPressManager.HISTOGRAM,
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.XR_DELEGATE))
                        .build();
        triggerBackPressWithoutAssertionError(manager);
        watcher.assertExpected();
    }

    @Test
    @SmallTest
    public void testFallbackCallback() throws TimeoutException {
        CallbackHelper callbackHelper = new CallbackHelper();
        BackPressManager manager = new BackPressManager();
        manager.setFallbackOnBackPressed(callbackHelper::notifyCalled);
        var textBubbleFailedHandler =
                ThreadUtils.runOnUiThreadBlocking(FailedBackPressHandler::new);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(textBubbleFailedHandler, BackPressHandler.Type.TEXT_BUBBLE);
                    textBubbleFailedHandler.getHandleBackPressChangedSupplier().set(true);
                });

        var watcher =
                HistogramWatcher.newBuilder()
                        .expectIntRecord(
                                BackPressManager.FAILURE_HISTOGRAM,
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.TEXT_BUBBLE))
                        .build();
        triggerBackPressWithoutAssertionError(manager);
        callbackHelper.waitForOnly("Fallback should be triggered if all handlers failed.");
        watcher.assertExpected();
    }

    @Test
    @SmallTest
    public void testNoRecordWhenBackIsCancelled() {
        BackPressManager manager = new BackPressManager();

        EmptyBackPressHandler h1 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);

        var record =
                HistogramWatcher.newBuilder()
                        .expectNoRecords("Android.BackPress.SwipeEdge")
                        .expectNoRecords("Android.BackPress.Intercept")
                        .expectNoRecords("Android.BackPress.Intercept.LeftEdge")
                        .expectNoRecords("Android.BackPress.Intercept.RightEdge")
                        .expectNoRecords("Android.BackPress.SwipeEdge.TabHistoryNavigation")
                        .build();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(h1, BackPressHandler.Type.TAB_HISTORY);
                    h1.getHandleBackPressChangedSupplier().set(true);

                    var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT);
                    manager.getCallback().handleOnBackStarted(backEvent);
                    backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_LEFT);
                    manager.getCallback().handleOnBackProgressed(backEvent);

                    manager.getCallback().handleOnBackCancelled();
                });

        record.assertExpected("No record when back gesture is cancelled.");
    }

    @Test
    @SmallTest
    public void testRecordSwipeEdge() {
        BackPressManager manager = new BackPressManager();
        manager.setIsGestureNavEnabledSupplier(() -> true);

        EmptyBackPressHandler h1 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);
        EmptyBackPressHandler h2 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);

        var edgeRecords =
                HistogramWatcher.newBuilder()
                        .expectIntRecord("Android.BackPress.SwipeEdge", 0) // from left
                        .expectIntRecord(
                                "Android.BackPress.Intercept.LeftEdge",
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.XR_DELEGATE))
                        .expectNoRecords("Android.BackPress.Intercept.RightEdge")
                        .expectNoRecords("Android.BackPress.SwipeEdge.TabHistoryNavigation")
                        .build();
        // Trigger XR delegate back press handler from left side.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(h1, BackPressHandler.Type.TEXT_BUBBLE);
                    manager.addHandler(h2, BackPressHandler.Type.XR_DELEGATE);
                    h1.getHandleBackPressChangedSupplier().set(false);
                    h2.getHandleBackPressChangedSupplier().set(true);
                    Assert.assertTrue(manager.getCallback().isEnabled());

                    var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT);
                    manager.getCallback().handleOnBackStarted(backEvent);
                    backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_LEFT);
                    manager.getCallback().handleOnBackProgressed(backEvent);

                    manager.getCallback().handleOnBackPressed();
                });

        edgeRecords.assertExpected("Wrong histogram records for XR delegate.");

        var edgeRecords2 =
                HistogramWatcher.newBuilder()
                        .expectIntRecord("Android.BackPress.SwipeEdge", 1) // from right
                        .expectIntRecord(
                                "Android.BackPress.Intercept.RightEdge",
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.TEXT_BUBBLE))
                        .expectNoRecords("Android.BackPress.Intercept.LeftEdge")
                        .expectNoRecords("Android.BackPress.SwipeEdge.TabHistoryNavigation")
                        .build();
        // Trigger Text bubble delegate back press handler from right side.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    h1.getHandleBackPressChangedSupplier().set(true);
                    h2.getHandleBackPressChangedSupplier().set(true);

                    var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_RIGHT);
                    manager.getCallback().handleOnBackStarted(backEvent);
                    backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_RIGHT);
                    manager.getCallback().handleOnBackProgressed(backEvent);

                    manager.getCallback().handleOnBackPressed();
                });

        edgeRecords2.assertExpected("Wrong histogram records for Text Bubble delegate.");
    }

    @Test
    @SmallTest
    public void testRecordSwipeEdgeOfTabHistoryNavigation() {
        BackPressManager manager = new BackPressManager();
        manager.setIsGestureNavEnabledSupplier(() -> true);

        EmptyBackPressHandler h1 = ThreadUtils.runOnUiThreadBlocking(EmptyBackPressHandler::new);

        var edgeRecords =
                HistogramWatcher.newBuilder()
                        .expectIntRecord("Android.BackPress.SwipeEdge", 0) // from left
                        .expectIntRecord("Android.BackPress.SwipeEdge.TabHistoryNavigation", 0)
                        .expectIntRecord(
                                "Android.BackPress.Intercept.LeftEdge",
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.TAB_HISTORY))
                        .expectNoRecords("Android.BackPress.Intercept.RightEdge")
                        .build();
        // Trigger tab history navigation from left side.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    manager.addHandler(h1, BackPressHandler.Type.TAB_HISTORY);
                    h1.getHandleBackPressChangedSupplier().set(true);

                    var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_LEFT);
                    manager.getCallback().handleOnBackStarted(backEvent);
                    backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_LEFT);
                    manager.getCallback().handleOnBackProgressed(backEvent);

                    manager.getCallback().handleOnBackPressed();
                });

        edgeRecords.assertExpected("Wrong histogram records for tab history navigation.");

        var edgeRecords2 =
                HistogramWatcher.newBuilder()
                        .expectIntRecord("Android.BackPress.SwipeEdge", 1) // from right
                        .expectIntRecord("Android.BackPress.SwipeEdge.TabHistoryNavigation", 1)
                        .expectIntRecord(
                                "Android.BackPress.Intercept.RightEdge",
                                BackPressManager.getHistogramValue(
                                        BackPressHandler.Type.TAB_HISTORY))
                        .expectNoRecords("Android.BackPress.Intercept.LeftEdge")
                        .build();
        // Trigger tab history navigation from right side.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    var backEvent = new BackEventCompat(0, 0, 0, BackEventCompat.EDGE_RIGHT);
                    manager.getCallback().handleOnBackStarted(backEvent);
                    backEvent = new BackEventCompat(1, 0, .5f, BackEventCompat.EDGE_RIGHT);
                    manager.getCallback().handleOnBackProgressed(backEvent);

                    manager.getCallback().handleOnBackPressed();
                });

        edgeRecords2.assertExpected("Wrong histogram records for VR delegate.");
    }

    // Trigger back press ignoring built-in assertion errors.
    private void triggerBackPressWithoutAssertionError(BackPressManager manager) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    try {
                        manager.getCallback().handleOnBackPressed();
                    } catch (AssertionError ignored) {
                        String msg = ignored.getMessage();
                        if (msg == null) throw ignored;
                        if (msg.equals(
                                "Callback is enabled but no handler consumed back gesture.")) {
                            return;
                        } else if (msg.contains("didn't correctly handle back press; handled by")) {
                            return;
                        }
                        throw ignored;
                    }
                });
    }
}