[android] add regression tests for recent quick zoom/double tap issues

This commit is contained in:
Łukasz Paczos
2019-07-10 19:36:56 +02:00
committed by Łukasz Paczos
parent 0b0d7b844a
commit 200af48754
2 changed files with 615 additions and 0 deletions

View File

@@ -0,0 +1,454 @@
package com.mapbox.mapboxsdk.maps
import android.graphics.PointF
import android.os.SystemClock
import android.support.test.espresso.Espresso
import android.support.test.espresso.InjectEventSecurityException
import android.support.test.espresso.UiController
import android.support.test.espresso.ViewAction
import android.support.test.espresso.matcher.ViewMatchers
import android.view.MotionEvent
import android.view.View
import org.hamcrest.Matcher
object GesturesUiTestUtils {
private const val DEFAULT_GESTURE_DURATION = 500L
fun pinch(startSpan: Float, endSpan: Float, center: PointF? = null, duration: Long = DEFAULT_GESTURE_DURATION): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isEnabled()
}
override fun getDescription(): String = "Pinch $startSpan -> $endSpan"
override fun perform(uiController: UiController, view: View) {
var middlePosition = center
if (middlePosition == null) {
middlePosition = getCenterPointF(view)
}
val startPoint1 = PointF(middlePosition.x - startSpan / 2f, middlePosition.y)
val startPoint2 = PointF(middlePosition.x + startSpan / 2f, middlePosition.y)
val endPoint1 = PointF(middlePosition.x - endSpan / 2f, middlePosition.y)
val endPoint2 = PointF(middlePosition.x + endSpan / 2f, middlePosition.y)
performPinch(uiController, startPoint1, startPoint2, endPoint1, endPoint2, duration)
}
}
}
fun quickScale(deltaY: Float, startPoint: PointF? = null, withVelocity: Boolean = true, duration: Long = DEFAULT_GESTURE_DURATION, interrupt: Boolean = false): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = ViewMatchers.isEnabled()
override fun getDescription(): String = "quick scale ${deltaY}Y"
override fun perform(uiController: UiController, view: View) {
var middlePosition = startPoint
if (middlePosition == null) {
middlePosition = getCenterPointF(view)
}
val endPoint = PointF(middlePosition.x, middlePosition.y + deltaY)
performQuickScale(uiController, middlePosition, endPoint, withVelocity, duration, interrupt)
}
}
}
fun move(deltaX: Float, deltaY: Float, startPoint: PointF? = null, withVelocity: Boolean = true, duration: Long = DEFAULT_GESTURE_DURATION): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = ViewMatchers.isEnabled()
override fun getDescription(): String = "move ${deltaX}X, ${deltaY}Y"
override fun perform(uiController: UiController, view: View) {
var middlePosition = startPoint
if (middlePosition == null) {
middlePosition = getCenterPointF(view)
}
val endPoint = PointF(middlePosition.x + deltaX, middlePosition.y + deltaY)
performMove(uiController, middlePosition, endPoint, withVelocity, duration)
}
}
}
private fun getCenterPointF(view: View): PointF {
val locationOnScreen = IntArray(2)
view.getLocationOnScreen(locationOnScreen)
val viewHeight = view.height * view.scaleY
val viewWidth = view.width * view.scaleX
return PointF(
(locationOnScreen[0] + viewWidth / 2).toInt().toFloat(),
(locationOnScreen[1] + viewHeight / 2).toInt().toFloat())
}
// https://stackoverflow.com/a/46443628/9126211
private fun performPinch(
uiController: UiController,
startPoint1: PointF,
startPoint2: PointF,
endPoint1: PointF,
endPoint2: PointF,
duration: Long
) {
val eventMinInterval: Long = 10
val startTime = SystemClock.uptimeMillis()
var eventTime = startTime
var event: MotionEvent
var eventX1: Float = startPoint1.x
var eventY1: Float = startPoint1.y
var eventX2: Float = startPoint2.x
var eventY2: Float = startPoint2.y
// Specify the property for the two touch points
val properties = arrayOfNulls<MotionEvent.PointerProperties>(2)
val pp1 = MotionEvent.PointerProperties()
pp1.id = 0
pp1.toolType = MotionEvent.TOOL_TYPE_FINGER
val pp2 = MotionEvent.PointerProperties()
pp2.id = 1
pp2.toolType = MotionEvent.TOOL_TYPE_FINGER
properties[0] = pp1
properties[1] = pp2
// Specify the coordinations of the two touch points
// NOTE: you MUST set the pressure and size value, or it doesn't work
val pointerCoords = arrayOfNulls<MotionEvent.PointerCoords>(2)
val pc1 = MotionEvent.PointerCoords()
pc1.x = eventX1
pc1.y = eventY1
pc1.pressure = 1f
pc1.size = 1f
val pc2 = MotionEvent.PointerCoords()
pc2.x = eventX2
pc2.y = eventY2
pc2.pressure = 1f
pc2.size = 1f
pointerCoords[0] = pc1
pointerCoords[1] = pc2
/*
* Events sequence of zoom gesture:
*
* 1. Send ACTION_DOWN event of one start point
* 2. Send ACTION_POINTER_DOWN of two start points
* 3. Send ACTION_MOVE of two middle points
* 4. Repeat step 3 with updated middle points (x,y), until reach the end points
* 5. Send ACTION_POINTER_UP of two end points
* 6. Send ACTION_UP of one end point
*/
try {
// Step 1
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_DOWN, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 2
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_POINTER_DOWN + (pp2.id shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2,
properties, pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 3, 4
val moveEventNumber = duration / eventMinInterval
val stepX1: Float
val stepY1: Float
val stepX2: Float
val stepY2: Float
stepX1 = (endPoint1.x - startPoint1.x) / moveEventNumber
stepY1 = (endPoint1.y - startPoint1.y) / moveEventNumber
stepX2 = (endPoint2.x - startPoint2.x) / moveEventNumber
stepY2 = (endPoint2.y - startPoint2.y) / moveEventNumber
for (i in 0 until moveEventNumber) {
// Update the move events
eventTime += eventMinInterval
eventX1 += stepX1
eventY1 += stepY1
eventX2 += stepX2
eventY2 += stepY2
pc1.x = eventX1
pc1.y = eventY1
pc2.x = eventX2
pc2.y = eventY2
pointerCoords[0] = pc1
pointerCoords[1] = pc2
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_MOVE, 2, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
}
// Step 5
pc1.x = endPoint1.x
pc1.y = endPoint1.y
pc2.x = endPoint2.x
pc2.y = endPoint2.y
pointerCoords[0] = pc1
pointerCoords[1] = pc2
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_POINTER_UP + (pp2.id shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 6
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_UP, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
} catch (ex: InjectEventSecurityException) {
throw RuntimeException("Could not perform pinch", ex)
}
}
private fun performQuickScale(uiController: UiController, startPoint: PointF, endPoint: PointF, withVelocity: Boolean, duration: Long, interrupt: Boolean) {
val eventMinInterval: Long = 10
val tapDownMinInterval: Long = 40 // ViewConfiguration#DOUBLE_TAP_MIN_TIME = 40
val startTime = SystemClock.uptimeMillis()
var eventTime = startTime
var event: MotionEvent
var eventX1: Float = startPoint.x
var eventY1: Float = startPoint.y
var properties = arrayOfNulls<MotionEvent.PointerProperties>(1)
val pp1 = MotionEvent.PointerProperties()
pp1.id = 0
pp1.toolType = MotionEvent.TOOL_TYPE_FINGER
properties[0] = pp1
var pointerCoords = arrayOfNulls<MotionEvent.PointerCoords>(1)
val pc1 = MotionEvent.PointerCoords()
pc1.x = eventX1
pc1.y = eventY1
pc1.pressure = 1f
pc1.size = 1f
pointerCoords[0] = pc1
/*
* Events sequence of quick scale gesture:
*
* 1. Send ACTION_DOWN
* 2. Send ACTION_UP
* 3. Send ACTION_DOWN
* 4. Send ACTION_MOVE with updated middle points (x,y), until reach the end points
* 5. Send ACTION_UP of one end point
*/
try {
// Step 1
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_DOWN, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 2
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_UP, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 3
eventTime += tapDownMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_DOWN, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 4
val moveEventNumber = duration / eventMinInterval
val stepX1: Float
val stepY1: Float
stepX1 = (endPoint.x - startPoint.x) / moveEventNumber
stepY1 = (endPoint.y - startPoint.y) / moveEventNumber
var interrupted = false
for (i in 0 until moveEventNumber) {
// Update the move events
eventTime += eventMinInterval
eventX1 += stepX1
eventY1 += stepY1
pc1.x = eventX1
pc1.y = eventY1
pointerCoords[0] = pc1
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_MOVE, if (interrupted) 2 else 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
if (interrupt && i == moveEventNumber / 2) {
// Specify the property for the two touch points
properties = arrayOfNulls<MotionEvent.PointerProperties>(2)
val pp2 = MotionEvent.PointerProperties()
pp2.id = 1
pp2.toolType = MotionEvent.TOOL_TYPE_FINGER
properties[0] = pp1
properties[1] = pp2
// Specify the coordinations of the two touch points
// NOTE: you MUST set the pressure and size value, or it doesn't work
pointerCoords = arrayOfNulls<MotionEvent.PointerCoords>(2)
val pc2 = MotionEvent.PointerCoords()
pc2.x = startPoint.x
pc2.y = startPoint.y
pc2.pressure = 1f
pc2.size = 1f
pointerCoords[0] = pc1
pointerCoords[1] = pc2
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_POINTER_DOWN + (pp2.id shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2,
properties, pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
interrupted = true
}
}
if (!withVelocity) {
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_MOVE, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
}
// Step 5
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_UP, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
} catch (ex: InjectEventSecurityException) {
throw RuntimeException("Could not perform quick scale", ex)
}
}
private fun performMove(uiController: UiController, startPoint: PointF, endPoint: PointF, withVelocity: Boolean, duration: Long) {
val eventMinInterval: Long = 10
val startTime = SystemClock.uptimeMillis()
var eventTime = startTime
var event: MotionEvent
var eventX1: Float = startPoint.x
var eventY1: Float = startPoint.y
val properties = arrayOfNulls<MotionEvent.PointerProperties>(1)
val pp1 = MotionEvent.PointerProperties()
pp1.id = 0
pp1.toolType = MotionEvent.TOOL_TYPE_FINGER
properties[0] = pp1
val pointerCoords = arrayOfNulls<MotionEvent.PointerCoords>(1)
val pc1 = MotionEvent.PointerCoords()
pc1.x = eventX1
pc1.y = eventY1
pc1.pressure = 1f
pc1.size = 1f
pointerCoords[0] = pc1
/*
* Events sequence of move gesture:
*
* 1. Send ACTION_DOWN
* 2. Send ACTION_MOVE with updated middle points (x,y), until reach the end points
* 3. Send ACTION_UP
*/
try {
// Step 1
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_DOWN, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
// Step 2
val moveEventNumber = duration / eventMinInterval
val stepX1: Float
val stepY1: Float
stepX1 = (endPoint.x - startPoint.x) / moveEventNumber
stepY1 = (endPoint.y - startPoint.y) / moveEventNumber
for (i in 0 until moveEventNumber) {
// Update the move events
eventTime += eventMinInterval
eventX1 += stepX1
eventY1 += stepY1
pc1.x = eventX1
pc1.y = eventY1
pointerCoords[0] = pc1
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_MOVE, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
}
if (!withVelocity) {
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_MOVE, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
}
// Step 3
eventTime += eventMinInterval
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_UP, 1, properties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
injectMotionEventToUiController(uiController, event)
} catch (ex: InjectEventSecurityException) {
throw RuntimeException("Could not perform quick scale", ex)
}
}
/**
* Safely call uiController.injectMotionEvent(event): Detect any error and "convert" it to an
* IllegalStateException
*/
@Throws(InjectEventSecurityException::class)
private fun injectMotionEventToUiController(uiController: UiController, event: MotionEvent) {
val injectEventSucceeded = uiController.injectMotionEvent(event)
if (!injectEventSucceeded) {
throw IllegalStateException("Error performing event $event")
}
}
}
fun Int.loopFor(millis: Long) {
Espresso.onView(ViewMatchers.withId(this)).perform(object : ViewAction {
override fun getDescription(): String = "waiting for $millis"
override fun getConstraints(): Matcher<View> = ViewMatchers.isEnabled()
override fun perform(uiController: UiController?, view: View?) {
uiController?.loopMainThreadForAtLeast(millis)
}
})
}

View File

@@ -0,0 +1,161 @@
package com.mapbox.mapboxsdk.maps
import android.graphics.PointF
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.matcher.ViewMatchers.withId
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.constants.MapboxConstants
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.GesturesUiTestUtils.move
import com.mapbox.mapboxsdk.maps.GesturesUiTestUtils.quickScale
import com.mapbox.mapboxsdk.testapp.R
import com.mapbox.mapboxsdk.testapp.activity.BaseTest
import com.mapbox.mapboxsdk.testapp.activity.maplayout.SimpleMapActivity
import junit.framework.Assert
import org.junit.Before
import org.junit.Test
class MapGestureDetectorTest : BaseTest() {
override fun getActivityClass() = SimpleMapActivity::class.java
private var maxWidth: Int = 0
private var maxHeight: Int = 0
@Before
fun setup() {
maxWidth = mapView.width
maxHeight = mapView.height
}
@Test
fun sanity_quickZoom() {
validateTestSetup()
var initialZoom: Double? = null
rule.runOnUiThread {
initialZoom = mapboxMap.cameraPosition.zoom
}
onView(withId(R.id.mapView)).perform(quickScale(maxHeight / 2f, withVelocity = false))
rule.runOnUiThread {
Assert.assertTrue(mapboxMap.cameraPosition.zoom > initialZoom!!)
Assert.assertTrue(Math.abs(mapboxMap.cameraPosition.zoom - initialZoom!!) > 0.5)
}
}
@Test
fun quickZoomDisabled_phantomQuickZoom_moveStillEnabled_15091() {
// regression test for https://github.com/mapbox/mapbox-gl-native/issues/15091
validateTestSetup()
var initialCameraPosition: CameraPosition? = null
rule.runOnUiThread {
// zoom in so we can move vertically
mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(4.0))
initialCameraPosition = mapboxMap.cameraPosition
mapboxMap.uiSettings.isQuickZoomGesturesEnabled = false
}
onView(withId(R.id.mapView)).perform(quickScale(maxHeight / 2f))
rule.runOnUiThread {
// camera did not move
Assert.assertEquals(initialCameraPosition!!, mapboxMap.cameraPosition)
}
var expectedTarget: LatLng? = null
rule.runOnUiThread {
val currentPoint = mapboxMap.projection.toScreenLocation(initialCameraPosition!!.target)
val resultingPoint = PointF(currentPoint.x + maxWidth / 2f, currentPoint.y + maxHeight / 2f)
expectedTarget = mapboxMap.projection.fromScreenLocation(resultingPoint)
}
// move to expected target
onView(withId(R.id.mapView)).perform(move(-maxWidth / 2f, -maxHeight / 2f, withVelocity = false))
rule.runOnUiThread {
Assert.assertEquals(expectedTarget!!.latitude, mapboxMap.cameraPosition.target.latitude, 5.0)
Assert.assertEquals(expectedTarget!!.longitude, mapboxMap.cameraPosition.target.longitude, 5.0)
}
}
@Test
fun quickZoom_doNotMove_14227() {
// test for https://github.com/mapbox/mapbox-gl-native/issues/14227
validateTestSetup()
var initialTarget: LatLng? = null
rule.runOnUiThread {
initialTarget = mapboxMap.cameraPosition.target
}
onView(withId(R.id.mapView)).perform(quickScale(maxHeight / 2f))
rule.runOnUiThread {
// camera did not move
Assert.assertEquals(initialTarget!!.latitude, mapboxMap.cameraPosition.target.latitude, 0.5)
Assert.assertEquals(initialTarget!!.longitude, mapboxMap.cameraPosition.target.longitude, 0.5)
}
}
@Test
fun quickZoom_interrupted_moveStillEnabled_14598() {
// test for https://github.com/mapbox/mapbox-gl-native/issues/14598
validateTestSetup()
onView(withId(R.id.mapView)).perform(quickScale(maxHeight / 2f, interrupt = true))
var expectedTarget: LatLng? = null
rule.runOnUiThread {
val currentPoint = mapboxMap.projection.toScreenLocation(mapboxMap.cameraPosition.target)
val resultingPoint = PointF(currentPoint.x + maxWidth / 2f, currentPoint.y + maxHeight / 2f)
expectedTarget = mapboxMap.projection.fromScreenLocation(resultingPoint)
}
// move to expected target
onView(withId(R.id.mapView)).perform(move(-maxWidth / 2f, -maxHeight / 2f, withVelocity = false))
rule.runOnUiThread {
Assert.assertEquals(expectedTarget!!.latitude, mapboxMap.cameraPosition.target.latitude, 10.0)
Assert.assertEquals(expectedTarget!!.longitude, mapboxMap.cameraPosition.target.longitude, 10.0)
}
}
@Test
fun quickZoom_ignoreDoubleTap() {
// test for https://github.com/mapbox/mapbox-gl-native/issues/14013
validateTestSetup()
var initialZoom: Double? = null
rule.runOnUiThread {
mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(2.0))
initialZoom = mapboxMap.cameraPosition.zoom
}
onView(withId(R.id.mapView)).perform(quickScale(-(mapboxMap.gesturesManager.standardScaleGestureDetector.spanSinceStartThreshold * 2), withVelocity = false, duration = 1000L))
R.id.mapView.loopFor(MapboxConstants.ANIMATION_DURATION.toLong())
rule.runOnUiThread {
Assert.assertTrue(mapboxMap.cameraPosition.zoom < initialZoom!!)
}
}
@Test
fun doubleTap_minimalMovement() {
validateTestSetup()
var initialZoom: Double? = null
rule.runOnUiThread {
initialZoom = mapboxMap.cameraPosition.zoom
}
onView(withId(R.id.mapView)).perform(quickScale(mapboxMap.gesturesManager.standardScaleGestureDetector.spanSinceStartThreshold / 2, withVelocity = false, duration = 50L))
R.id.mapView.loopFor(MapboxConstants.ANIMATION_DURATION.toLong())
rule.runOnUiThread {
Assert.assertEquals(initialZoom!! + 1, mapboxMap.cameraPosition.zoom, 0.1)
}
}
@Test
fun doubleTap_overMaxThreshold_ignore_14013() {
// test for https://github.com/mapbox/mapbox-gl-native/issues/14013
validateTestSetup()
var initialZoom: Double? = null
rule.runOnUiThread {
initialZoom = mapboxMap.cameraPosition.zoom
mapboxMap.uiSettings.isQuickZoomGesturesEnabled = false
}
onView(withId(R.id.mapView)).perform(quickScale(mapboxMap.gesturesManager.standardScaleGestureDetector.spanSinceStartThreshold * 2, withVelocity = false, duration = 50L))
R.id.mapView.loopFor(MapboxConstants.ANIMATION_DURATION.toLong())
rule.runOnUiThread {
Assert.assertEquals(initialZoom!!, mapboxMap.cameraPosition.zoom, 0.01)
}
}
}