chromium/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheetSwipeDetectorTest.java

// Copyright 2017 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.bottomsheet;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.view.MotionEvent;

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

import org.chromium.base.MathUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetSwipeDetector.SwipeableBottomSheet;

import java.util.ArrayList;
import java.util.List;

/** Unit tests for the {@link BottomSheetSwipeDetector} class. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public final class BottomSheetSwipeDetectorTest {
    /** The minimum height of the bottom sheet. */
    private static final float MIN_SHEET_OFFSET = 100;

    /** An arbitrary screen height. */
    private static final float SCREEN_HEIGHT = 1000;

    /** An instance of the mock swipable sheet. */
    private MockSwipeableBottomSheet mSwipeableBottomSheet;

    /** The swipe detector to process motion events. */
    private BottomSheetSwipeDetector mSwipeDetector;

    /** A mock implementation of a swipeable bottom sheet. */
    private static class MockSwipeableBottomSheet implements SwipeableBottomSheet {
        /** The minimum offset of the sheet. */
        private final float mMinOffset;

        /** The maximum offset of the sheet. */
        private final float mMaxOffset;

        /** Whether the content in the sheet is currently scrolled to the top. */
        public boolean isContentScrolledToTop;

        /** Whether the sheet should currently be animating. */
        public boolean shouldBeAnimating;

        /** The current offset of the bottom sheet. */
        private float mCurrentSheetOffset;

        public MockSwipeableBottomSheet(float minOffset, float maxOffset) {
            mMinOffset = minOffset;
            mMaxOffset = maxOffset;

            // The sheet should be initialized at the minimum state.
            mCurrentSheetOffset = mMinOffset;

            isContentScrolledToTop = true;
        }

        @Override
        public boolean isContentScrolledToTop() {
            return isContentScrolledToTop;
        }

        @Override
        public float getCurrentOffsetPx() {
            return mCurrentSheetOffset;
        }

        @Override
        public float getMinOffsetPx() {
            return mMinOffset;
        }

        @Override
        public float getMaxOffsetPx() {
            return mMaxOffset;
        }

        @Override
        public boolean isTouchEventInToolbar(MotionEvent event) {
            // This will be implementation specific in practice. This checks that the motion event
            // occured above the bottom of the toolbar.
            return event.getRawY() < (mMaxOffset - mCurrentSheetOffset) + mMinOffset;
        }

        @Override
        public boolean shouldGestureMoveSheet(
                MotionEvent initialDownEvent, MotionEvent currentEvent) {
            return true;
        }

        @Override
        public void setSheetOffset(float offset, boolean shouldAnimate) {
            mCurrentSheetOffset = offset;
            shouldBeAnimating = shouldAnimate;
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mSwipeableBottomSheet = new MockSwipeableBottomSheet(MIN_SHEET_OFFSET, SCREEN_HEIGHT);
        mSwipeDetector = new BottomSheetSwipeDetector(null, mSwipeableBottomSheet);
    }

    /**
     * Create a list of motion events simulating a scroll event stream from (x1, y1) to (x2, y2) and
     * apply it to the provided swipe detector.
     *
     * @param x1 The start x.
     * @param y1 The start y.
     * @param x2 The end x.
     * @param y2 The end y.
     * @param detector The detector to apply the swipe to.
     * @param endScroll Whether or not to include the up event at the end of the stream.
     * @return A list of motion events.
     */
    private static void performScroll(
            float x1,
            float y1,
            float x2,
            float y2,
            BottomSheetSwipeDetector detector,
            boolean endScroll) {
        int moveEventCount = 10;

        ArrayList<MotionEvent> eventStream = new ArrayList<>();
        float xInterval = (x2 - x1) / moveEventCount;
        float yInterval = (y2 - y1) / moveEventCount;
        eventStream.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x1, y1, 0));
        for (int i = 0; i < moveEventCount; i++) {
            eventStream.add(
                    MotionEvent.obtain(
                            0,
                            0,
                            MotionEvent.ACTION_MOVE,
                            x1 + ((i + 1) * xInterval),
                            y1 + ((i + 1) * yInterval),
                            0));
        }
        if (endScroll) eventStream.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x2, y2, 0));

        applyGestureStream(eventStream, detector);
    }

    /**
     * Apply a list of events to a swipe detector.
     *
     * @param stream The list of motion events to apply to the detector.
     * @param detector The detector to apply the swipe to.
     */
    private static void applyGestureStream(
            List<MotionEvent> stream, BottomSheetSwipeDetector detector) {
        for (MotionEvent e : stream) {
            if (!detector.isScrolling()) {
                detector.onInterceptTouchEvent(e);
            } else {
                detector.onTouchEvent(e);
            }
        }
    }

    /** Test that the sheet moves when scrolled up from min height. */
    @Test
    public void testScrollToolbarUp_minHeight() {
        assertEquals(
                "The sheet should be at the minimum state.",
                MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        // Scrolling up half the screen should put the sheet at half + the min offset.
        performScroll(0, SCREEN_HEIGHT, 0, halfScreenHeight, mSwipeDetector, true);

        assertEquals(
                "The sheet is not at the correct height.",
                halfScreenHeight + MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertTrue("The sheet should be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /** Test that the sheet is not told to animate mid-stream. */
    @Test
    public void testScrollToolbarUp_minHeight_noUpEvent() {
        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        performScroll(0, SCREEN_HEIGHT, 0, halfScreenHeight, mSwipeDetector, false);

        assertEquals(
                "The sheet is not at the correct height.",
                halfScreenHeight + MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertFalse(
                "The sheet should not be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /** Test that the sheet does not move when scrolled up from max height. */
    @Test
    public void testScrollToolbarUp_maxHeight() {
        // Init the sheet to be full height.
        mSwipeableBottomSheet.setSheetOffset(SCREEN_HEIGHT, false);

        assertEquals(
                "The sheet should be at the maximum state.",
                SCREEN_HEIGHT,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);

        performScroll(0, 0, 0, -500, mSwipeDetector, true);

        assertEquals(
                "The sheet should still be at the maximum state.",
                SCREEN_HEIGHT,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertFalse(
                "The sheet should not be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /** Test that the sheet does not move when scrolled down from min height. */
    @Test
    public void testScrollToolbarDown_minHeight() {
        assertEquals(
                "The sheet should be at the minimum state.",
                MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);

        performScroll(0, SCREEN_HEIGHT, 0, SCREEN_HEIGHT + 500, mSwipeDetector, true);

        assertEquals(
                "The sheet should still be at the minimum state.",
                MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertFalse(
                "The sheet should not be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /** Test that the sheet moves when scrolled down from max height. */
    @Test
    public void testScrollToolbarDown_maxHeight() {
        // Init the sheet to be full height.
        mSwipeableBottomSheet.setSheetOffset(SCREEN_HEIGHT, false);

        assertEquals(
                "The sheet should be at the maximum state.",
                SCREEN_HEIGHT,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        // Scrolling down half the screen should put the sheet at half height.
        performScroll(0, 0, 0, halfScreenHeight, mSwipeDetector, true);

        assertEquals(
                "The sheet is not at the correct height.",
                halfScreenHeight,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertTrue("The sheet should be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /**
     * Test that the sheet moves when scrolled down from max height while the content has been
     * scrolled.
     */
    @Test
    public void testScrollToolbarDown_maxHeight_contentScrolled() {
        // Init the sheet to be full height.
        mSwipeableBottomSheet.setSheetOffset(SCREEN_HEIGHT, false);

        assertEquals(
                "The sheet should be at the maximum state.",
                SCREEN_HEIGHT,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        // Scrolling down half the screen should put the sheet at half height, regardless of the
        // state of the content.
        performScroll(0, 0, 0, halfScreenHeight, mSwipeDetector, true);

        assertEquals(
                "The sheet is not at the correct height.",
                halfScreenHeight,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertTrue("The sheet should be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /** Test that the sheet does not move when a scroll is not sufficiently in the up direction. */
    @Test
    public void testScrollToolbarDiagonal_minHeight() {
        assertEquals(
                "The sheet should be at the minimum state.",
                MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        performScroll(
                0, halfScreenHeight, halfScreenHeight, halfScreenHeight, mSwipeDetector, true);

        assertEquals(
                "The sheet should still be at the minimum state.",
                MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
        assertFalse(
                "The sheet should not be set to animate.", mSwipeableBottomSheet.shouldBeAnimating);
    }

    /**
     * Test that the sheet does not move when the content is scrolled up and the sheet is at max
     * height.
     */
    @Test
    public void testScrollContent_maxHeight() {
        // Init the sheet to be full height.
        mSwipeableBottomSheet.setSheetOffset(SCREEN_HEIGHT, false);

        // Content is scrolled some amount.
        mSwipeableBottomSheet.isContentScrolledToTop = false;

        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        // Scroll down half the screen. The sheet should not move since the content is scrolled.
        performScroll(0, halfScreenHeight, 0, SCREEN_HEIGHT, mSwipeDetector, true);

        assertEquals(
                "The sheet should still be at the maximum state.",
                SCREEN_HEIGHT,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
    }

    /**
     * Test that the sheet moves when a scroll occurs on the body of the sheet. Content should only
     * scroll if the sheet is at max height.
     */
    @Test
    public void testScrollContent_halfHeight() {
        final float halfScreenHeight = SCREEN_HEIGHT / 2f;

        // Init the sheet to be half height.
        mSwipeableBottomSheet.setSheetOffset(halfScreenHeight, false);

        // Content is scrolled some amount.
        mSwipeableBottomSheet.isContentScrolledToTop = false;

        // Scroll down on the content, the sheet should move.
        performScroll(0, halfScreenHeight / 2f, 0, SCREEN_HEIGHT, mSwipeDetector, true);

        assertEquals(
                "The sheet should be at the minimum state.",
                MIN_SHEET_OFFSET,
                mSwipeableBottomSheet.getCurrentOffsetPx(),
                MathUtils.EPSILON);
    }
}