diff options
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.kt | 417 |
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), + ) + } +} |