summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InputResultDetailTest.kt
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InputResultDetailTest.kt')
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InputResultDetailTest.kt417
1 files changed, 417 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InputResultDetailTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InputResultDetailTest.kt
new file mode 100644
index 0000000000..cfe0bcaf12
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InputResultDetailTest.kt
@@ -0,0 +1,417 @@
+package org.mozilla.geckoview.test
+
+import android.os.SystemClock
+import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.ContentDelegate
+import org.mozilla.geckoview.PanZoomController
+import org.mozilla.geckoview.PanZoomController.InputResultDetail
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class InputResultDetailTest : BaseSessionTest() {
+ private val scrollWaitTimeout = 10000.0 // 10 seconds
+
+ private fun setupDocument(documentPath: String) {
+ mainSession.loadTestPath(documentPath)
+ sessionRule.waitUntilCalled(object : ContentDelegate {
+ @GeckoSessionTestRule.AssertCalled(count = 1)
+ override fun onFirstContentfulPaint(session: GeckoSession) {
+ }
+ })
+ mainSession.flushApzRepaints()
+ }
+
+ private fun sendDownEvent(x: Float, y: Float): GeckoResult<InputResultDetail> {
+ val downTime = SystemClock.uptimeMillis()
+ val down = MotionEvent.obtain(
+ downTime,
+ SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_DOWN,
+ x,
+ y,
+ 0,
+ )
+
+ val result = mainSession.panZoomController.onTouchEventForDetailResult(down)
+
+ val up = MotionEvent.obtain(
+ downTime,
+ SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_UP,
+ x,
+ y,
+ 0,
+ )
+
+ mainSession.panZoomController.onTouchEvent(up)
+
+ return result
+ }
+
+ private fun assertResultDetail(
+ testName: String,
+ actual: InputResultDetail,
+ expectedHandledResult: Int,
+ expectedScrollableDirections: Int,
+ expectedOverscrollDirections: Int,
+ ) {
+ assertThat(
+ testName + ": The handled result",
+ actual.handledResult(),
+ equalTo(expectedHandledResult),
+ )
+ assertThat(
+ testName + ": The scrollable directions",
+ actual.scrollableDirections(),
+ equalTo(expectedScrollableDirections),
+ )
+ assertThat(
+ testName + ": The overscroll directions",
+ actual.overscrollDirections(),
+ equalTo(expectedOverscrollDirections),
+ )
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testTouchAction() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+
+ for (subframe in arrayOf(true, false)) {
+ for (scrollable in arrayOf(true, false)) {
+ for (event in arrayOf(true, false)) {
+ for (touchAction in arrayOf("auto", "none", "pan-x", "pan-y")) {
+ var url = TOUCH_ACTION_HTML_PATH + "?"
+ if (subframe) {
+ url += "subframe&"
+ }
+ if (scrollable) {
+ url += "scrollable&"
+ }
+ if (event) {
+ url += "event&"
+ }
+ url += ("touch-action=" + touchAction)
+
+ setupDocument(url)
+
+ // Since sendDownEvent() just sends a touch-down, APZ doesn't
+ // yet know the direction, hence it allows scrolling in both
+ // the pan-x and pan-y cases.
+ var expectedPlace = if (touchAction == "none" || (subframe && scrollable)) {
+ PanZoomController.INPUT_RESULT_HANDLED_CONTENT
+ } else if (scrollable) {
+ PanZoomController.INPUT_RESULT_HANDLED
+ } else {
+ PanZoomController.INPUT_RESULT_UNHANDLED
+ }
+
+ var expectedScrollableDirections = if (scrollable) {
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM
+ } else {
+ PanZoomController.SCROLLABLE_FLAG_NONE
+ }
+
+ // FIXME: There are a couple of bugs here:
+ // 1. In the case where touch-action allows the scrolling, the
+ // overscroll directions shouldn't depend on the presence of
+ // an event handler, but they do.
+ // 2. In the case where touch-action doesn't allow the scrolling,
+ // the overscroll directions should probably be NONE.
+ var expectedOverscrollDirections = if (touchAction != "none" && !scrollable && event) {
+ PanZoomController.OVERSCROLL_FLAG_NONE
+ } else {
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL)
+ }
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 20f))
+ assertResultDetail(
+ "`subframe=$subframe, scrollable=$scrollable, event=$event, touch-action=$touchAction`",
+ value,
+ expectedPlace,
+ expectedScrollableDirections,
+ expectedOverscrollDirections,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testScrollableWithDynamicToolbar() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+
+ // Set active since setVerticalClipping call affects only for forground tab.
+ mainSession.setActive(true)
+
+ setupDocument(ROOT_100VH_HTML_PATH + "?event")
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ ROOT_100VH_HTML_PATH,
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL),
+ )
+
+ // Prepare a resize event listener.
+ val resizePromise = mainSession.evaluatePromiseJS(
+ """
+ new Promise(resolve => {
+ window.visualViewport.addEventListener('resize', () => {
+ resolve(true);
+ }, { once: true });
+ });
+ """.trimIndent(),
+ )
+
+ // Hide the dynamic toolbar.
+ sessionRule.display?.run { setVerticalClipping(-20) }
+
+ // Wait a visualViewport resize event to make sure the toolbar change has been reflected.
+ assertThat("resize", resizePromise.value as Boolean, equalTo(true))
+
+ value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+ assertResultDetail(
+ ROOT_100VH_HTML_PATH,
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_TOP,
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL),
+ )
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testOverscrollBehaviorAuto() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+ setupDocument(OVERSCROLL_BEHAVIOR_AUTO_HTML_PATH)
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ "`overscroll-behavior: auto`",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL),
+ )
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testOverscrollBehaviorAutoNone() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+ setupDocument(OVERSCROLL_BEHAVIOR_AUTO_NONE_HTML_PATH)
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ "`overscroll-behavior: auto, none`",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ PanZoomController.OVERSCROLL_FLAG_HORIZONTAL,
+ )
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testOverscrollBehaviorNoneAuto() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+ setupDocument(OVERSCROLL_BEHAVIOR_NONE_AUTO_HTML_PATH)
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ "`overscroll-behavior: none, auto`",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ PanZoomController.OVERSCROLL_FLAG_VERTICAL,
+ )
+ }
+
+ // NOTE: This function requires #scroll element in the target document.
+ private fun scrollToBottom() {
+ // Prepare a scroll event listener.
+ val scrollPromise = mainSession.evaluatePromiseJS(
+ """
+ new Promise(resolve => {
+ const scroll = document.getElementById('scroll');
+ scroll.addEventListener('scroll', () => {
+ resolve(true);
+ }, { once: true });
+ });
+ """.trimIndent(),
+ )
+
+ // Scroll to the bottom edge of the scroll container.
+ mainSession.evaluateJS(
+ """
+ const scroll = document.getElementById('scroll');
+ scroll.scrollTo(0, scroll.scrollHeight);
+ """.trimIndent(),
+ )
+ assertThat("scroll", scrollPromise.value as Boolean, equalTo(true))
+ mainSession.flushApzRepaints()
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testScrollHandoff() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+ setupDocument(SCROLL_HANDOFF_HTML_PATH)
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ // There is a child scroll container and its overscroll-behavior is `contain auto`
+ assertResultDetail(
+ "handoff",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED_CONTENT,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ PanZoomController.OVERSCROLL_FLAG_VERTICAL,
+ )
+
+ // Scroll to the bottom edge
+ scrollToBottom()
+
+ value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ // Now the touch event should be handed to the root scroller.
+ assertResultDetail(
+ "handoff",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL),
+ )
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testOverscrollBehaviorNoneOnNonRoot() {
+ var files = arrayOf(
+ OVERSCROLL_BEHAVIOR_NONE_NON_ROOT_HTML_PATH,
+ )
+
+ for (file in files) {
+ setupDocument(file)
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ "`overscroll-behavior: none` on non root scroll container",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED_CONTENT,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ PanZoomController.OVERSCROLL_FLAG_NONE,
+ )
+
+ // Scroll to the bottom edge so that the container is no longer scrollable downwards.
+ scrollToBottom()
+
+ value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ // The touch event should be handled in the scroll container content.
+ assertResultDetail(
+ "`overscroll-behavior: none` on non root scroll container",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED_CONTENT,
+ PanZoomController.SCROLLABLE_FLAG_TOP,
+ PanZoomController.OVERSCROLL_FLAG_NONE,
+ )
+ }
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
+ fun testOverscrollBehaviorNoneOnNonRootWithDynamicToolbar() {
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(20) }
+
+ var files = arrayOf(
+ OVERSCROLL_BEHAVIOR_NONE_NON_ROOT_HTML_PATH,
+ )
+
+ for (file in files) {
+ setupDocument(file)
+
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ "`overscroll-behavior: none` on non root scroll container",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED_CONTENT,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ PanZoomController.OVERSCROLL_FLAG_NONE,
+ )
+
+ // Scroll to the bottom edge so that the container is no longer scrollable downwards.
+ scrollToBottom()
+
+ value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ // Now the touch event should be handed to the root scroller even if
+ // the scroll container's `overscroll-behavior` is none to move
+ // the dynamic toolbar.
+ assertResultDetail(
+ "`overscroll-behavior: none, none`",
+ value,
+ PanZoomController.INPUT_RESULT_HANDLED,
+ PanZoomController.SCROLLABLE_FLAG_BOTTOM,
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL),
+ )
+ }
+ }
+
+ // Tests a situation where converting a scrollport size between CSS units and app units will
+ // result different values, and the difference causes an issue that unscrollable documents
+ // behave as if it's scrollable.
+ //
+ // Note about metrics that this test uses.
+ // A basic here is that documents having no meta viewport tags are laid out on 980px width
+ // canvas, the 980px is defined as "browser.viewport.desktopWidth".
+ //
+ // So, if the device screen size is (1080px, 2160px) then the document is scaled to
+ // (1080 / 980) = 1.10204. Then if the dynamic toolbar height is 59px, the scaled document
+ // height is (2160 - 59) / 1.10204 = 1906.46 (in CSS units). It's converted and actually rounded
+ // to 114388 (= 1906.46 * 60) in app units. And it's converted back to 1906.47 (114388 / 60) in
+ // CSS units unfortunately.
+ @WithDisplay(width = 1080, height = 2160)
+ @Test
+ fun testFractionalScrollPortSize() {
+ sessionRule.setPrefsUntilTestEnd(
+ mapOf(
+ "browser.viewport.desktopWidth" to 980,
+ ),
+ )
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(59) }
+
+ setupDocument(NO_META_VIEWPORT_HTML_PATH)
+
+ // Try to scroll down to see if the document is scrollable or not.
+ var value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
+
+ assertResultDetail(
+ "The document isn't not scrollable at all",
+ value,
+ PanZoomController.INPUT_RESULT_UNHANDLED,
+ PanZoomController.SCROLLABLE_FLAG_NONE,
+ (PanZoomController.OVERSCROLL_FLAG_HORIZONTAL or PanZoomController.OVERSCROLL_FLAG_VERTICAL),
+ )
+ }
+}