tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 06b4ab52c3eb5234cc74526070642d1a0a8b229a
parent 789d605bd69af414d0fa9eaba2315b6504629b84
Author: Mugurell <Mugurell@users.noreply.github.com>
Date:   Wed, 12 Nov 2025 08:07:06 +0000

Bug 1991654 - part 1 - Rename current scrolling behavior to EngineViewScrollingGesturesBehavior r=android-reviewers,skhan

The plan is to use the previous name - EngineViewScrollingBehavior for the supertype
of this and also of a new behavior using the scroll data for animating the toolbar.

Differential Revision: https://phabricator.services.mozilla.com/D267221

Diffstat:
Mmobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt | 10+++++-----
Mmobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt | 10+++++-----
Dmobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt | 225-------------------------------------------------------------------------------
Amobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehavior.kt | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt | 550-------------------------------------------------------------------------------
Amobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehaviorTest.kt | 550+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BottomToolbarContainerView.kt | 12++++++------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/FenixBrowserToolbarView.kt | 12++++++------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserHomeToolbarViewTest.kt | 6+++---
Mmobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt | 4++--
Mmobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt | 4++--
11 files changed, 804 insertions(+), 804 deletions(-)

diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt @@ -37,7 +37,7 @@ import mozilla.components.support.ktx.kotlin.trimmed import mozilla.components.ui.autocomplete.AutocompleteView import mozilla.components.ui.autocomplete.InlineAutocompleteEditText import mozilla.components.ui.autocomplete.OnFilterListener -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import kotlin.coroutines.CoroutineContext internal fun ImageView.setTintResource(@ColorRes tintColorResource: Int) { @@ -394,26 +394,26 @@ class BrowserToolbar @JvmOverloads constructor( override fun enableScrolling() { // Behavior can be changed without us knowing. Not safe to use a memoized value. (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.enableScrolling() + (behavior as? EngineViewScrollingGesturesBehavior)?.enableScrolling() } } override fun disableScrolling() { // Behavior can be changed without us knowing. Not safe to use a memoized value. (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.disableScrolling() + (behavior as? EngineViewScrollingGesturesBehavior)?.disableScrolling() } } override fun expand() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.forceExpand(this@BrowserToolbar) + (behavior as? EngineViewScrollingGesturesBehavior)?.forceExpand(this@BrowserToolbar) } } override fun collapse() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.forceCollapse(this@BrowserToolbar) + (behavior as? EngineViewScrollingGesturesBehavior)?.forceCollapse(this@BrowserToolbar) } } diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt @@ -29,7 +29,7 @@ import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import mozilla.components.ui.widgets.behavior.ViewPosition import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -924,7 +924,7 @@ class BrowserToolbarTest { fun `enable scrolling is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -939,7 +939,7 @@ class BrowserToolbarTest { fun `disable scrolling is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -954,7 +954,7 @@ class BrowserToolbarTest { fun `expand is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } @@ -969,7 +969,7 @@ class BrowserToolbarTest { fun `collapse is forwarded to the toolbar behavior`() { // Seems like real instances are needed for things to be set properly val toolbar = BrowserToolbar(testContext) - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) val params = CoordinatorLayout.LayoutParams(10, 10).apply { this.behavior = behavior } diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt @@ -1,225 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.ui.widgets.behavior - -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.View -import androidx.annotation.VisibleForTesting -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat -import mozilla.components.concept.base.crash.CrashReporting -import mozilla.components.concept.engine.EngineView -import mozilla.components.support.ktx.android.view.findViewInHierarchy - -/** - * Where the view is placed on the screen. - */ -enum class ViewPosition { - TOP, - BOTTOM, -} - -/** - * A [CoordinatorLayout.Behavior] implementation to be used when placing [View] at the bottom of the screen. - * - * This is safe to use even if the [View] may be added / removed from a parent layout later - * or if it could have Visibility.GONE set. - * - * This implementation will: - * - Show/Hide the [View] automatically when scrolling vertically. - * - Snap the [View] to be hidden or visible when the user stops scrolling. - */ -class EngineViewScrollingBehavior( - val context: Context?, - attrs: AttributeSet?, - private val viewPosition: ViewPosition, - private val crashReporting: CrashReporting? = null, -) : CoordinatorLayout.Behavior<View>(context, attrs) { - // This implementation is heavily based on this blog article: - // https://android.jlelse.eu/scroll-your-bottom-navigation-view-away-with-10-lines-of-code-346f1ed40e9e - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var shouldSnapAfterScroll: Boolean = false - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var startedScroll = false - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var isScrollEnabled = false - - /** - * Reference to [EngineView] used to check user's [android.view.MotionEvent]s. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var engineView: EngineView? = null - - /** - * Reference to the actual [View] that we'll animate. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var dynamicScrollView: View? = null - - /** - * Depending on how user's touch was consumed by EngineView / current website, - * - * we will animate the dynamic navigation bar if: - * - touches were used for zooming / panning operations in the website. - * - * We will do nothing if: - * - the website is not scrollable - * - the website handles the touch events itself through it's own touch event listeners. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal val shouldScroll: Boolean - get() = engineView?.getInputResultDetail()?.let { - (it.canScrollToBottom() || it.canScrollToTop()) && isScrollEnabled - } ?: false - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var gesturesDetector: BrowserGestureDetector = createGestureDetector() - - @VisibleForTesting - internal var yTranslator: ViewYTranslator = createYTranslationStrategy() - - private fun createYTranslationStrategy() = ViewYTranslator(viewPosition) - - override fun onStartNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: View, - directTargetChild: View, - target: View, - axes: Int, - type: Int, - ): Boolean { - return if (dynamicScrollView != null) { - startNestedScroll(axes, type) - } else { - return false // not interested in subsequent scroll events - } - } - - override fun onStopNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: View, - target: View, - type: Int, - ) { - if (dynamicScrollView != null) { - stopNestedScroll(type, child) - } - } - - override fun onInterceptTouchEvent( - parent: CoordinatorLayout, - child: View, - ev: MotionEvent, - ): Boolean { - if (dynamicScrollView != null) { - gesturesDetector.handleTouchEvent(ev) - } - return false // allow events to be passed to below listeners - } - - override fun onLayoutChild( - parent: CoordinatorLayout, - child: View, - layoutDirection: Int, - ): Boolean { - dynamicScrollView = child - engineView = parent.findViewInHierarchy { it is EngineView } as? EngineView - - return super.onLayoutChild(parent, child, layoutDirection) - } - - /** - * Used to expand the [View] - */ - fun forceExpand(view: View) { - yTranslator.expandWithAnimation(view) - } - - /** - * Used to collapse the [View] - */ - fun forceCollapse(view: View) { - yTranslator.collapseWithAnimation(view) - } - - /** - * Allow this view to be animated. - * - * @see disableScrolling - */ - fun enableScrolling() { - isScrollEnabled = true - } - - /** - * Disable scrolling of the view irrespective of the intrinsic checks. - * - * @see enableScrolling - */ - fun disableScrolling() { - isScrollEnabled = false - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun tryToScrollVertically(distance: Float) { - dynamicScrollView?.let { view -> - if (shouldScroll && startedScroll) { - yTranslator.translate(view, distance) - } - } - } - - /** - * Helper function to ease testing. - * (Re)Initializes the [BrowserGestureDetector] in a new context. - * - * Useful in spied behaviors, to ensure callbacks are of the spy and not of the initially created object - * if the passed in argument is the result of [createGestureDetector]. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun initGesturesDetector(detector: BrowserGestureDetector) { - gesturesDetector = detector - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun createGestureDetector() = - BrowserGestureDetector( - context!!, - BrowserGestureDetector.GesturesListener( - onVerticalScroll = ::tryToScrollVertically, - onScaleBegin = { - // Scale shouldn't animate the view but a small y translation is still possible - // because of a previous scroll. Try to be swift about such an in progress animation. - yTranslator.snapImmediately(dynamicScrollView) - }, - ), - crashReporting = crashReporting, - ) - - @VisibleForTesting - internal fun startNestedScroll(axes: Int, type: Int): Boolean { - return if (shouldScroll && axes == ViewCompat.SCROLL_AXIS_VERTICAL) { - startedScroll = true - shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH - yTranslator.cancelInProgressTranslation() - true - } else { - false - } - } - - @VisibleForTesting - internal fun stopNestedScroll(type: Int, view: View) { - startedScroll = false - if (shouldSnapAfterScroll || type == ViewCompat.TYPE_NON_TOUCH) { - yTranslator.snapWithAnimation(view) - } - } -} diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehavior.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehavior.kt @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.ui.widgets.behavior + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import mozilla.components.concept.base.crash.CrashReporting +import mozilla.components.concept.engine.EngineView +import mozilla.components.support.ktx.android.view.findViewInHierarchy + +/** + * Where the view is placed on the screen. + */ +enum class ViewPosition { + TOP, + BOTTOM, +} + +/** + * A [CoordinatorLayout.Behavior] implementation to be used when placing [View] at the bottom of the screen. + * + * This is safe to use even if the [View] may be added / removed from a parent layout later + * or if it could have Visibility.GONE set. + * + * This implementation will: + * - Show/Hide the [View] automatically when scrolling vertically. + * - Snap the [View] to be hidden or visible when the user stops scrolling. + */ +class EngineViewScrollingGesturesBehavior( + val context: Context?, + attrs: AttributeSet?, + private val viewPosition: ViewPosition, + private val crashReporting: CrashReporting? = null, +) : CoordinatorLayout.Behavior<View>(context, attrs) { + // This implementation is heavily based on this blog article: + // https://android.jlelse.eu/scroll-your-bottom-navigation-view-away-with-10-lines-of-code-346f1ed40e9e + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var shouldSnapAfterScroll: Boolean = false + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var startedScroll = false + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var isScrollEnabled = false + + /** + * Reference to [EngineView] used to check user's [android.view.MotionEvent]s. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var engineView: EngineView? = null + + /** + * Reference to the actual [View] that we'll animate. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var dynamicScrollView: View? = null + + /** + * Depending on how user's touch was consumed by EngineView / current website, + * + * we will animate the dynamic navigation bar if: + * - touches were used for zooming / panning operations in the website. + * + * We will do nothing if: + * - the website is not scrollable + * - the website handles the touch events itself through it's own touch event listeners. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val shouldScroll: Boolean + get() = engineView?.getInputResultDetail()?.let { + (it.canScrollToBottom() || it.canScrollToTop()) && isScrollEnabled + } ?: false + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var gesturesDetector: BrowserGestureDetector = createGestureDetector() + + @VisibleForTesting + internal var yTranslator: ViewYTranslator = createYTranslationStrategy() + + private fun createYTranslationStrategy() = ViewYTranslator(viewPosition) + + override fun onStartNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: View, + directTargetChild: View, + target: View, + axes: Int, + type: Int, + ): Boolean { + return if (dynamicScrollView != null) { + startNestedScroll(axes, type) + } else { + return false // not interested in subsequent scroll events + } + } + + override fun onStopNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: View, + target: View, + type: Int, + ) { + if (dynamicScrollView != null) { + stopNestedScroll(type, child) + } + } + + override fun onInterceptTouchEvent( + parent: CoordinatorLayout, + child: View, + ev: MotionEvent, + ): Boolean { + if (dynamicScrollView != null) { + gesturesDetector.handleTouchEvent(ev) + } + return false // allow events to be passed to below listeners + } + + override fun onLayoutChild( + parent: CoordinatorLayout, + child: View, + layoutDirection: Int, + ): Boolean { + dynamicScrollView = child + engineView = parent.findViewInHierarchy { it is EngineView } as? EngineView + + return super.onLayoutChild(parent, child, layoutDirection) + } + + /** + * Used to expand the [View] + */ + fun forceExpand(view: View) { + yTranslator.expandWithAnimation(view) + } + + /** + * Used to collapse the [View] + */ + fun forceCollapse(view: View) { + yTranslator.collapseWithAnimation(view) + } + + /** + * Allow this view to be animated. + * + * @see disableScrolling + */ + fun enableScrolling() { + isScrollEnabled = true + } + + /** + * Disable scrolling of the view irrespective of the intrinsic checks. + * + * @see enableScrolling + */ + fun disableScrolling() { + isScrollEnabled = false + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun tryToScrollVertically(distance: Float) { + dynamicScrollView?.let { view -> + if (shouldScroll && startedScroll) { + yTranslator.translate(view, distance) + } + } + } + + /** + * Helper function to ease testing. + * (Re)Initializes the [BrowserGestureDetector] in a new context. + * + * Useful in spied behaviors, to ensure callbacks are of the spy and not of the initially created object + * if the passed in argument is the result of [createGestureDetector]. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun initGesturesDetector(detector: BrowserGestureDetector) { + gesturesDetector = detector + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun createGestureDetector() = + BrowserGestureDetector( + context!!, + BrowserGestureDetector.GesturesListener( + onVerticalScroll = ::tryToScrollVertically, + onScaleBegin = { + // Scale shouldn't animate the view but a small y translation is still possible + // because of a previous scroll. Try to be swift about such an in progress animation. + yTranslator.snapImmediately(dynamicScrollView) + }, + ), + crashReporting = crashReporting, + ) + + @VisibleForTesting + internal fun startNestedScroll(axes: Int, type: Int): Boolean { + return if (shouldScroll && axes == ViewCompat.SCROLL_AXIS_VERTICAL) { + startedScroll = true + shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH + yTranslator.cancelInProgressTranslation() + true + } else { + false + } + } + + @VisibleForTesting + internal fun stopNestedScroll(type: Int, view: View) { + startedScroll = false + if (shouldSnapAfterScroll || type == ViewCompat.TYPE_NON_TOUCH) { + yTranslator.snapWithAnimation(view) + } + } +} diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt @@ -1,550 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.ui.widgets.behavior - -import android.content.Context -import android.graphics.Bitmap -import android.view.MotionEvent.ACTION_DOWN -import android.view.MotionEvent.ACTION_MOVE -import android.view.View -import android.widget.FrameLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.flow.flowOf -import mozilla.components.concept.engine.EngineSession -import mozilla.components.concept.engine.EngineView -import mozilla.components.concept.engine.InputResultDetail -import mozilla.components.concept.engine.selection.SelectionActionDelegate -import mozilla.components.support.test.any -import mozilla.components.support.test.mock -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyFloat -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` - -@RunWith(AndroidJUnit4::class) -class EngineViewScrollingBehaviorTest { - @Test - fun `onStartNestedScroll should attempt scrolling only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - doReturn(true).`when`(behavior).shouldScroll - - behavior.dynamicScrollView = null - var acceptsNestedScroll = behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = mock(), - directTargetChild = mock(), - target = mock(), - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - assertFalse(acceptsNestedScroll) - verify(behavior, never()).startNestedScroll(anyInt(), anyInt()) - - behavior.dynamicScrollView = mock() - acceptsNestedScroll = behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = mock(), - directTargetChild = mock(), - target = mock(), - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - assertTrue(acceptsNestedScroll) - verify(behavior).startNestedScroll(anyInt(), anyInt()) - } - - @Test - fun `startNestedScroll should cancel an ongoing snap animation`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - doReturn(true).`when`(behavior).shouldScroll - - val acceptsNestedScroll = behavior.startNestedScroll( - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - - assertTrue(acceptsNestedScroll) - verify(yTranslator).cancelInProgressTranslation() - } - - @Test - fun `startNestedScroll should not accept nested scrolls on the horizontal axis`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - doReturn(true).`when`(behavior).shouldScroll - - var acceptsNestedScroll = behavior.startNestedScroll( - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - assertTrue(acceptsNestedScroll) - - acceptsNestedScroll = behavior.startNestedScroll( - axes = ViewCompat.SCROLL_AXIS_HORIZONTAL, - type = ViewCompat.TYPE_TOUCH, - ) - assertFalse(acceptsNestedScroll) - } - - @Test - fun `GIVEN a gesture that doesn't scroll the toolbar WHEN startNestedScroll THEN nested scroll is not accepted`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val engineView: EngineView = mock() - val inputResultDetail: InputResultDetail = mock() - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - doReturn(false).`when`(behavior).shouldScroll - doReturn(true).`when`(inputResultDetail).isTouchUnhandled() - behavior.engineView = engineView - doReturn(inputResultDetail).`when`(engineView).getInputResultDetail() - - val acceptsNestedScroll = behavior.startNestedScroll( - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - - assertFalse(acceptsNestedScroll) - } - - @Test - fun `Behavior should not accept nested scrolls on the horizontal axis`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - behavior.dynamicScrollView = mock() - doReturn(true).`when`(behavior).shouldScroll - - var acceptsNestedScroll = behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = mock(), - directTargetChild = mock(), - target = mock(), - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - assertTrue(acceptsNestedScroll) - - acceptsNestedScroll = behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = mock(), - directTargetChild = mock(), - target = mock(), - axes = ViewCompat.SCROLL_AXIS_HORIZONTAL, - type = ViewCompat.TYPE_TOUCH, - ) - assertFalse(acceptsNestedScroll) - } - - @Test - fun `Behavior should delegate the onStartNestedScroll logic`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val view: View = mock() - behavior.dynamicScrollView = view - val inputType = ViewCompat.TYPE_TOUCH - val axes = ViewCompat.SCROLL_AXIS_VERTICAL - - behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = view, - directTargetChild = mock(), - target = mock(), - axes = axes, - type = inputType, - ) - - verify(behavior).startNestedScroll(axes, inputType) - } - - @Test - fun `onStopNestedScroll should attempt stopping nested scrolling only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - - behavior.dynamicScrollView = null - behavior.onStopNestedScroll( - coordinatorLayout = mock(), - child = mock(), - target = mock(), - type = 0, - ) - verify(behavior, never()).stopNestedScroll(anyInt(), any()) - - behavior.dynamicScrollView = mock() - behavior.onStopNestedScroll( - coordinatorLayout = mock(), - child = mock(), - target = mock(), - type = 0, - ) - verify(behavior).stopNestedScroll(anyInt(), any()) - } - - @Test - fun `Behavior should delegate the onStopNestedScroll logic`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val inputType = ViewCompat.TYPE_TOUCH - val view: View = mock() - - behavior.dynamicScrollView = null - behavior.onStopNestedScroll( - coordinatorLayout = mock(), - child = view, - target = mock(), - type = inputType, - ) - verify(behavior, never()).stopNestedScroll(inputType, view) - } - - @Test - fun `stopNestedScroll will snap toolbar up if toolbar is more than 50 percent visible`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - behavior.dynamicScrollView = mock() - doReturn(true).`when`(behavior).shouldScroll - - val child = mock<View>() - doReturn(100).`when`(child).height - doReturn(10f).`when`(child).translationY - - behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = child, - directTargetChild = mock(), - target = mock(), - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - - assertTrue(behavior.shouldSnapAfterScroll) - verify(yTranslator).cancelInProgressTranslation() - verify(yTranslator, never()).expandWithAnimation(any()) - verify(yTranslator, never()).collapseWithAnimation(any()) - - behavior.stopNestedScroll(0, child) - - verify(yTranslator).snapWithAnimation(child) - } - - @Test - fun `stopNestedScroll will snap toolbar down if toolbar is less than 50 percent visible`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - doReturn(true).`when`(behavior).shouldScroll - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - - val child = mock<View>() - behavior.dynamicScrollView = child - doReturn(100).`when`(child).height - doReturn(90f).`when`(child).translationY - - behavior.onStartNestedScroll( - coordinatorLayout = mock(), - child = child, - directTargetChild = mock(), - target = mock(), - axes = ViewCompat.SCROLL_AXIS_VERTICAL, - type = ViewCompat.TYPE_TOUCH, - ) - - assertTrue(behavior.shouldSnapAfterScroll) - verify(yTranslator).cancelInProgressTranslation() - verify(yTranslator, never()).expandWithAnimation(any()) - verify(yTranslator, never()).collapseWithAnimation(any()) - - behavior.stopNestedScroll(0, child) - - verify(yTranslator).snapWithAnimation(child) - } - - @Test - fun `onStopNestedScroll should snap the toolbar only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - behavior.dynamicScrollView = null - - behavior.onStopNestedScroll( - coordinatorLayout = mock(), - child = mock(), - target = mock(), - type = ViewCompat.TYPE_TOUCH, - ) - - verify(behavior, never()).stopNestedScroll(anyInt(), any()) - } - - @Test - fun `Behavior will intercept MotionEvents and pass them to the custom gesture detector`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val gestureDetector: BrowserGestureDetector = mock() - behavior.initGesturesDetector(gestureDetector) - behavior.dynamicScrollView = mock() - val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) - - behavior.onInterceptTouchEvent(mock(), mock(), downEvent) - - verify(gestureDetector).handleTouchEvent(downEvent) - } - - @Test - fun `Behavior should only dispatch MotionEvents to the gesture detector only if browserToolbar is valid`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val gestureDetector: BrowserGestureDetector = mock() - behavior.initGesturesDetector(gestureDetector) - val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) - - behavior.onInterceptTouchEvent(mock(), mock(), downEvent) - - verify(gestureDetector, never()).handleTouchEvent(downEvent) - } - - @Test - fun `Behavior will apply translation to toolbar only for vertical scrolls`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - behavior.initGesturesDetector(behavior.createGestureDetector()) - val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child - val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f) - val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 100f, downEvent) - - behavior.onInterceptTouchEvent(mock(), mock(), downEvent) - behavior.onInterceptTouchEvent(mock(), mock(), moveEvent) - - verify(behavior).tryToScrollVertically(-100f) - } - - @Test - fun `GIVEN a null InputResultDetail from the EngineView WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = null - assertFalse(behavior.shouldScroll) - behavior.engineView = mock() - `when`(behavior.engineView!!.getInputResultDetail()).thenReturn(null) - - assertFalse(behavior.shouldScroll) - } - - @Test - fun `GIVEN an InputResultDetail with the right values and scroll enabled WHEN shouldScroll is called THEN it returns true`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - val engineView: EngineView = mock() - behavior.engineView = engineView - behavior.isScrollEnabled = true - val validInputResultDetail: InputResultDetail = mock() - doReturn(validInputResultDetail).`when`(engineView).getInputResultDetail() - - doReturn(true).`when`(validInputResultDetail).canScrollToBottom() - doReturn(false).`when`(validInputResultDetail).canScrollToTop() - assertTrue(behavior.shouldScroll) - - doReturn(false).`when`(validInputResultDetail).canScrollToBottom() - doReturn(true).`when`(validInputResultDetail).canScrollToTop() - assertTrue(behavior.shouldScroll) - - doReturn(true).`when`(validInputResultDetail).canScrollToBottom() - doReturn(true).`when`(validInputResultDetail).canScrollToTop() - assertTrue(behavior.shouldScroll) - } - - @Test - fun `GIVEN an InputResultDetail with the right values but with scroll disabled WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = mock() - behavior.isScrollEnabled = false - val validInputResultDetail: InputResultDetail = mock() - doReturn(true).`when`(validInputResultDetail).canScrollToBottom() - doReturn(true).`when`(validInputResultDetail).canScrollToTop() - - assertFalse(behavior.shouldScroll) - } - - @Test - fun `GIVEN scroll enabled but EngineView cannot scroll to bottom WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = mock() - behavior.isScrollEnabled = true - val validInputResultDetail: InputResultDetail = mock() - doReturn(false).`when`(validInputResultDetail).canScrollToBottom() - doReturn(true).`when`(validInputResultDetail).canScrollToTop() - - assertFalse(behavior.shouldScroll) - } - - @Test - fun `GIVEN scroll enabled but EngineView cannot scroll to top WHEN shouldScroll is called THEN it returns false`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.engineView = mock() - behavior.isScrollEnabled = true - val validInputResultDetail: InputResultDetail = mock() - doReturn(true).`when`(validInputResultDetail).canScrollToBottom() - doReturn(false).`when`(validInputResultDetail).canScrollToTop() - - assertFalse(behavior.shouldScroll) - } - - @Test - fun `Behavior will vertically scroll nested scroll started and EngineView handled the event`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - doReturn(true).`when`(behavior).shouldScroll - val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child - doReturn(100).`when`(child).height - doReturn(0f).`when`(child).translationY - behavior.startedScroll = true - - behavior.tryToScrollVertically(25f) - - verify(yTranslator).translate(child, 25f) - } - - @Test - fun `Behavior will not scroll vertically if startedScroll is false`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - doReturn(true).`when`(behavior).shouldScroll - val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child - doReturn(100).`when`(child).height - doReturn(0f).`when`(child).translationY - behavior.startedScroll = false - - behavior.tryToScrollVertically(25f) - - verify(yTranslator, never()).translate(any(), anyFloat()) - } - - @Test - fun `Behavior will not scroll vertically if EngineView did not handled the event`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - doReturn(false).`when`(behavior).shouldScroll - val child = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = child - doReturn(100).`when`(child).height - doReturn(0f).`when`(child).translationY - - behavior.tryToScrollVertically(25f) - - verify(yTranslator, never()).translate(any(), anyFloat()) - } - - @Test - fun `forceExpand should delegate the translator`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - val view: View = mock() - - behavior.forceExpand(view) - - verify(yTranslator).expandWithAnimation(view) - } - - @Test - fun `forceCollapse should delegate the translator`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - val view: View = mock() - - behavior.forceCollapse(view) - - verify(yTranslator).collapseWithAnimation(view) - } - - @Test - fun `Behavior will not forceExpand when scrolling up and !shouldScroll if the touch was not yet handled in the browser`() { - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - val yTranslator: ViewYTranslator = mock() - behavior.yTranslator = yTranslator - behavior.initGesturesDetector(behavior.createGestureDetector()) - val view: View = spy(View(testContext, null, 0)) - behavior.dynamicScrollView = view - val engineView: EngineView = mock() - behavior.engineView = engineView - val handledTouchInput = InputResultDetail.newInstance() - doReturn(handledTouchInput).`when`(engineView).getInputResultDetail() - - doReturn(100).`when`(view).height - doReturn(100f).`when`(view).translationY - - val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f) - val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 30f, downEvent) - - behavior.onInterceptTouchEvent(mock(), mock(), downEvent) - behavior.onInterceptTouchEvent(mock(), mock(), moveEvent) - - verify(behavior).tryToScrollVertically(-30f) - verify(yTranslator, never()).forceExpandIfNotAlready(view, -30f) - } - - @Test - fun `onLayoutChild initializes browserToolbar and engineView`() { - val view = View(testContext) - val engineView = createDummyEngineView(testContext).asView() - val container = CoordinatorLayout(testContext).apply { - addView(View(testContext)) - addView(engineView) - } - val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)) - - behavior.onLayoutChild(container, view, View.LAYOUT_DIRECTION_LTR) - - assertEquals(view, behavior.dynamicScrollView) - assertEquals(engineView, behavior.engineView) - } - - @Test - fun `enableScrolling sets isScrollEnabled to true`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - - assertFalse(behavior.isScrollEnabled) - behavior.enableScrolling() - - assertTrue(behavior.isScrollEnabled) - } - - @Test - fun `disableScrolling sets isScrollEnabled to false`() { - val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM) - behavior.isScrollEnabled = true - - assertTrue(behavior.isScrollEnabled) - behavior.disableScrolling() - - assertFalse(behavior.isScrollEnabled) - } - - private fun createDummyEngineView(context: Context): EngineView = DummyEngineView(context) - - open class DummyEngineView(context: Context) : FrameLayout(context), EngineView { - override val verticalScrollPosition = flowOf(0f) - override val verticalScrollDelta = flowOf(0f) - override fun setVerticalClipping(clippingHeight: Int) {} - override fun setDynamicToolbarMaxHeight(height: Int) {} - override fun setActivityContext(context: Context?) {} - override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit - override fun render(session: EngineSession) {} - override fun release() {} - override var selectionActionDelegate: SelectionActionDelegate? = null - override fun addWindowInsetsListener( - key: String, - listener: androidx.core.view.OnApplyWindowInsetsListener?, - ) {} - override fun removeWindowInsetsListener(key: String) {} - } -} diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehaviorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingGesturesBehaviorTest.kt @@ -0,0 +1,550 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.ui.widgets.behavior + +import android.content.Context +import android.graphics.Bitmap +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_MOVE +import android.view.View +import android.widget.FrameLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.flow.flowOf +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineView +import mozilla.components.concept.engine.InputResultDetail +import mozilla.components.concept.engine.selection.SelectionActionDelegate +import mozilla.components.support.test.any +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class EngineViewScrollingGesturesBehaviorTest { + @Test + fun `onStartNestedScroll should attempt scrolling only if browserToolbar is valid`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + doReturn(true).`when`(behavior).shouldScroll + + behavior.dynamicScrollView = null + var acceptsNestedScroll = behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = mock(), + directTargetChild = mock(), + target = mock(), + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + assertFalse(acceptsNestedScroll) + verify(behavior, never()).startNestedScroll(anyInt(), anyInt()) + + behavior.dynamicScrollView = mock() + acceptsNestedScroll = behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = mock(), + directTargetChild = mock(), + target = mock(), + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + assertTrue(acceptsNestedScroll) + verify(behavior).startNestedScroll(anyInt(), anyInt()) + } + + @Test + fun `startNestedScroll should cancel an ongoing snap animation`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + doReturn(true).`when`(behavior).shouldScroll + + val acceptsNestedScroll = behavior.startNestedScroll( + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + + assertTrue(acceptsNestedScroll) + verify(yTranslator).cancelInProgressTranslation() + } + + @Test + fun `startNestedScroll should not accept nested scrolls on the horizontal axis`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + doReturn(true).`when`(behavior).shouldScroll + + var acceptsNestedScroll = behavior.startNestedScroll( + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + assertTrue(acceptsNestedScroll) + + acceptsNestedScroll = behavior.startNestedScroll( + axes = ViewCompat.SCROLL_AXIS_HORIZONTAL, + type = ViewCompat.TYPE_TOUCH, + ) + assertFalse(acceptsNestedScroll) + } + + @Test + fun `GIVEN a gesture that doesn't scroll the toolbar WHEN startNestedScroll THEN nested scroll is not accepted`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val engineView: EngineView = mock() + val inputResultDetail: InputResultDetail = mock() + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + doReturn(false).`when`(behavior).shouldScroll + doReturn(true).`when`(inputResultDetail).isTouchUnhandled() + behavior.engineView = engineView + doReturn(inputResultDetail).`when`(engineView).getInputResultDetail() + + val acceptsNestedScroll = behavior.startNestedScroll( + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + + assertFalse(acceptsNestedScroll) + } + + @Test + fun `Behavior should not accept nested scrolls on the horizontal axis`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + behavior.dynamicScrollView = mock() + doReturn(true).`when`(behavior).shouldScroll + + var acceptsNestedScroll = behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = mock(), + directTargetChild = mock(), + target = mock(), + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + assertTrue(acceptsNestedScroll) + + acceptsNestedScroll = behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = mock(), + directTargetChild = mock(), + target = mock(), + axes = ViewCompat.SCROLL_AXIS_HORIZONTAL, + type = ViewCompat.TYPE_TOUCH, + ) + assertFalse(acceptsNestedScroll) + } + + @Test + fun `Behavior should delegate the onStartNestedScroll logic`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val view: View = mock() + behavior.dynamicScrollView = view + val inputType = ViewCompat.TYPE_TOUCH + val axes = ViewCompat.SCROLL_AXIS_VERTICAL + + behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = view, + directTargetChild = mock(), + target = mock(), + axes = axes, + type = inputType, + ) + + verify(behavior).startNestedScroll(axes, inputType) + } + + @Test + fun `onStopNestedScroll should attempt stopping nested scrolling only if browserToolbar is valid`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + + behavior.dynamicScrollView = null + behavior.onStopNestedScroll( + coordinatorLayout = mock(), + child = mock(), + target = mock(), + type = 0, + ) + verify(behavior, never()).stopNestedScroll(anyInt(), any()) + + behavior.dynamicScrollView = mock() + behavior.onStopNestedScroll( + coordinatorLayout = mock(), + child = mock(), + target = mock(), + type = 0, + ) + verify(behavior).stopNestedScroll(anyInt(), any()) + } + + @Test + fun `Behavior should delegate the onStopNestedScroll logic`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val inputType = ViewCompat.TYPE_TOUCH + val view: View = mock() + + behavior.dynamicScrollView = null + behavior.onStopNestedScroll( + coordinatorLayout = mock(), + child = view, + target = mock(), + type = inputType, + ) + verify(behavior, never()).stopNestedScroll(inputType, view) + } + + @Test + fun `stopNestedScroll will snap toolbar up if toolbar is more than 50 percent visible`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + behavior.dynamicScrollView = mock() + doReturn(true).`when`(behavior).shouldScroll + + val child = mock<View>() + doReturn(100).`when`(child).height + doReturn(10f).`when`(child).translationY + + behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = child, + directTargetChild = mock(), + target = mock(), + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + + assertTrue(behavior.shouldSnapAfterScroll) + verify(yTranslator).cancelInProgressTranslation() + verify(yTranslator, never()).expandWithAnimation(any()) + verify(yTranslator, never()).collapseWithAnimation(any()) + + behavior.stopNestedScroll(0, child) + + verify(yTranslator).snapWithAnimation(child) + } + + @Test + fun `stopNestedScroll will snap toolbar down if toolbar is less than 50 percent visible`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + doReturn(true).`when`(behavior).shouldScroll + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + + val child = mock<View>() + behavior.dynamicScrollView = child + doReturn(100).`when`(child).height + doReturn(90f).`when`(child).translationY + + behavior.onStartNestedScroll( + coordinatorLayout = mock(), + child = child, + directTargetChild = mock(), + target = mock(), + axes = ViewCompat.SCROLL_AXIS_VERTICAL, + type = ViewCompat.TYPE_TOUCH, + ) + + assertTrue(behavior.shouldSnapAfterScroll) + verify(yTranslator).cancelInProgressTranslation() + verify(yTranslator, never()).expandWithAnimation(any()) + verify(yTranslator, never()).collapseWithAnimation(any()) + + behavior.stopNestedScroll(0, child) + + verify(yTranslator).snapWithAnimation(child) + } + + @Test + fun `onStopNestedScroll should snap the toolbar only if browserToolbar is valid`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + behavior.dynamicScrollView = null + + behavior.onStopNestedScroll( + coordinatorLayout = mock(), + child = mock(), + target = mock(), + type = ViewCompat.TYPE_TOUCH, + ) + + verify(behavior, never()).stopNestedScroll(anyInt(), any()) + } + + @Test + fun `Behavior will intercept MotionEvents and pass them to the custom gesture detector`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val gestureDetector: BrowserGestureDetector = mock() + behavior.initGesturesDetector(gestureDetector) + behavior.dynamicScrollView = mock() + val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) + + behavior.onInterceptTouchEvent(mock(), mock(), downEvent) + + verify(gestureDetector).handleTouchEvent(downEvent) + } + + @Test + fun `Behavior should only dispatch MotionEvents to the gesture detector only if browserToolbar is valid`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val gestureDetector: BrowserGestureDetector = mock() + behavior.initGesturesDetector(gestureDetector) + val downEvent = TestUtils.getMotionEvent(ACTION_DOWN) + + behavior.onInterceptTouchEvent(mock(), mock(), downEvent) + + verify(gestureDetector, never()).handleTouchEvent(downEvent) + } + + @Test + fun `Behavior will apply translation to toolbar only for vertical scrolls`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + behavior.initGesturesDetector(behavior.createGestureDetector()) + val child = spy(View(testContext, null, 0)) + behavior.dynamicScrollView = child + val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f) + val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 100f, downEvent) + + behavior.onInterceptTouchEvent(mock(), mock(), downEvent) + behavior.onInterceptTouchEvent(mock(), mock(), moveEvent) + + verify(behavior).tryToScrollVertically(-100f) + } + + @Test + fun `GIVEN a null InputResultDetail from the EngineView WHEN shouldScroll is called THEN it returns false`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + behavior.engineView = null + assertFalse(behavior.shouldScroll) + behavior.engineView = mock() + `when`(behavior.engineView!!.getInputResultDetail()).thenReturn(null) + + assertFalse(behavior.shouldScroll) + } + + @Test + fun `GIVEN an InputResultDetail with the right values and scroll enabled WHEN shouldScroll is called THEN it returns true`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + val engineView: EngineView = mock() + behavior.engineView = engineView + behavior.isScrollEnabled = true + val validInputResultDetail: InputResultDetail = mock() + doReturn(validInputResultDetail).`when`(engineView).getInputResultDetail() + + doReturn(true).`when`(validInputResultDetail).canScrollToBottom() + doReturn(false).`when`(validInputResultDetail).canScrollToTop() + assertTrue(behavior.shouldScroll) + + doReturn(false).`when`(validInputResultDetail).canScrollToBottom() + doReturn(true).`when`(validInputResultDetail).canScrollToTop() + assertTrue(behavior.shouldScroll) + + doReturn(true).`when`(validInputResultDetail).canScrollToBottom() + doReturn(true).`when`(validInputResultDetail).canScrollToTop() + assertTrue(behavior.shouldScroll) + } + + @Test + fun `GIVEN an InputResultDetail with the right values but with scroll disabled WHEN shouldScroll is called THEN it returns false`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + behavior.engineView = mock() + behavior.isScrollEnabled = false + val validInputResultDetail: InputResultDetail = mock() + doReturn(true).`when`(validInputResultDetail).canScrollToBottom() + doReturn(true).`when`(validInputResultDetail).canScrollToTop() + + assertFalse(behavior.shouldScroll) + } + + @Test + fun `GIVEN scroll enabled but EngineView cannot scroll to bottom WHEN shouldScroll is called THEN it returns false`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + behavior.engineView = mock() + behavior.isScrollEnabled = true + val validInputResultDetail: InputResultDetail = mock() + doReturn(false).`when`(validInputResultDetail).canScrollToBottom() + doReturn(true).`when`(validInputResultDetail).canScrollToTop() + + assertFalse(behavior.shouldScroll) + } + + @Test + fun `GIVEN scroll enabled but EngineView cannot scroll to top WHEN shouldScroll is called THEN it returns false`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + behavior.engineView = mock() + behavior.isScrollEnabled = true + val validInputResultDetail: InputResultDetail = mock() + doReturn(true).`when`(validInputResultDetail).canScrollToBottom() + doReturn(false).`when`(validInputResultDetail).canScrollToTop() + + assertFalse(behavior.shouldScroll) + } + + @Test + fun `Behavior will vertically scroll nested scroll started and EngineView handled the event`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + doReturn(true).`when`(behavior).shouldScroll + val child = spy(View(testContext, null, 0)) + behavior.dynamicScrollView = child + doReturn(100).`when`(child).height + doReturn(0f).`when`(child).translationY + behavior.startedScroll = true + + behavior.tryToScrollVertically(25f) + + verify(yTranslator).translate(child, 25f) + } + + @Test + fun `Behavior will not scroll vertically if startedScroll is false`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + doReturn(true).`when`(behavior).shouldScroll + val child = spy(View(testContext, null, 0)) + behavior.dynamicScrollView = child + doReturn(100).`when`(child).height + doReturn(0f).`when`(child).translationY + behavior.startedScroll = false + + behavior.tryToScrollVertically(25f) + + verify(yTranslator, never()).translate(any(), anyFloat()) + } + + @Test + fun `Behavior will not scroll vertically if EngineView did not handled the event`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + doReturn(false).`when`(behavior).shouldScroll + val child = spy(View(testContext, null, 0)) + behavior.dynamicScrollView = child + doReturn(100).`when`(child).height + doReturn(0f).`when`(child).translationY + + behavior.tryToScrollVertically(25f) + + verify(yTranslator, never()).translate(any(), anyFloat()) + } + + @Test + fun `forceExpand should delegate the translator`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + val view: View = mock() + + behavior.forceExpand(view) + + verify(yTranslator).expandWithAnimation(view) + } + + @Test + fun `forceCollapse should delegate the translator`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + val view: View = mock() + + behavior.forceCollapse(view) + + verify(yTranslator).collapseWithAnimation(view) + } + + @Test + fun `Behavior will not forceExpand when scrolling up and !shouldScroll if the touch was not yet handled in the browser`() { + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + val yTranslator: ViewYTranslator = mock() + behavior.yTranslator = yTranslator + behavior.initGesturesDetector(behavior.createGestureDetector()) + val view: View = spy(View(testContext, null, 0)) + behavior.dynamicScrollView = view + val engineView: EngineView = mock() + behavior.engineView = engineView + val handledTouchInput = InputResultDetail.newInstance() + doReturn(handledTouchInput).`when`(engineView).getInputResultDetail() + + doReturn(100).`when`(view).height + doReturn(100f).`when`(view).translationY + + val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f) + val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 30f, downEvent) + + behavior.onInterceptTouchEvent(mock(), mock(), downEvent) + behavior.onInterceptTouchEvent(mock(), mock(), moveEvent) + + verify(behavior).tryToScrollVertically(-30f) + verify(yTranslator, never()).forceExpandIfNotAlready(view, -30f) + } + + @Test + fun `onLayoutChild initializes browserToolbar and engineView`() { + val view = View(testContext) + val engineView = createDummyEngineView(testContext).asView() + val container = CoordinatorLayout(testContext).apply { + addView(View(testContext)) + addView(engineView) + } + val behavior = spy(EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM)) + + behavior.onLayoutChild(container, view, View.LAYOUT_DIRECTION_LTR) + + assertEquals(view, behavior.dynamicScrollView) + assertEquals(engineView, behavior.engineView) + } + + @Test + fun `enableScrolling sets isScrollEnabled to true`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + + assertFalse(behavior.isScrollEnabled) + behavior.enableScrolling() + + assertTrue(behavior.isScrollEnabled) + } + + @Test + fun `disableScrolling sets isScrollEnabled to false`() { + val behavior = EngineViewScrollingGesturesBehavior(testContext, null, ViewPosition.BOTTOM) + behavior.isScrollEnabled = true + + assertTrue(behavior.isScrollEnabled) + behavior.disableScrolling() + + assertFalse(behavior.isScrollEnabled) + } + + private fun createDummyEngineView(context: Context): EngineView = DummyEngineView(context) + + open class DummyEngineView(context: Context) : FrameLayout(context), EngineView { + override val verticalScrollPosition = flowOf(0f) + override val verticalScrollDelta = flowOf(0f) + override fun setVerticalClipping(clippingHeight: Int) {} + override fun setDynamicToolbarMaxHeight(height: Int) {} + override fun setActivityContext(context: Context?) {} + override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit + override fun render(session: EngineSession) {} + override fun release() {} + override var selectionActionDelegate: SelectionActionDelegate? = null + override fun addWindowInsetsListener( + key: String, + listener: androidx.core.view.OnApplyWindowInsetsListener?, + ) {} + override fun removeWindowInsetsListener(key: String) {} + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BottomToolbarContainerView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BottomToolbarContainerView.kt @@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import androidx.coordinatorlayout.widget.CoordinatorLayout import mozilla.components.concept.toolbar.ScrollableToolbar -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import mozilla.components.ui.widgets.behavior.ViewPosition import org.mozilla.fenix.R @@ -50,7 +50,7 @@ class BottomToolbarContainerView( ).apply { gravity = Gravity.BOTTOM if (hideOnScroll) { - behavior = EngineViewScrollingBehavior(parent.context, null, ViewPosition.BOTTOM) + behavior = EngineViewScrollingGesturesBehavior(parent.context, null, ViewPosition.BOTTOM) } } @@ -78,25 +78,25 @@ class ToolbarContainerView @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyleAttr), ScrollableToolbar { override fun enableScrolling() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.enableScrolling() + (behavior as? EngineViewScrollingGesturesBehavior)?.enableScrolling() } } override fun disableScrolling() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.disableScrolling() + (behavior as? EngineViewScrollingGesturesBehavior)?.disableScrolling() } } override fun expand() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.forceExpand(this@ToolbarContainerView) + (behavior as? EngineViewScrollingGesturesBehavior)?.forceExpand(this@ToolbarContainerView) } } override fun collapse() { (layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - (behavior as? EngineViewScrollingBehavior)?.forceCollapse(this@ToolbarContainerView) + (behavior as? EngineViewScrollingGesturesBehavior)?.forceCollapse(this@ToolbarContainerView) } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/FenixBrowserToolbarView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/FenixBrowserToolbarView.kt @@ -12,7 +12,7 @@ import androidx.core.view.isVisible import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.ExternalAppType import mozilla.components.concept.toolbar.ScrollableToolbar -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import mozilla.components.ui.widgets.behavior.ViewPosition import org.mozilla.fenix.utils.Settings @@ -49,7 +49,7 @@ abstract class FenixBrowserToolbarView( } (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingBehavior)?.forceExpand(layout) + (behavior as? EngineViewScrollingGesturesBehavior)?.forceExpand(layout) } } @@ -60,19 +60,19 @@ abstract class FenixBrowserToolbarView( } (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingBehavior)?.forceCollapse(layout) + (behavior as? EngineViewScrollingGesturesBehavior)?.forceCollapse(layout) } } override fun enableScrolling() { (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingBehavior)?.enableScrolling() + (behavior as? EngineViewScrollingGesturesBehavior)?.enableScrolling() } } override fun disableScrolling() { (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as? EngineViewScrollingBehavior)?.disableScrolling() + (behavior as? EngineViewScrollingGesturesBehavior)?.disableScrolling() } } @@ -132,7 +132,7 @@ abstract class FenixBrowserToolbarView( @VisibleForTesting internal fun setDynamicToolbarBehavior(toolbarPosition: ViewPosition) { (layout.layoutParams as CoordinatorLayout.LayoutParams).apply { - behavior = EngineViewScrollingBehavior(layout.context, null, toolbarPosition) + behavior = EngineViewScrollingGesturesBehavior(layout.context, null, toolbarPosition) } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserHomeToolbarViewTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserHomeToolbarViewTest.kt @@ -14,7 +14,7 @@ import io.mockk.verify import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.test.robolectric.testContext -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before @@ -31,7 +31,7 @@ import mozilla.components.ui.widgets.behavior.ViewPosition as MozacToolbarPositi class BrowserHomeToolbarViewTest { private lateinit var toolbarView: BrowserToolbarView private lateinit var toolbar: BrowserToolbar - private lateinit var behavior: EngineViewScrollingBehavior + private lateinit var behavior: EngineViewScrollingGesturesBehavior private lateinit var settings: Settings @Before @@ -60,7 +60,7 @@ class BrowserHomeToolbarViewTest { ) toolbarView.toolbar = toolbar - behavior = spyk(EngineViewScrollingBehavior(testContext, null, MozacToolbarPosition.BOTTOM)) + behavior = spyk(EngineViewScrollingGesturesBehavior(testContext, null, MozacToolbarPosition.BOTTOM)) (toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior } diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt @@ -10,7 +10,7 @@ import androidx.core.view.isVisible import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.concept.engine.EngineView import mozilla.components.ui.widgets.behavior.EngineViewClippingBehavior -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import org.mozilla.focus.R import mozilla.components.ui.widgets.behavior.ViewPosition as browserToolbarPosition @@ -39,7 +39,7 @@ fun BrowserToolbar.disableDynamicBehavior(engineView: EngineView) { * @param engineView [EngineView] that should react to toolbar's dynamic behavior. */ fun BrowserToolbar.enableDynamicBehavior(context: Context, engineView: EngineView) { - (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = EngineViewScrollingBehavior( + (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = EngineViewScrollingGesturesBehavior( context, null, browserToolbarPosition.TOP, diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt @@ -10,7 +10,7 @@ import mozilla.components.browser.engine.gecko.GeckoEngineView import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.support.test.robolectric.testContext import mozilla.components.ui.widgets.behavior.EngineViewClippingBehavior -import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingGesturesBehavior import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -42,7 +42,7 @@ internal class BrowserToolbarTest { toolbar.enableDynamicBehavior(testContext, engineView) - assertTrue((toolbar.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior is EngineViewScrollingBehavior) + assertTrue((toolbar.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior is EngineViewScrollingGesturesBehavior) assertTrue((engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior is EngineViewClippingBehavior) assertEquals(0, (engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin) verify(engineView).setDynamicToolbarMaxHeight(toolbarHeight)