blob: dfbc831777370de016543c4ead2f4646922e1ee6 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.core.pointerinput
import androidx.test.filters.SmallTest
import androidx.ui.core.ConsumedData
import androidx.ui.core.LayoutNode
import androidx.ui.core.Modifier
import androidx.ui.core.Owner
import androidx.ui.core.PointerEventPass
import androidx.ui.core.PointerId
import androidx.ui.core.PointerInputChange
import androidx.ui.core.PointerInputData
import androidx.ui.core.PointerInputHandler
import androidx.ui.core.consumePositionChange
import androidx.ui.unit.IntPxPosition
import androidx.ui.unit.IntPxSize
import androidx.ui.unit.PxPosition
import androidx.ui.unit.Uptime
import androidx.ui.unit.ipx
import androidx.ui.unit.milliseconds
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.inOrder
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
// TODO(shepshapard): Write the following PointerInputEvent to PointerInputChangeEvent tests
// 2 down, 2 move, 2 up, converted correctly
// 3 down, 3 move, 3 up, converted correctly
// down, up, down, up, converted correctly
// 2 down, 1 up, same down, both up, converted correctly
// 2 down, 1 up, new down, both up, converted correctly
// new is up, throws exception
// TODO(shepshapard): Write the following hit testing tests
// 2 down, one hits, target receives correct event
// 2 down, one moves in, one out, 2 up, target receives correct event stream
// down, up, receives down and up
// down, move, up, receives all 3
// down, up, then down and misses, target receives down and up
// down, misses, moves in bounds, up, target does not receive event
// down, hits, moves out of bounds, up, target receives all events
// TODO(shepshapard): Write the following offset testing tests
// 3 simultaneous moves, offsets are correct
// TODO(shepshapard): Write the following pointer input dispatch path tests:
// down, move, up, on 2, hits all 5 passes
@SmallTest
@RunWith(JUnit4::class)
class PointerInputEventProcessorTest {
private lateinit var root: LayoutNode
private lateinit var pointerInputEventProcessor: PointerInputEventProcessor
private val testOwner: TestOwner = spy()
@Before
fun setup() {
root = LayoutNode(0, 0, 500, 500)
root.attach(testOwner)
pointerInputEventProcessor = PointerInputEventProcessor(root)
}
@Test
fun process_downMoveUp_convertedCorrectlyAndTraversesAllPassesInCorrectOrder() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0,
0,
500,
500,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.insertAt(0, layoutNode)
val offset = PxPosition(100f, 200f)
val offset2 = PxPosition(300f, 400f)
val events = arrayOf(
PointerInputEvent(8712, Uptime.Boot + 3.milliseconds, offset, true),
PointerInputEvent(8712, Uptime.Boot + 11.milliseconds, offset2, true),
PointerInputEvent(8712, Uptime.Boot + 13.milliseconds, offset2, false)
)
val expectedChanges = arrayOf(
PointerInputChange(
id = PointerId(8712),
current = PointerInputData(Uptime.Boot + 3.milliseconds, offset, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
),
PointerInputChange(
id = PointerId(8712),
current = PointerInputData(Uptime.Boot + 11.milliseconds, offset2, true),
previous = PointerInputData(Uptime.Boot + 3.milliseconds, offset, true),
consumed = ConsumedData()
),
PointerInputChange(
id = PointerId(8712),
current = PointerInputData(Uptime.Boot + 13.milliseconds, offset2, false),
previous = PointerInputData(Uptime.Boot + 11.milliseconds, offset2, true),
consumed = ConsumedData()
)
)
// Act
events.forEach { pointerInputEventProcessor.process(it) }
// Assert
// Verify call count
verify(
pointerInputFilter,
times(PointerEventPass.values().size * expectedChanges.size)
).onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter) {
for (expected in expectedChanges) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expected)),
eq(pass),
any()
)
}
}
}
}
@Test
fun process_downHits_targetReceives() {
// Arrange
val childOffset = PxPosition(100f, 200f)
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
100, 200, 301, 401,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.insertAt(0, layoutNode)
val offsets = arrayOf(
PxPosition(100f, 200f),
PxPosition(300f, 200f),
PxPosition(100f, 400f),
PxPosition(300f, 400f)
)
val events = Array(4) { index ->
PointerInputEvent(index, Uptime.Boot + 5.milliseconds, offsets[index], true)
}
val expectedChanges = Array(4) { index ->
PointerInputChange(
id = PointerId(index.toLong()),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
offsets[index] - childOffset,
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
}
// Act
events.forEach {
pointerInputEventProcessor.process(it)
}
// Assert
// Verify call count
verify(pointerInputFilter, times(expectedChanges.size)).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
// Verify call values
for (expected in expectedChanges) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expected)),
eq(PointerEventPass.InitialDown),
any()
)
}
}
@Test
fun process_downMisses_targetDoesNotReceive() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
100, 200, 301, 401,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.insertAt(0, layoutNode)
val offsets = arrayOf(
PxPosition(99f, 200f),
PxPosition(99f, 400f),
PxPosition(100f, 199f),
PxPosition(100f, 401f),
PxPosition(300f, 199f),
PxPosition(300f, 401f),
PxPosition(301f, 200f),
PxPosition(301f, 400f)
)
val events = Array(8) { index ->
PointerInputEvent(index, Uptime.Boot + 0.milliseconds, offsets[index], true)
}
// Act
events.forEach {
pointerInputEventProcessor.process(it)
}
// Assert
verify(pointerInputFilter, never()).onPointerInput(any(), any(), any())
}
@Test
fun process_downHits3of3_all3PointerNodesReceive() {
process_partialTreeHits(3)
}
@Test
fun process_downHits2of3_correct2PointerNodesReceive() {
process_partialTreeHits(2)
}
@Test
fun process_downHits1of3_onlyCorrectPointerNodesReceives() {
process_partialTreeHits(1)
}
private fun process_partialTreeHits(numberOfChildrenHit: Int) {
// Arrange
val childPointerInputFilter: PointerInputFilter = spy()
val middlePointerInputFilter: PointerInputFilter = spy()
val parentPointerInputFilter: PointerInputFilter = spy()
val childLayoutNode =
LayoutNode(
100, 100, 200, 200,
PointerInputModifierImpl(
childPointerInputFilter
)
)
val middleLayoutNode: LayoutNode =
LayoutNode(
100, 100, 400, 400,
PointerInputModifierImpl(
middlePointerInputFilter
)
).apply {
insertAt(0, childLayoutNode)
}
val parentLayoutNode: LayoutNode =
LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(
parentPointerInputFilter
)
).apply {
insertAt(0, middleLayoutNode)
}
root.insertAt(0, parentLayoutNode)
val offset = when (numberOfChildrenHit) {
3 -> PxPosition(250f, 250f)
2 -> PxPosition(150f, 150f)
1 -> PxPosition(50f, 50f)
else -> throw IllegalStateException()
}
val event = PointerInputEvent(0, Uptime.Boot + 5.milliseconds, offset, true)
// Act
pointerInputEventProcessor.process(event)
// Assert
when (numberOfChildrenHit) {
3 -> {
verify(parentPointerInputFilter).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
verify(middlePointerInputFilter).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
verify(childPointerInputFilter).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
}
2 -> {
verify(parentPointerInputFilter).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
verify(middlePointerInputFilter).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
verify(childPointerInputFilter, never()).onPointerInput(
any(),
any(),
any()
)
}
1 -> {
verify(parentPointerInputFilter).onPointerInput(
any(),
eq(PointerEventPass.InitialDown),
any()
)
verify(middlePointerInputFilter, never()).onPointerInput(
any(),
any(),
any()
)
verify(childPointerInputFilter, never()).onPointerInput(
any(),
any(),
any()
)
}
else -> throw IllegalStateException()
}
}
@Test
fun process_modifiedChange_isPassedToNext() {
// Arrange
val input = PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(100f, 0f),
true
),
previous = PointerInputData(Uptime.Boot + 3.milliseconds, PxPosition(0f, 0f), true),
consumed = ConsumedData(positionChange = PxPosition(0f, 0f))
)
val output = PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(100f, 0f),
true
),
previous = PointerInputData(Uptime.Boot + 3.milliseconds, PxPosition(0f, 0f), true),
consumed = ConsumedData(positionChange = PxPosition(13f, 0f))
)
val pointerInputFilter: PointerInputFilter =
spy(
TestPointerInputFilter { changes, pass, _ ->
if (changes == listOf(input) &&
pass == PointerEventPass.InitialDown
) {
listOf(output)
} else {
changes
}
}
)
val layoutNode = LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.insertAt(0, layoutNode)
val down = PointerInputEvent(
0,
Uptime.Boot + 3.milliseconds,
PxPosition(0f, 0f),
true
)
val move = PointerInputEvent(
0,
Uptime.Boot + 5.milliseconds,
PxPosition(100f, 0f),
true
)
// Act
pointerInputEventProcessor.process(down)
reset(pointerInputFilter)
pointerInputEventProcessor.process(move)
// Assert
verify(pointerInputFilter)
.onPointerInput(eq(listOf(input)), eq(PointerEventPass.InitialDown), any())
verify(pointerInputFilter)
.onPointerInput(eq(listOf(output)), eq(PointerEventPass.PreUp), any())
}
@Test
fun process_nodesAndAdditionalOffsetIncreasinglyInset_dispatchInfoIsCorrect() {
process_dispatchInfoIsCorrect(
0, 0, 100, 100,
2, 11, 100, 100,
23, 31, 100, 100,
43, 51,
99, 99
)
}
@Test
fun process_nodesAndAdditionalOffsetIncreasinglyOutset_dispatchInfoIsCorrect() {
process_dispatchInfoIsCorrect(
0, 0, 100, 100,
-2, -11, 100, 100,
-23, -31, 100, 100,
-43, -51,
1, 1
)
}
@Test
fun process_nodesAndAdditionalOffsetNotOffset_dispatchInfoIsCorrect() {
process_dispatchInfoIsCorrect(
0, 0, 100, 100,
0, 0, 100, 100,
0, 0, 100, 100,
0, 0,
50, 50
)
}
@Suppress("SameParameterValue")
private fun process_dispatchInfoIsCorrect(
pX1: Int,
pY1: Int,
pX2: Int,
pY2: Int,
mX1: Int,
mY1: Int,
mX2: Int,
mY2: Int,
cX1: Int,
cY1: Int,
cX2: Int,
cY2: Int,
aOX: Int,
aOY: Int,
pointerX: Int,
pointerY: Int
) {
// Arrange
val childPointerInputFilter: PointerInputFilter = spy()
val middlePointerInputFilter: PointerInputFilter = spy()
val parentPointerInputFilter: PointerInputFilter = spy()
val childOffset = PxPosition(cX1.toFloat(), cY1.toFloat())
val childLayoutNode = LayoutNode(
cX1, cY1, cX2, cY2,
PointerInputModifierImpl(
childPointerInputFilter
)
)
val middleOffset = PxPosition(mX1.toFloat(), mY1.toFloat())
val middleLayoutNode: LayoutNode = LayoutNode(
mX1, mY1, mX2, mY2,
PointerInputModifierImpl(
middlePointerInputFilter
)
).apply {
insertAt(0, childLayoutNode)
}
val parentLayoutNode: LayoutNode = LayoutNode(
pX1, pY1, pX2, pY2,
PointerInputModifierImpl(
parentPointerInputFilter
)
).apply {
insertAt(0, middleLayoutNode)
}
testOwner.position = IntPxPosition(aOX.ipx, aOY.ipx)
root.insertAt(0, parentLayoutNode)
val additionalOffset = IntPxPosition(aOX.ipx, aOY.ipx)
val offset = PxPosition(pointerX.toFloat(), pointerY.toFloat())
val down = PointerInputEvent(0, Uptime.Boot + 7.milliseconds, offset, true)
val pointerInputHandlers = arrayOf(
parentPointerInputFilter,
middlePointerInputFilter,
childPointerInputFilter
)
val expectedPointerInputChanges = arrayOf(
PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 7.milliseconds,
offset - additionalOffset,
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
),
PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 7.milliseconds,
offset - middleOffset - additionalOffset,
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
),
PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 7.milliseconds,
offset - middleOffset - childOffset - additionalOffset,
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
)
val expectedSizes = arrayOf(
IntPxSize(pX2.ipx - pX1.ipx, pY2.ipx - pY1.ipx),
IntPxSize(mX2.ipx - mX1.ipx, mY2.ipx - mY1.ipx),
IntPxSize(cX2.ipx - cX1.ipx, cY2.ipx - cY1.ipx)
)
// Act
pointerInputEventProcessor.process(down)
// Assert
// Verify call count
pointerInputHandlers.forEach {
verify(it, times(PointerEventPass.values().size)).onPointerInput(
any(),
any(),
any()
)
}
// Verify call values
for (pass in PointerEventPass.values()) {
for (i in pointerInputHandlers.indices) {
verify(pointerInputHandlers[i]).onPointerInput(
listOf(expectedPointerInputChanges[i]),
pass,
expectedSizes[i]
)
}
}
}
/**
* This test creates a layout of this shape:
*
* -------------
* | | |
* | t | |
* | | |
* |-----| |
* | |
* | |-----|
* | | |
* | | t |
* | | |
* -------------
*
* Where there is one child in the top right, and one in the bottom left, and 2 down touches,
* one in the top left and one in the bottom right.
*/
@Test
fun process_2DownOn2DifferentPointerNodes_hitAndDispatchInfoAreCorrect() {
// Arrange
val childPointerInputFilter1: PointerInputFilter = spy()
val childPointerInputFilter2: PointerInputFilter = spy()
val childLayoutNode1 =
LayoutNode(
0, 0, 50, 50,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 =
LayoutNode(
50, 50, 100, 100,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
root.apply {
insertAt(0, childLayoutNode1)
insertAt(0, childLayoutNode2)
}
val offset1 = PxPosition(25f, 25f)
val offset2 = PxPosition(75f, 75f)
val down = PointerInputEvent(
Uptime.Boot + 5.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 5.milliseconds, offset1, true),
PointerInputEventData(1, Uptime.Boot + 5.milliseconds, offset2, true)
)
)
val expectedChange1 = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 5.milliseconds, offset1, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange2 = PointerInputChange(
id = PointerId(1),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
offset2 - PxPosition(50f, 50f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
// Assert
// Verify call count
verify(childPointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(childPointerInputFilter1)
.onPointerInput(
listOf(expectedChange1),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
verify(childPointerInputFilter2)
.onPointerInput(
listOf(expectedChange2),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
}
}
/**
* This test creates a layout of this shape:
*
* ---------------
* | t | |
* | | |
* | |-------| |
* | | t | |
* | | | |
* | | | |
* |--| |-------|
* | | | t |
* | | | |
* | | | |
* | |--| |
* | | |
* ---------------
*
* There are 3 staggered children and 3 down events, the first is on child 1, the second is on
* child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
* child 2.
*/
@Test
fun process_3DownOnOverlappingPointerNodes_hitAndDispatchInfoAreCorrect() {
val childPointerInputFilter1: PointerInputFilter = spy()
val childPointerInputFilter2: PointerInputFilter = spy()
val childPointerInputFilter3: PointerInputFilter = spy()
val childLayoutNode1 = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 = LayoutNode(
50, 50, 150, 150,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
val childLayoutNode3 = LayoutNode(
100, 100, 200, 200,
PointerInputModifierImpl(
childPointerInputFilter3
)
)
root.apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
insertAt(2, childLayoutNode3)
}
val offset1 = PxPosition(25f, 25f)
val offset2 = PxPosition(75f, 75f)
val offset3 = PxPosition(125f, 125f)
val down = PointerInputEvent(
Uptime.Boot + 5.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 5.milliseconds, offset1, true),
PointerInputEventData(1, Uptime.Boot + 5.milliseconds, offset2, true),
PointerInputEventData(2, Uptime.Boot + 5.milliseconds, offset3, true)
)
)
val expectedChange1 = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 5.milliseconds, offset1, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange2 = PointerInputChange(
id = PointerId(1),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
offset2 - PxPosition(50f, 50f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange3 = PointerInputChange(
id = PointerId(2),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
offset3 - PxPosition(100f, 100f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
// Assert
// Verify call count
verify(childPointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter3, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(childPointerInputFilter1)
.onPointerInput(
listOf(expectedChange1),
pointerEventPass,
IntPxSize(100.ipx, 100.ipx)
)
verify(childPointerInputFilter2)
.onPointerInput(
listOf(expectedChange2),
pointerEventPass,
IntPxSize(100.ipx, 100.ipx)
)
verify(childPointerInputFilter3)
.onPointerInput(
listOf(expectedChange3),
pointerEventPass,
IntPxSize(100.ipx, 100.ipx)
)
}
}
/**
* This test creates a layout of this shape:
*
* ---------------
* | |
* | t |
* | |
* | |-------| |
* | | | |
* | | t | |
* | | | |
* | |-------| |
* | |
* | t |
* | |
* ---------------
*
* There are 3 staggered children and 3 down events, the first is on child 1, the second is on
* child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
* child 2.
*/
@Test
fun process_3DownOnFloatingPointerNodeV_hitAndDispatchInfoAreCorrect() {
val childPointerInputFilter1: PointerInputFilter = spy()
val childPointerInputFilter2: PointerInputFilter = spy()
val childLayoutNode1 = LayoutNode(
0, 0, 100, 150,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 = LayoutNode(
25, 50, 75, 100,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
root.apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
}
val offset1 = PxPosition(50f, 25f)
val offset2 = PxPosition(50f, 75f)
val offset3 = PxPosition(50f, 125f)
val down = PointerInputEvent(
Uptime.Boot + 7.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 7.milliseconds, offset1, true),
PointerInputEventData(1, Uptime.Boot + 7.milliseconds, offset2, true),
PointerInputEventData(2, Uptime.Boot + 7.milliseconds, offset3, true)
)
)
val expectedChange1 = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 7.milliseconds, offset1, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange2 = PointerInputChange(
id = PointerId(1),
current = PointerInputData(
Uptime.Boot + 7.milliseconds,
offset2 - PxPosition(25f, 50f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange3 = PointerInputChange(
id = PointerId(2),
current = PointerInputData(Uptime.Boot + 7.milliseconds, offset3, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
// Assert
// Verify call count
verify(childPointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(childPointerInputFilter1)
.onPointerInput(
listOf(expectedChange1, expectedChange3),
pointerEventPass,
IntPxSize(100.ipx, 150.ipx)
)
verify(childPointerInputFilter2)
.onPointerInput(
listOf(expectedChange2),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
}
}
/**
* This test creates a layout of this shape:
*
* -----------------
* | |
* | |-------| |
* | | | |
* | t | t | t |
* | | | |
* | |-------| |
* | |
* -----------------
*
* There are 3 staggered children and 3 down events, the first is on child 1, the second is on
* child 2 in a space that overlaps child 1, and the third is in a space that overlaps both
* child 2.
*/
@Test
fun process_3DownOnFloatingPointerNodeH_hitAndDispatchInfoAreCorrect() {
val childPointerInputFilter1: PointerInputFilter = spy()
val childPointerInputFilter2: PointerInputFilter = spy()
val childLayoutNode1 = LayoutNode(
0, 0, 150, 100,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 = LayoutNode(
50, 25, 100, 75,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
root.apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
}
val offset1 = PxPosition(25f, 50f)
val offset2 = PxPosition(75f, 50f)
val offset3 = PxPosition(125f, 50f)
val down = PointerInputEvent(
Uptime.Boot + 11.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 11.milliseconds, offset1, true),
PointerInputEventData(1, Uptime.Boot + 11.milliseconds, offset2, true),
PointerInputEventData(2, Uptime.Boot + 11.milliseconds, offset3, true)
)
)
val expectedChange1 = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 11.milliseconds, offset1, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange2 = PointerInputChange(
id = PointerId(1),
current = PointerInputData(
Uptime.Boot + 11.milliseconds,
offset2 - PxPosition(50f, 25f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange3 = PointerInputChange(
id = PointerId(2),
current = PointerInputData(Uptime.Boot + 11.milliseconds, offset3, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
// Assert
// Verify call count
verify(childPointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(childPointerInputFilter1)
.onPointerInput(
listOf(expectedChange1, expectedChange3),
pointerEventPass,
IntPxSize(150.ipx, 100.ipx)
)
verify(childPointerInputFilter2)
.onPointerInput(
listOf(expectedChange2),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
}
}
/**
* This test creates a layout of this shape:
* 0 1 2 3 4
* ......... .........
* 0 . t . . t .
* . |---|---|---| .
* 1 . t | t | | t | t .
* ....|---| |---|....
* 2 | |
* ....|---| |---|....
* 3 . t | t | | t | t .
* . |---|---|---| .
* 4 . t . . t .
* ......... .........
*
* 4 LayoutNodes with PointerInputModifiers that are clipped by their parent LayoutNode. 4
* touches touch just inside the parent LayoutNode and inside the child LayoutNodes. 8
* touches touch just outside the parent LayoutNode but inside the child LayoutNodes.
*
* Because LayoutNodes clip the bounds where children LayoutNodes can be hit, all 8 should miss,
* but the other 4 touches are inside both, so hit.
*/
@Test
fun process_4DownInClippedAreaOfLnsWithPims_onlyCorrectPointersHit() {
// Arrange
val pointerInputFilter1: PointerInputFilter = spy()
val pointerInputFilter2: PointerInputFilter = spy()
val pointerInputFilter3: PointerInputFilter = spy()
val pointerInputFilter4: PointerInputFilter = spy()
val layoutNode1 = LayoutNode(
-1, -1, 1, 1,
PointerInputModifierImpl(
pointerInputFilter1
)
)
val layoutNode2 = LayoutNode(
2, -1, 4, 1,
PointerInputModifierImpl(
pointerInputFilter2
)
)
val layoutNode3 = LayoutNode(
-1, 2, 1, 4,
PointerInputModifierImpl(
pointerInputFilter3
)
)
val layoutNode4 = LayoutNode(
2, 2, 4, 4,
PointerInputModifierImpl(
pointerInputFilter4
)
)
val parentLayoutNode = LayoutNode(1, 1, 4, 4).apply {
insertAt(0, layoutNode1)
insertAt(1, layoutNode2)
insertAt(2, layoutNode3)
insertAt(3, layoutNode4)
}
root.apply {
insertAt(0, parentLayoutNode)
}
val offsetsThatHit =
listOf(
PxPosition(1f, 1f),
PxPosition(3f, 1f),
PxPosition(1f, 3f),
PxPosition(3f, 3f)
)
val offsetsThatMiss =
listOf(
PxPosition(1f, 0f),
PxPosition(3f, 0f),
PxPosition(0f, 1f),
PxPosition(4f, 1f),
PxPosition(0f, 3f),
PxPosition(4f, 3f),
PxPosition(1f, 4f),
PxPosition(3f, 4f)
)
val allOffsets = offsetsThatHit + offsetsThatMiss
val pointerInputEvent =
PointerInputEvent(
Uptime.Boot + 11.milliseconds,
(allOffsets.indices).map {
PointerInputEventData(it, Uptime.Boot + 11.milliseconds, allOffsets[it], true)
}
)
// Act
pointerInputEventProcessor.process(pointerInputEvent)
// Assert
val expectedChanges =
(offsetsThatHit.indices).map {
PointerInputChange(
id = PointerId(it.toLong()),
current = PointerInputData(
Uptime.Boot + 11.milliseconds,
PxPosition(
if (offsetsThatHit[it].x == 1f) 1f else 0f,
if (offsetsThatHit[it].y == 1f) 1f else 0f
),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
}
// Verify call count
verify(
pointerInputFilter1,
times(PointerEventPass.values().size)
).onPointerInput(any(), any(), any())
verify(
pointerInputFilter2,
times(PointerEventPass.values().size)
).onPointerInput(any(), any(), any())
verify(
pointerInputFilter3,
times(PointerEventPass.values().size)
).onPointerInput(any(), any(), any())
verify(
pointerInputFilter4,
times(PointerEventPass.values().size)
).onPointerInput(any(), any(), any())
// Verify call values
PointerEventPass.values().forEach { pointerEventPass ->
verify(pointerInputFilter1).onPointerInput(
eq(listOf(expectedChanges[0])),
eq(pointerEventPass),
any()
)
verify(pointerInputFilter2).onPointerInput(
eq(listOf(expectedChanges[1])),
eq(pointerEventPass),
any()
)
verify(pointerInputFilter3).onPointerInput(
eq(listOf(expectedChanges[2])),
eq(pointerEventPass),
any()
)
verify(pointerInputFilter4).onPointerInput(
eq(listOf(expectedChanges[3])),
eq(pointerEventPass),
any()
)
}
}
/**
* This test creates a layout of this shape:
*
* |---|
* |tt |
* |t |
* |---|t
* tt
*
* But where the additional offset suggest something more like this shape.
*
* tt
* t|---|
* | t|
* | tt|
* |---|
*
* Without the additional offset, it would be expected that only the top left 3 pointers would
* hit, but with the additional offset, only the bottom right 3 hit.
*/
@Test
fun process_rootIsOffset_onlyCorrectPointersHit() {
// Arrange
val singlePointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 2, 2,
PointerInputModifierImpl(
singlePointerInputFilter
)
)
root.apply {
insertAt(0, layoutNode)
}
val offsetsThatHit =
listOf(
PxPosition(2f, 2f),
PxPosition(2f, 1f),
PxPosition(1f, 2f)
)
val offsetsThatMiss =
listOf(
PxPosition(0f, 0f),
PxPosition(0f, 1f),
PxPosition(1f, 0f)
)
val allOffsets = offsetsThatHit + offsetsThatMiss
val pointerInputEvent =
PointerInputEvent(
Uptime.Boot + 11.milliseconds,
(allOffsets.indices).map {
PointerInputEventData(it, Uptime.Boot + 11.milliseconds, allOffsets[it], true)
}
)
testOwner.position = IntPxPosition(1.ipx, 1.ipx)
// Act
pointerInputEventProcessor.process(pointerInputEvent)
// Assert
val expectedChanges =
(offsetsThatHit.indices).map {
PointerInputChange(
id = PointerId(it.toLong()),
current = PointerInputData(
Uptime.Boot + 11.milliseconds,
offsetsThatHit[it] - PxPosition(1f, 1f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
}
// Verify call count
verify(singlePointerInputFilter, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
PointerEventPass.values().forEach { pointerEventPass ->
verify(singlePointerInputFilter).onPointerInput(
eq(expectedChanges),
eq(pointerEventPass),
any()
)
}
}
@Test
fun process_downOn3NestedPointerInputModifiers_hitAndDispatchInfoAreCorrect() {
val pointerInputFilter1: PointerInputFilter = spy()
val pointerInputFilter2: PointerInputFilter = spy()
val pointerInputFilter3: PointerInputFilter = spy()
val modifier = PointerInputModifierImpl(pointerInputFilter1) +
PointerInputModifierImpl(pointerInputFilter2) +
PointerInputModifierImpl(pointerInputFilter3)
val layoutNode = LayoutNode(
25, 50, 75, 100,
modifier
)
root.apply {
insertAt(0, layoutNode)
}
val offset1 = PxPosition(50f, 75f)
val down = PointerInputEvent(
Uptime.Boot + 7.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 7.milliseconds, offset1, true)
)
)
val expectedChange = PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 7.milliseconds,
offset1 - PxPosition(25f, 50f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
// Assert
// Verify call count
verify(pointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(pointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(pointerInputFilter3, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(pointerInputFilter1)
.onPointerInput(
listOf(expectedChange),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
verify(pointerInputFilter2)
.onPointerInput(
listOf(expectedChange),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
verify(pointerInputFilter3)
.onPointerInput(
listOf(expectedChange),
pointerEventPass,
IntPxSize(50.ipx, 50.ipx)
)
}
}
@Test
fun process_downOnDeeplyNestedPointerInputModifier_hitAndDispatchInfoAreCorrect() {
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode1 =
LayoutNode(
1, 5, 500, 500,
PointerInputModifierImpl(pointerInputFilter)
)
val layoutNode2: LayoutNode = LayoutNode(2, 6, 500, 500).apply {
insertAt(0, layoutNode1)
}
val layoutNode3: LayoutNode = LayoutNode(3, 7, 500, 500).apply {
insertAt(0, layoutNode2)
}
val layoutNode4: LayoutNode = LayoutNode(4, 8, 500, 500).apply {
insertAt(0, layoutNode3)
}
root.apply {
insertAt(0, layoutNode4)
}
val offset1 = PxPosition(499f, 499f)
val downEvent = PointerInputEvent(
Uptime.Boot + 7.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 7.milliseconds, offset1, true)
)
)
val expectedChange = PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 7.milliseconds,
offset1 - PxPosition(1f + 2f + 3f + 4f, 5f + 6f + 7f + 8f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(downEvent)
// Assert
// Verify call count
verify(pointerInputFilter, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(pointerInputFilter)
.onPointerInput(
listOf(expectedChange),
pointerEventPass,
IntPxSize(499.ipx, 495.ipx)
)
}
}
@Test
fun process_downOnComplexPointerAndLayoutNodePath_hitAndDispatchInfoAreCorrect() {
val pointerInputFilter1: PointerInputFilter = spy()
val pointerInputFilter2: PointerInputFilter = spy()
val pointerInputFilter3: PointerInputFilter = spy()
val pointerInputFilter4: PointerInputFilter = spy()
val layoutNode1 = LayoutNode(
1, 6, 500, 500,
PointerInputModifierImpl(
pointerInputFilter1
) + PointerInputModifierImpl(
pointerInputFilter2
)
)
val layoutNode2: LayoutNode = LayoutNode(2, 7, 500, 500).apply {
insertAt(0, layoutNode1)
}
val layoutNode3 =
LayoutNode(
3, 8, 500, 500,
PointerInputModifierImpl(pointerInputFilter3) +
PointerInputModifierImpl(pointerInputFilter4)
).apply {
insertAt(0, layoutNode2)
}
val layoutNode4: LayoutNode = LayoutNode(4, 9, 500, 500).apply {
insertAt(0, layoutNode3)
}
val layoutNode5: LayoutNode = LayoutNode(5, 10, 500, 500).apply {
insertAt(0, layoutNode4)
}
root.apply {
insertAt(0, layoutNode5)
}
val offset1 = PxPosition(499f, 499f)
val downEvent = PointerInputEvent(
Uptime.Boot + 3.milliseconds,
listOf(
PointerInputEventData(0, Uptime.Boot + 3.milliseconds, offset1, true)
)
)
val expectedChange1 = PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 3.milliseconds,
offset1 - PxPosition(
1f + 2f + 3f + 4f + 5f,
6f + 7f + 8f + 9f + 10f
),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange2 = PointerInputChange(
id = PointerId(0),
current = PointerInputData(
Uptime.Boot + 3.milliseconds,
offset1 - PxPosition(3f + 4f + 5f, 8f + 9f + 10f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(downEvent)
// Assert
// Verify call count
verify(pointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(pointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(pointerInputFilter3, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(pointerInputFilter4, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
PointerEventPass.values()
// Verify call values
for (pointerEventPass in PointerEventPass.values()) {
verify(pointerInputFilter1)
.onPointerInput(
listOf(expectedChange1),
pointerEventPass,
IntPxSize(499.ipx, 494.ipx)
)
verify(pointerInputFilter2)
.onPointerInput(
listOf(expectedChange1),
pointerEventPass,
IntPxSize(499.ipx, 494.ipx)
)
verify(pointerInputFilter3)
.onPointerInput(
listOf(expectedChange2),
pointerEventPass,
IntPxSize(497.ipx, 492.ipx)
)
verify(pointerInputFilter4)
.onPointerInput(
listOf(expectedChange2),
pointerEventPass,
IntPxSize(497.ipx, 492.ipx)
)
}
}
@Test
fun process_downOnFullyOverlappingPointerInputModifiers_onlyTopPointerInputModifierReceives() {
val pointerInputFilter1: PointerInputFilter = spy()
val pointerInputFilter2: PointerInputFilter = spy()
val layoutNode1 = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
pointerInputFilter1
)
)
val layoutNode2 = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
pointerInputFilter2
)
)
root.apply {
insertAt(0, layoutNode1)
insertAt(1, layoutNode2)
}
val down = PointerInputEvent(
1, Uptime.Boot + 0.milliseconds, PxPosition(50f, 50f), true
)
// Act
pointerInputEventProcessor.process(down)
// Assert
verify(pointerInputFilter2, times(5)).onPointerInput(any(), any(), any())
verify(pointerInputFilter1, never()).onPointerInput(any(), any(), any())
}
@Test
fun process_downOnPointerInputModifierInLayoutNodeWithNoSize_downNotReceived() {
val pointerInputFilter1: PointerInputFilter = spy()
val layoutNode1 = LayoutNode(
0, 0, 0, 0,
PointerInputModifierImpl(pointerInputFilter1)
)
root.apply {
insertAt(0, layoutNode1)
}
val down = PointerInputEvent(
1, Uptime.Boot + 0.milliseconds, PxPosition(0f, 0f), true
)
// Act
pointerInputEventProcessor.process(down)
// Assert
verify(pointerInputFilter1, never()).onPointerInput(any(), any(), any())
}
// Cancel Handlers
@Test
fun processCancel_noPointers_doesntCrash() {
pointerInputEventProcessor.processCancel()
}
@Test
fun processCancel_downThenCancel_pimOnlyReceivesCorrectDownThenCancel() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(pointerInputFilter)
)
root.insertAt(0, layoutNode)
val pointerInputEvent =
PointerInputEvent(
7,
Uptime.Boot + 5.milliseconds,
PxPosition(250f, 250f),
true
)
val expectedChange =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(250f, 250f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(pointerInputEvent)
pointerInputEventProcessor.processCancel()
// Assert
// Verify call count
verify(pointerInputFilter, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expectedChange)),
eq(pass),
any()
)
}
verify(pointerInputFilter).onCancel()
}
}
@Test
fun processCancel_downDownOnSamePimThenCancel_pimOnlyReceivesCorrectChangesThenCancel() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.insertAt(0, layoutNode)
val pointerInputEvent1 =
PointerInputEvent(
7,
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
)
val pointerInputEvent2 =
PointerInputEvent(
Uptime.Boot + 10.milliseconds,
listOf(
PointerInputEventData(
7,
Uptime.Boot + 10.milliseconds,
PxPosition(200f, 200f),
true
),
PointerInputEventData(
9,
Uptime.Boot + 10.milliseconds,
PxPosition(300f, 300f),
true
)
)
)
val expectedChanges1 =
listOf(
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
)
val expectedChanges2 =
listOf(
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 10.milliseconds,
PxPosition(200f, 200f),
true
),
previous = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
),
consumed = ConsumedData()
),
PointerInputChange(
id = PointerId(9),
current = PointerInputData(
Uptime.Boot + 10.milliseconds,
PxPosition(300f, 300f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
)
// Act
pointerInputEventProcessor.process(pointerInputEvent1)
pointerInputEventProcessor.process(pointerInputEvent2)
pointerInputEventProcessor.processCancel()
// Assert
// Verify call count
verify(pointerInputFilter, times(PointerEventPass.values().size * 2))
.onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(expectedChanges1),
eq(pass),
any()
)
}
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(expectedChanges2),
eq(pass),
any()
)
}
verify(pointerInputFilter).onCancel()
}
}
@Test
fun processCancel_downOn2DifferentPimsThenCancel_pimsOnlyReceiveCorrectDownsThenCancel() {
// Arrange
val pointerInputFilter1: PointerInputFilter = spy()
val layoutNode1 = LayoutNode(
0, 0, 199, 199,
PointerInputModifierImpl(pointerInputFilter1)
)
val pointerInputFilter2: PointerInputFilter = spy()
val layoutNode2 = LayoutNode(
200, 200, 399, 399,
PointerInputModifierImpl(pointerInputFilter2)
)
root.insertAt(0, layoutNode1)
root.insertAt(1, layoutNode2)
val pointerInputEventData1 =
PointerInputEventData(
7,
Uptime.Boot + 5.milliseconds,
PxPosition(100f, 100f),
true
)
val pointerInputEventData2 =
PointerInputEventData(
9,
Uptime.Boot + 5.milliseconds,
PxPosition(300f, 300f),
true
)
val pointerInputEvent = PointerInputEvent(
Uptime.Boot + 5.milliseconds,
listOf(pointerInputEventData1, pointerInputEventData2)
)
val expectedChange1 =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(100f, 100f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedChange2 =
PointerInputChange(
id = PointerId(9),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(100f, 100f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(pointerInputEvent)
pointerInputEventProcessor.processCancel()
// Assert
// Verify call count
verify(pointerInputFilter1, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
verify(pointerInputFilter2, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter1) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter1).onPointerInput(
eq(listOf(expectedChange1)),
eq(pass),
any()
)
}
verify(pointerInputFilter1).onCancel()
}
inOrder(pointerInputFilter2) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter2).onPointerInput(
eq(listOf(expectedChange2)),
eq(pass),
any()
)
}
verify(pointerInputFilter2).onCancel()
}
}
@Test
fun processCancel_downMoveCancel_pimOnlyReceivesCorrectDownMoveCancel() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(pointerInputFilter)
)
root.insertAt(0, layoutNode)
val down =
PointerInputEvent(
7,
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
)
val move =
PointerInputEvent(
7,
Uptime.Boot + 10.milliseconds,
PxPosition(300f, 300f),
true
)
val expectedDown =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedMove =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 10.milliseconds,
PxPosition(300f, 300f),
true
),
previous = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
pointerInputEventProcessor.process(move)
pointerInputEventProcessor.processCancel()
// Assert
// Verify call count
verify(pointerInputFilter, times(PointerEventPass.values().size * 2))
.onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expectedDown)),
eq(pass),
any()
)
}
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expectedMove)),
eq(pass),
any()
)
}
verify(pointerInputFilter).onCancel()
}
}
@Test
fun processCancel_downCancelMoveUp_pimOnlyReceivesCorrectDownCancel() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(pointerInputFilter)
)
root.insertAt(0, layoutNode)
val down =
PointerInputEvent(
7,
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
)
val expectedDown =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
pointerInputEventProcessor.processCancel()
// Assert
// Verify call count
verify(pointerInputFilter, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expectedDown)),
eq(pass),
any()
)
}
verify(pointerInputFilter).onCancel()
}
}
@Test
fun processCancel_downCancelDown_pimOnlyReceivesCorrectDownCancelDown() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.insertAt(0, layoutNode)
val down1 =
PointerInputEvent(
7,
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
)
val down2 =
PointerInputEvent(
7,
Uptime.Boot + 10.milliseconds,
PxPosition(200f, 200f),
true
)
val expectedDown1 =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 5.milliseconds,
PxPosition(200f, 200f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedDown2 =
PointerInputChange(
id = PointerId(7),
current = PointerInputData(
Uptime.Boot + 10.milliseconds,
PxPosition(200f, 200f),
true
),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down1)
pointerInputEventProcessor.processCancel()
pointerInputEventProcessor.process(down2)
// Assert
// Verify call count
verify(pointerInputFilter, times(PointerEventPass.values().size * 2))
.onPointerInput(any(), any(), any())
// Verify call values
inOrder(pointerInputFilter) {
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expectedDown1)),
eq(pass),
any()
)
}
verify(pointerInputFilter).onCancel()
for (pass in PointerEventPass.values()) {
verify(pointerInputFilter).onPointerInput(
eq(listOf(expectedDown2)),
eq(pass),
any()
)
}
}
}
@Test
fun process_layoutNodeRemovedDuringInput_correctPointerInputChangesReceived() {
// Arrange
val childPointerInputFilter: PointerInputFilter = spy()
val childLayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(childPointerInputFilter)
)
val parentPointerInputFilter: PointerInputFilter = spy()
val parentLayoutNode: LayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(parentPointerInputFilter)
).apply {
insertAt(0, childLayoutNode)
}
root.insertAt(0, parentLayoutNode)
val offset = PxPosition(50f, 50f)
val down = PointerInputEvent(0, Uptime.Boot + 7.milliseconds, offset, true)
val up = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, null, false)
val expectedDownChange = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 7.milliseconds, offset, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedUpChange = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 11.milliseconds, null, false),
previous = PointerInputData(Uptime.Boot + 7.milliseconds, offset, true),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
parentLayoutNode.removeAt(0, 1)
pointerInputEventProcessor.process(up)
// Assert
// Verify call count
verify(parentPointerInputFilter, times(PointerEventPass.values().size * 2))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
PointerEventPass.values().forEach {
verify(parentPointerInputFilter)
.onPointerInput(eq(listOf(expectedDownChange)), eq(it), any())
verify(childPointerInputFilter)
.onPointerInput(eq(listOf(expectedDownChange)), eq(it), any())
verify(parentPointerInputFilter)
.onPointerInput(eq(listOf(expectedUpChange)), eq(it), any())
}
}
@Test
fun process_layoutNodeRemovedDuringInput_cancelDispatchedToCorrectPointerInputModifierImpl() {
// Arrange
val childPointerInputFilter: PointerInputFilter = spy()
val childLayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(childPointerInputFilter)
)
val parentPointerInputFilter: PointerInputFilter = spy()
val parentLayoutNode: LayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(parentPointerInputFilter)
).apply {
insertAt(0, childLayoutNode)
}
root.insertAt(0, parentLayoutNode)
val down =
PointerInputEvent(0, Uptime.Boot + 7.milliseconds, PxPosition(50f, 50f), true)
val up = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, null, false)
// Act
pointerInputEventProcessor.process(down)
parentLayoutNode.removeAt(0, 1)
pointerInputEventProcessor.process(up)
// Assert
verify(childPointerInputFilter).onCancel()
verify(parentPointerInputFilter, never()).onCancel()
}
@Test
fun process_pointerInputModifierRemovedDuringInput_correctPointerInputChangesReceived() {
// Arrange
val childPointerInputFilter: PointerInputFilter = spy()
val childLayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
childPointerInputFilter
)
)
val parentPointerInputFilter: PointerInputFilter = spy()
val parentLayoutNode: LayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
parentPointerInputFilter
)
).apply {
insertAt(0, childLayoutNode)
}
root.insertAt(0, parentLayoutNode)
val offset = PxPosition(50f, 50f)
val down = PointerInputEvent(0, Uptime.Boot + 7.milliseconds, offset, true)
val up = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, null, false)
val expectedDownChange = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 7.milliseconds, offset, true),
previous = PointerInputData(null, null, false),
consumed = ConsumedData()
)
val expectedUpChange = PointerInputChange(
id = PointerId(0),
current = PointerInputData(Uptime.Boot + 11.milliseconds, null, false),
previous = PointerInputData(Uptime.Boot + 7.milliseconds, offset, true),
consumed = ConsumedData()
)
// Act
pointerInputEventProcessor.process(down)
childLayoutNode.modifier = Modifier
pointerInputEventProcessor.process(up)
// Assert
// Verify call count
verify(parentPointerInputFilter, times(PointerEventPass.values().size * 2))
.onPointerInput(any(), any(), any())
verify(childPointerInputFilter, times(PointerEventPass.values().size))
.onPointerInput(any(), any(), any())
// Verify call values
PointerEventPass.values().forEach {
verify(parentPointerInputFilter)
.onPointerInput(eq(listOf(expectedDownChange)), eq(it), any())
verify(childPointerInputFilter)
.onPointerInput(eq(listOf(expectedDownChange)), eq(it), any())
verify(parentPointerInputFilter)
.onPointerInput(eq(listOf(expectedUpChange)), eq(it), any())
}
}
@Test
fun process_pointerInputModifierRemovedDuringInput_cancelDispatchedToCorrectPim() {
// Arrange
val childPointerInputFilter: PointerInputFilter = spy()
val childLayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(childPointerInputFilter)
)
val parentPointerInputFilter: PointerInputFilter = spy()
val parentLayoutNode: LayoutNode = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(parentPointerInputFilter)
).apply {
insertAt(0, childLayoutNode)
}
root.insertAt(0, parentLayoutNode)
val down =
PointerInputEvent(0, Uptime.Boot + 7.milliseconds, PxPosition(50f, 50f), true)
val up = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, null, false)
// Act
pointerInputEventProcessor.process(down)
childLayoutNode.modifier = Modifier
pointerInputEventProcessor.process(up)
// Assert
verify(childPointerInputFilter).onCancel()
verify(parentPointerInputFilter, never()).onCancel()
}
@Test
fun process_downNoPointerInputModifiers_nothingInteractedWithAndNoMovementConsumed() {
val pointerInputEvent =
PointerInputEvent(0, Uptime.Boot + 7.milliseconds, PxPosition(0f, 0f), true)
val result: ProcessResult = pointerInputEventProcessor.process(pointerInputEvent)
assertThat(result).isEqualTo(
ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = false
)
)
}
@Test
fun process_downNoPointerInputModifiersHit_nothingInteractedWithAndNoMovementConsumed() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.apply {
insertAt(0, layoutNode)
}
val offsets =
listOf(
PxPosition(-1f, 0f),
PxPosition(0f, -1f),
PxPosition(1f, 0f),
PxPosition(0f, 1f)
)
val pointerInputEvent =
PointerInputEvent(
Uptime.Boot + 11.milliseconds,
(offsets.indices).map {
PointerInputEventData(it, Uptime.Boot + 11.milliseconds, offsets[it], true)
}
)
// Act
val result: ProcessResult = pointerInputEventProcessor.process(pointerInputEvent)
// Assert
assertThat(result).isEqualTo(
ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = false
)
)
}
@Test
fun process_downPointerInputModifierHit_somethingInteractedWithAndNoMovementConsumed() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.apply { insertAt(0, layoutNode) }
val pointerInputEvent =
PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(0f, 0f), true)
// Act
val result = pointerInputEventProcessor.process(pointerInputEvent)
// Assert
assertThat(result).isEqualTo(
ProcessResult(
dispatchedToAPointerInputModifier = true,
anyMovementConsumed = false
)
)
}
@Test
fun process_downHitsPifRemovedPointerMoves_nothingInteractedWithAndNoMovementConsumed() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.apply { insertAt(0, layoutNode) }
val down = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(0f, 0f), true)
pointerInputEventProcessor.process(down)
val move = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(1f, 0f), true)
// Act
root.removeAt(0, 1)
val result = pointerInputEventProcessor.process(move)
// Assert
assertThat(result).isEqualTo(
ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = false
)
)
}
@Test
fun process_downHitsPointerMovesNothingConsumed_somethingInteractedWithAndNoMovementConsumed() {
// Arrange
val pointerInputFilter: PointerInputFilter = spy()
val layoutNode = LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.apply { insertAt(0, layoutNode) }
val down = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(0f, 0f), true)
pointerInputEventProcessor.process(down)
val move = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(1f, 0f), true)
// Act
val result = pointerInputEventProcessor.process(move)
// Assert
assertThat(result).isEqualTo(
ProcessResult(
dispatchedToAPointerInputModifier = true,
anyMovementConsumed = false
)
)
}
@Test
fun process_downHitsPointerMovementConsumed_somethingInteractedWithAndMovementConsumed() {
// Arrange
val pointerInputFilter: PointerInputFilter =
spy(
TestPointerInputFilter { changes, pass, _ ->
if (pass == PointerEventPass.InitialDown) {
changes.map { it.consumePositionChange(1f, 0f) }
} else {
changes
}
}
)
val layoutNode = LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(
pointerInputFilter
)
)
root.apply { insertAt(0, layoutNode) }
val down = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(0f, 0f), true)
pointerInputEventProcessor.process(down)
val move = PointerInputEvent(0, Uptime.Boot + 11.milliseconds, PxPosition(1f, 0f), true)
// Act
val result = pointerInputEventProcessor.process(move)
// Assert
assertThat(result).isEqualTo(
ProcessResult(
dispatchedToAPointerInputModifier = true,
anyMovementConsumed = true
)
)
}
@Test
fun processResult_trueTrue_propValuesAreCorrect() {
val processResult1 = ProcessResult(
dispatchedToAPointerInputModifier = true,
anyMovementConsumed = true
)
assertThat(processResult1.dispatchedToAPointerInputModifier).isTrue()
assertThat(processResult1.anyMovementConsumed).isTrue()
}
@Test
fun processResult_trueFalse_propValuesAreCorrect() {
val processResult1 = ProcessResult(
dispatchedToAPointerInputModifier = true,
anyMovementConsumed = false
)
assertThat(processResult1.dispatchedToAPointerInputModifier).isTrue()
assertThat(processResult1.anyMovementConsumed).isFalse()
}
@Test
fun processResult_falseTrue_propValuesAreCorrect() {
val processResult1 = ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = true
)
assertThat(processResult1.dispatchedToAPointerInputModifier).isFalse()
assertThat(processResult1.anyMovementConsumed).isTrue()
}
@Test
fun processResult_falseFalse_propValuesAreCorrect() {
val processResult1 = ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = false
)
assertThat(processResult1.dispatchedToAPointerInputModifier).isFalse()
assertThat(processResult1.anyMovementConsumed).isFalse()
}
}
abstract class TestOwner : Owner {
var position = IntPxPosition.Origin
override val root: LayoutNode
get() = LayoutNode()
override fun calculatePosition() = position
}
open class TestPointerInputFilter(
val pointerInputHandler: PointerInputHandler = { changes, _, _ -> changes }
) : PointerInputFilter() {
override fun onPointerInput(
changes: List<PointerInputChange>,
pass: PointerEventPass,
bounds: IntPxSize
): List<PointerInputChange> {
return pointerInputHandler(changes, pass, bounds)
}
override fun onCancel() {}
}
private class PointerInputModifierImpl(override val pointerInputFilter: PointerInputFilter) :
PointerInputModifier