| /* |
| * Copyright 2019 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.DrawNode |
| import androidx.ui.core.IntPxPosition |
| import androidx.ui.core.IntPxSize |
| import androidx.ui.core.LayoutNode |
| import androidx.ui.core.Owner |
| import androidx.ui.core.PointerEventPass |
| import androidx.ui.core.PointerEventPass.InitialDown |
| import androidx.ui.core.PointerEventPass.PreUp |
| import androidx.ui.core.PointerInputChange |
| import androidx.ui.core.PointerInputData |
| import androidx.ui.core.PointerInputNode |
| import androidx.ui.core.PxPosition |
| import androidx.ui.core.SemanticsComponentNode |
| import androidx.ui.core.ipx |
| import androidx.ui.core.millisecondsToTimestamp |
| import androidx.ui.core.px |
| import com.nhaarman.mockitokotlin2.any |
| import com.nhaarman.mockitokotlin2.eq |
| import com.nhaarman.mockitokotlin2.inOrder |
| import com.nhaarman.mockitokotlin2.never |
| import com.nhaarman.mockitokotlin2.spy |
| import com.nhaarman.mockitokotlin2.times |
| import com.nhaarman.mockitokotlin2.verify |
| import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions |
| import com.nhaarman.mockitokotlin2.whenever |
| import org.junit.Before |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.junit.runners.JUnit4 |
| import org.mockito.Mockito.mock |
| |
| // 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 mockOwner = mock(Owner::class.java) |
| private lateinit var mTrackerList: |
| MutableList<Triple<PointerInputNode, PointerEventPass, PointerInputChange>> |
| |
| @Before |
| fun setup() { |
| root = LayoutNode(0, 0, 500, 500) |
| root.attach(mockOwner) |
| pointerInputEventProcessor = PointerInputEventProcessor(root) |
| mTrackerList = mutableListOf() |
| } |
| |
| @Test |
| fun process_downMoveUp_convertedCorrectlyAndTraversesAllPassesInCorrectOrder() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val offset = PxPosition(100.px, 200.px) |
| val offset2 = PxPosition(300.px, 400.px) |
| |
| val events = arrayOf( |
| PointerInputEvent(8712, 3L.millisecondsToTimestamp(), offset, true), |
| PointerInputEvent(8712, 11L.millisecondsToTimestamp(), offset2, true), |
| PointerInputEvent(8712, 13L.millisecondsToTimestamp(), offset2, false) |
| ) |
| |
| val expectedChanges = arrayOf( |
| PointerInputChange( |
| id = 8712, |
| current = PointerInputData(3L.millisecondsToTimestamp(), offset, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ), |
| PointerInputChange( |
| id = 8712, |
| current = PointerInputData(11L.millisecondsToTimestamp(), offset2, true), |
| previous = PointerInputData(3L.millisecondsToTimestamp(), offset, true), |
| consumed = ConsumedData() |
| ), |
| PointerInputChange( |
| id = 8712, |
| current = PointerInputData(13L.millisecondsToTimestamp(), offset2, false), |
| previous = PointerInputData(11L.millisecondsToTimestamp(), offset2, true), |
| consumed = ConsumedData() |
| ) |
| ) |
| |
| // Act |
| |
| events.forEach { pointerInputEventProcessor.process(it, IntPxPosition.Origin) } |
| |
| // Assert |
| |
| inOrder(pointerInputNode.pointerInputHandler) { |
| for (expected in expectedChanges) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expected)), |
| eq(pass), |
| any() |
| ) |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun process_downHits_targetReceives() { |
| |
| // Arrange |
| |
| val childOffset = PxPosition(100.px, 200.px) |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(100, 200, 301, 401)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val offsets = arrayOf( |
| PxPosition(100.px, 200.px), |
| PxPosition(300.px, 200.px), |
| PxPosition(100.px, 400.px), |
| PxPosition(300.px, 400.px) |
| ) |
| |
| val events = Array(4) { index -> |
| PointerInputEvent(index, 5L.millisecondsToTimestamp(), offsets[index], true) |
| } |
| |
| val expectedChanges = Array(4) { index -> |
| PointerInputChange( |
| id = index, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| offsets[index] - childOffset, |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| |
| // Act |
| |
| events.forEach { |
| pointerInputEventProcessor.process(it, IntPxPosition.Origin) |
| } |
| |
| // Assert |
| |
| verify(pointerInputNode.pointerInputHandler, times(4)).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| for (expected in expectedChanges) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expected)), |
| eq(InitialDown), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun process_downMisses_targetDoesNotReceive() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(100, 200, 301, 401)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val offsets = arrayOf( |
| PxPosition(99.px, 200.px), |
| PxPosition(99.px, 400.px), |
| PxPosition(100.px, 199.px), |
| PxPosition(100.px, 401.px), |
| PxPosition(300.px, 199.px), |
| PxPosition(300.px, 401.px), |
| PxPosition(301.px, 200.px), |
| PxPosition(301.px, 400.px) |
| ) |
| |
| val events = Array(8) { index -> |
| PointerInputEvent(index, 0L.millisecondsToTimestamp(), offsets[index], true) |
| } |
| |
| // Act |
| |
| events.forEach { |
| pointerInputEventProcessor.process(it, IntPxPosition.Origin) |
| } |
| |
| // Assert |
| |
| verify(pointerInputNode.pointerInputHandler, never()).invoke(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 childLayoutNode = LayoutNode(100, 100, 200, 200) |
| val childPointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, childLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val middleLayoutNode: LayoutNode = LayoutNode(100, 100, 400, 400).apply { |
| emitInsertAt(0, childPointerInputNode) |
| } |
| val middlePointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, middleLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val parentLayoutNode: LayoutNode = LayoutNode(0, 0, 500, 500).apply { |
| emitInsertAt(0, middlePointerInputNode) |
| } |
| val parentPointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, parentLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.emitInsertAt(0, parentPointerInputNode) |
| |
| val offset = when (numberOfChildrenHit) { |
| 3 -> PxPosition(250.px, 250.px) |
| 2 -> PxPosition(150.px, 150.px) |
| 1 -> PxPosition(50.px, 50.px) |
| else -> throw IllegalStateException() |
| } |
| |
| val event = PointerInputEvent(0, 5L.millisecondsToTimestamp(), offset, true) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(event, IntPxPosition.Origin) |
| |
| // Assert |
| |
| when (numberOfChildrenHit) { |
| 3 -> { |
| verify(parentPointerInputNode.pointerInputHandler).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| verify(middlePointerInputNode.pointerInputHandler).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| verify(childPointerInputNode.pointerInputHandler).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| } |
| 2 -> { |
| verify(parentPointerInputNode.pointerInputHandler).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| verify(middlePointerInputNode.pointerInputHandler).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| verify(childPointerInputNode.pointerInputHandler, never()).invoke( |
| any(), |
| any(), |
| any() |
| ) |
| } |
| 1 -> { |
| verify(parentPointerInputNode.pointerInputHandler).invoke( |
| any(), |
| eq(InitialDown), |
| any() |
| ) |
| verify(middlePointerInputNode.pointerInputHandler, never()).invoke( |
| any(), |
| any(), |
| any() |
| ) |
| verify(childPointerInputNode.pointerInputHandler, never()).invoke( |
| any(), |
| any(), |
| any() |
| ) |
| } |
| else -> throw IllegalStateException() |
| } |
| } |
| |
| @Test |
| fun process_modifiedChange_isPassedToNext() { |
| |
| // Arrange |
| |
| val input = PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(100.px, 0.px), |
| true |
| ), |
| previous = PointerInputData(3L.millisecondsToTimestamp(), PxPosition(0.px, 0.px), true), |
| consumed = ConsumedData(positionChange = PxPosition(0.px, 0.px)) |
| ) |
| val output = PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(100.px, 0.px), |
| true |
| ), |
| previous = PointerInputData(3L.millisecondsToTimestamp(), PxPosition(0.px, 0.px), true), |
| consumed = ConsumedData(positionChange = PxPosition(13.px, 0.px)) |
| ) |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| whenever( |
| pointerInputHandler.invoke( |
| listOf(input), |
| InitialDown, |
| IntPxSize(500.ipx, 500.ipx) |
| ) |
| ) |
| .thenReturn( |
| listOf( |
| output |
| ) |
| ) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val down = PointerInputEvent( |
| 0, |
| 3L.millisecondsToTimestamp(), |
| PxPosition(0.px, 0.px), |
| true |
| ) |
| val move = PointerInputEvent( |
| 0, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(100.px, 0.px), |
| true |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| pointerInputEventProcessor.process(move, IntPxPosition.Origin) |
| |
| // Assert |
| |
| verify(pointerInputNode.pointerInputHandler) |
| .invoke(eq(listOf(input)), eq(InitialDown), any()) |
| verify(pointerInputNode.pointerInputHandler) |
| .invoke(eq(listOf(output)), eq(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 childOffset = PxPosition(cX1.px, cY1.px) |
| val childLayoutNode = LayoutNode(cX1, cY1, cX2, cY2) |
| val childPointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, childLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val middleOffset = PxPosition(mX1.px, mY1.px) |
| val middleLayoutNode: LayoutNode = LayoutNode(mX1, mY1, mX2, mY2).apply { |
| emitInsertAt(0, childPointerInputNode) |
| } |
| val middlePointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, middleLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val parentLayoutNode: LayoutNode = LayoutNode(pX1, pY1, pX2, pY2).apply { |
| emitInsertAt(0, middlePointerInputNode) |
| } |
| val parentPointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, parentLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.emitInsertAt(0, parentPointerInputNode) |
| |
| val additionalOffset = IntPxPosition(aOX.ipx, aOY.ipx) |
| |
| val offset = PxPosition(pointerX.px, pointerY.px) |
| |
| val down = PointerInputEvent(0, 7L.millisecondsToTimestamp(), offset, true) |
| |
| val pointerInputNodes = arrayOf( |
| parentPointerInputNode, |
| middlePointerInputNode, |
| childPointerInputNode |
| ) |
| |
| val expectedPointerInputChanges = arrayOf( |
| PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 7L.millisecondsToTimestamp(), |
| offset - additionalOffset, |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ), |
| PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 7L.millisecondsToTimestamp(), |
| offset - middleOffset - additionalOffset, |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ), |
| PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 7L.millisecondsToTimestamp(), |
| 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, additionalOffset) |
| |
| // Assert |
| |
| for (pass in PointerEventPass.values()) { |
| for (i in pointerInputNodes.indices) { |
| verify(pointerInputNodes[i].pointerInputHandler).invoke( |
| 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 childPointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 50, 50)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(50, 50, 100, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, childPointerInputNode1) |
| emitInsertAt(0, childPointerInputNode2) |
| } |
| |
| val offset1 = PxPosition(25.px, 25.px) |
| val offset2 = PxPosition(75.px, 75.px) |
| |
| val down = PointerInputEvent( |
| 5L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 5L.millisecondsToTimestamp(), offset1, true), |
| PointerInputEventData(1, 5L.millisecondsToTimestamp(), offset2, true) |
| ) |
| ) |
| |
| val expectedChange1 = PointerInputChange( |
| id = 0, |
| current = PointerInputData(5L.millisecondsToTimestamp(), offset1, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange2 = PointerInputChange( |
| id = 1, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| offset2 - PxPosition(50.px, 50.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(childPointerInputNode1.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange1), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| verify(childPointerInputNode2.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange2), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| } |
| verifyNoMoreInteractions( |
| childPointerInputNode1.pointerInputHandler, |
| childPointerInputNode2.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * 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 childPointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 100, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(50, 50, 150, 150)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode3: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(100, 100, 200, 200)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, childPointerInputNode1) |
| emitInsertAt(1, childPointerInputNode2) |
| emitInsertAt(2, childPointerInputNode3) |
| } |
| |
| val offset1 = PxPosition(25.px, 25.px) |
| val offset2 = PxPosition(75.px, 75.px) |
| val offset3 = PxPosition(125.px, 125.px) |
| |
| val down = PointerInputEvent( |
| 5L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 5L.millisecondsToTimestamp(), offset1, true), |
| PointerInputEventData(1, 5L.millisecondsToTimestamp(), offset2, true), |
| PointerInputEventData(2, 5L.millisecondsToTimestamp(), offset3, true) |
| ) |
| ) |
| |
| val expectedChange1 = PointerInputChange( |
| id = 0, |
| current = PointerInputData(5L.millisecondsToTimestamp(), offset1, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange2 = PointerInputChange( |
| id = 1, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| offset2 - PxPosition(50.px, 50.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange3 = PointerInputChange( |
| id = 2, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| offset3 - PxPosition(100.px, 100.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(childPointerInputNode1.pointerInputHandler) |
| .invoke(listOf(expectedChange1), pointerEventPass, IntPxSize(100.ipx, 100.ipx)) |
| verify(childPointerInputNode2.pointerInputHandler) |
| .invoke(listOf(expectedChange2), pointerEventPass, IntPxSize(100.ipx, 100.ipx)) |
| verify(childPointerInputNode3.pointerInputHandler) |
| .invoke(listOf(expectedChange3), pointerEventPass, IntPxSize(100.ipx, 100.ipx)) |
| } |
| verifyNoMoreInteractions( |
| childPointerInputNode1.pointerInputHandler, |
| childPointerInputNode2.pointerInputHandler, |
| childPointerInputNode3.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * 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 childPointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 100, 150)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(25, 50, 75, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, childPointerInputNode1) |
| emitInsertAt(1, childPointerInputNode2) |
| } |
| |
| val offset1 = PxPosition(50.px, 25.px) |
| val offset2 = PxPosition(50.px, 75.px) |
| val offset3 = PxPosition(50.px, 125.px) |
| |
| val down = PointerInputEvent( |
| 7L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 7L.millisecondsToTimestamp(), offset1, true), |
| PointerInputEventData(1, 7L.millisecondsToTimestamp(), offset2, true), |
| PointerInputEventData(2, 7L.millisecondsToTimestamp(), offset3, true) |
| ) |
| ) |
| |
| val expectedChange1 = PointerInputChange( |
| id = 0, |
| current = PointerInputData(7L.millisecondsToTimestamp(), offset1, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange2 = PointerInputChange( |
| id = 1, |
| current = PointerInputData( |
| 7L.millisecondsToTimestamp(), |
| offset2 - PxPosition(25.px, 50.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange3 = PointerInputChange( |
| id = 2, |
| current = PointerInputData(7L.millisecondsToTimestamp(), offset3, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(childPointerInputNode1.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange1, expectedChange3), |
| pointerEventPass, |
| IntPxSize(100.ipx, 150.ipx) |
| ) |
| verify(childPointerInputNode2.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange2), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| } |
| verifyNoMoreInteractions( |
| childPointerInputNode1.pointerInputHandler, |
| childPointerInputNode2.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * 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 childPointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 150, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(50, 25, 100, 75)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, childPointerInputNode1) |
| emitInsertAt(1, childPointerInputNode2) |
| } |
| |
| val offset1 = PxPosition(25.px, 50.px) |
| val offset2 = PxPosition(75.px, 50.px) |
| val offset3 = PxPosition(125.px, 50.px) |
| |
| val down = PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 11L.millisecondsToTimestamp(), offset1, true), |
| PointerInputEventData(1, 11L.millisecondsToTimestamp(), offset2, true), |
| PointerInputEventData(2, 11L.millisecondsToTimestamp(), offset3, true) |
| ) |
| ) |
| |
| val expectedChange1 = PointerInputChange( |
| id = 0, |
| current = PointerInputData(11L.millisecondsToTimestamp(), offset1, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange2 = PointerInputChange( |
| id = 1, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| offset2 - PxPosition(50.px, 25.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| val expectedChange3 = PointerInputChange( |
| id = 2, |
| current = PointerInputData(11L.millisecondsToTimestamp(), offset3, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(childPointerInputNode1.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange1, expectedChange3), |
| pointerEventPass, |
| IntPxSize(150.ipx, 100.ipx) |
| ) |
| verify(childPointerInputNode2.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange2), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| } |
| verifyNoMoreInteractions( |
| childPointerInputNode1.pointerInputHandler, |
| childPointerInputNode2.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * This test creates a layout of this shape: |
| * |
| * t t |
| * |---| |
| * t|t | t t t |
| * | | |
| * |---| |
| * |
| * t t t |
| * |
| * |---| |
| * | | |
| * t t t | t|t |
| * |---| |
| * t t |
| * |
| * One PointerInputNode with 2 child LayoutNodes that are far apart. Touches happen both |
| * inside the bounding box that wraps around the LayoutNodes, and just outside of it. Those |
| * that happen inside all hit, those that happen outside do not. |
| */ |
| @Test |
| fun process_ManyPointersOnPinWith2LnsThatAreTopLeftBottomRight_onlyCorrectPointersHit() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(100, 100, 200, 200)) |
| emitInsertAt(1, LayoutNode(300, 300, 400, 400)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| val offsetsThatHit = |
| listOf( |
| PxPosition(100.px, 100.px), |
| PxPosition(250.px, 100.px), |
| PxPosition(399.px, 100.px), |
| PxPosition(100.px, 250.px), |
| PxPosition(250.px, 250.px), |
| PxPosition(399.px, 250.px), |
| PxPosition(100.px, 399.px), |
| PxPosition(250.px, 399.px), |
| PxPosition(399.px, 399.px) |
| ) |
| val offsetsThatMiss = |
| listOf( |
| PxPosition(100.px, 99.px), |
| PxPosition(399.px, 99.px), |
| PxPosition(99.px, 100.px), |
| PxPosition(400.px, 100.px), |
| PxPosition(99.px, 399.px), |
| PxPosition(400.px, 399.px), |
| PxPosition(100.px, 400.px), |
| PxPosition(399.px, 400.px) |
| ) |
| val allOffsets = offsetsThatHit + offsetsThatMiss |
| val pointerInputEvent = |
| PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| (allOffsets.indices).map { |
| PointerInputEventData(it, 11L.millisecondsToTimestamp(), allOffsets[it], true) |
| } |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| val expectedChanges = |
| (offsetsThatHit.indices).map { |
| PointerInputChange( |
| id = it, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| offsetsThatHit[it] - PxPosition(100.px, 100.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| PointerEventPass.values().forEach { pointerEventPass -> |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges), |
| eq(pointerEventPass), |
| any() |
| ) |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * This test creates a layout of this shape: |
| * |
| * t t |
| * |---| |
| * t t t | t|t |
| * | | |
| * |---| |
| * |
| * t t t |
| * |
| * |---| |
| * | | |
| * t|t | t t t |
| * |---| |
| * t t |
| * |
| * One PointerInputNode with 2 child LayoutNodes that are far apart. Touches happen both |
| * inside the bounding box that wraps around the LayoutNodes, and just outside of it. Those |
| * that happen inside all hit, those that happen outside do not. |
| */ |
| @Test |
| fun process_ManyPointersOnPinWith2LnsThatAreTopRightBottomLeft_onlyCorrectPointersHit() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(300, 100, 400, 200)) |
| emitInsertAt(1, LayoutNode(100, 300, 200, 400)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| val offsetsThatHit = |
| listOf( |
| PxPosition(100.px, 100.px), |
| PxPosition(250.px, 100.px), |
| PxPosition(399.px, 100.px), |
| PxPosition(100.px, 250.px), |
| PxPosition(250.px, 250.px), |
| PxPosition(399.px, 250.px), |
| PxPosition(100.px, 399.px), |
| PxPosition(250.px, 399.px), |
| PxPosition(399.px, 399.px) |
| ) |
| val offsetsThatMiss = |
| listOf( |
| PxPosition(100.px, 99.px), |
| PxPosition(399.px, 99.px), |
| PxPosition(99.px, 100.px), |
| PxPosition(400.px, 100.px), |
| PxPosition(99.px, 399.px), |
| PxPosition(400.px, 399.px), |
| PxPosition(100.px, 400.px), |
| PxPosition(399.px, 400.px) |
| ) |
| val allOffsets = offsetsThatHit + offsetsThatMiss |
| val pointerInputEvent = |
| PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| (allOffsets.indices).map { |
| PointerInputEventData(it, 11L.millisecondsToTimestamp(), allOffsets[it], true) |
| } |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| val expectedChanges = |
| (offsetsThatHit.indices).map { |
| PointerInputChange( |
| id = it, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| offsetsThatHit[it] - PxPosition(100.px, 100.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| PointerEventPass.values().forEach { pointerEventPass -> |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges), |
| eq(pointerEventPass), |
| any() |
| ) |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * 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 PointerInputNodes around 4 LayoutNodes 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_4DownInClippedAreaOfLnsWrappedByPins_onlyCorrectPointersHit() { |
| |
| // Arrange |
| |
| val singlePointerInputHandler = spy(MyPointerInputHandler()) |
| val pointerInputNode1 = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(-1, -1, 1, 1)) |
| pointerInputHandler = singlePointerInputHandler |
| } |
| val pointerInputNode2 = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(2, -1, 4, 1)) |
| pointerInputHandler = singlePointerInputHandler |
| } |
| val pointerInputNode3 = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(-1, 2, 1, 4)) |
| pointerInputHandler = singlePointerInputHandler |
| } |
| val pointerInputNode4 = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(2, 2, 4, 4)) |
| pointerInputHandler = singlePointerInputHandler |
| } |
| val parentLayoutNode = LayoutNode(1, 1, 4, 4).apply { |
| emitInsertAt(0, pointerInputNode1) |
| emitInsertAt(1, pointerInputNode2) |
| emitInsertAt(2, pointerInputNode3) |
| emitInsertAt(3, pointerInputNode4) |
| } |
| root.apply { |
| emitInsertAt(0, parentLayoutNode) |
| } |
| val offsetsThatHit = |
| listOf( |
| PxPosition(1.px, 1.px), |
| PxPosition(3.px, 1.px), |
| PxPosition(1.px, 3.px), |
| PxPosition(3.px, 3.px) |
| ) |
| val offsetsThatMiss = |
| listOf( |
| PxPosition(1.px, 0.px), |
| PxPosition(3.px, 0.px), |
| PxPosition(0.px, 1.px), |
| PxPosition(4.px, 1.px), |
| PxPosition(0.px, 3.px), |
| PxPosition(4.px, 3.px), |
| PxPosition(1.px, 4.px), |
| PxPosition(3.px, 4.px) |
| ) |
| val allOffsets = offsetsThatHit + offsetsThatMiss |
| val pointerInputEvent = |
| PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| (allOffsets.indices).map { |
| PointerInputEventData(it, 11L.millisecondsToTimestamp(), allOffsets[it], true) |
| } |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| val expectedChanges = |
| (offsetsThatHit.indices).map { |
| PointerInputChange( |
| id = it, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| PxPosition( |
| if (offsetsThatHit[it].x == 1.px) 1.px else 0.px, |
| if (offsetsThatHit[it].y == 1.px) 1.px else 0.px |
| ), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| PointerEventPass.values().forEach { pointerEventPass -> |
| expectedChanges.forEach { change -> |
| verify(singlePointerInputHandler).invoke( |
| eq(listOf(change)), |
| eq(pointerEventPass), |
| any() |
| ) |
| } |
| } |
| verifyNoMoreInteractions( |
| singlePointerInputHandler |
| ) |
| } |
| |
| /** |
| * This test creates a layout of this shape: |
| * |
| * ..... |
| * . B . |
| * ..... |
| * t t |
| * |-----| |
| * t|t t|t |
| * | A | |
| * t|t t|t |
| * |-----| |
| * t t |
| * ..... |
| * . C . |
| * ..... |
| * |
| * Here we have a LayoutNode (A) that is the parent of a PointerInputNode that is then a parent |
| * of LayoutNodes B and C. 4 touches are performed in the corners of (A). |
| * |
| * Even though B and C are themselves are not touchable because they are laid out outside of the |
| * bounds of their parent LayoutNode, the PointerInputNode that wraps them is sized to include |
| * the space underneath A, so all 4 touches should hit. |
| */ |
| @Test |
| fun process_lnWithPinWith2LnsOutsideOfLayoutBoundsPointerInsidePin_pointersHit() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(350, -50, 400, 0)) |
| emitInsertAt(1, LayoutNode(-50, 350, 0, 400)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val layoutNode: LayoutNode = LayoutNode(100, 100, 400, 400).apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| root.apply { |
| emitInsertAt(0, layoutNode) |
| } |
| val offsetsThatHit = |
| listOf( |
| PxPosition(100.px, 100.px), |
| PxPosition(399.px, 100.px), |
| PxPosition(100.px, 399.px), |
| PxPosition(399.px, 399.px) |
| ) |
| val offsetsThatMiss = |
| listOf( |
| PxPosition(100.px, 99.px), |
| PxPosition(399.px, 99.px), |
| PxPosition(99.px, 100.px), |
| PxPosition(400.px, 100.px), |
| PxPosition(99.px, 399.px), |
| PxPosition(400.px, 399.px), |
| PxPosition(100.px, 400.px), |
| PxPosition(399.px, 400.px) |
| ) |
| val allOffsets = offsetsThatHit + offsetsThatMiss |
| val pointerInputEvent = |
| PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| (allOffsets.indices).map { |
| PointerInputEventData(it, 11L.millisecondsToTimestamp(), allOffsets[it], true) |
| } |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| val expectedChanges = |
| (offsetsThatHit.indices).map { |
| PointerInputChange( |
| id = it, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| offsetsThatHit[it] - PxPosition(50.px, 50.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| PointerEventPass.values().forEach { pointerEventPass -> |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges), |
| eq(pointerEventPass), |
| any() |
| ) |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * This test creates a layout of this shape: |
| * |
| * t t |
| * * |
| * t t t t t |
| * |
| * |
| * |
| * t t t |
| * |
| * |
| * |
| * t t t t t |
| * * |
| * t t |
| * |
| * One PointerInputNode with 2 child LayoutNodes that have no size (represented by *). Touches |
| * happen both inside the bounding box that wraps around the LayoutNodes, and just outside of |
| * it. Those that happen inside all hit, those that happen outside do not. |
| */ |
| @Test |
| fun process_manyPointersOnPinWith2LnsWithNoSize_onlyCorrectPointersHit() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(400, 100, 400, 100)) |
| emitInsertAt(1, LayoutNode(100, 400, 100, 400)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| val offsetsThatHit = |
| listOf( |
| PxPosition(100.px, 100.px), |
| PxPosition(250.px, 100.px), |
| PxPosition(399.px, 100.px), |
| PxPosition(100.px, 250.px), |
| PxPosition(250.px, 250.px), |
| PxPosition(399.px, 250.px), |
| PxPosition(100.px, 399.px), |
| PxPosition(250.px, 399.px), |
| PxPosition(399.px, 399.px) |
| ) |
| val offsetsThatMiss = |
| listOf( |
| PxPosition(100.px, 99.px), |
| PxPosition(399.px, 99.px), |
| PxPosition(99.px, 100.px), |
| PxPosition(400.px, 100.px), |
| PxPosition(99.px, 399.px), |
| PxPosition(400.px, 399.px), |
| PxPosition(100.px, 400.px), |
| PxPosition(399.px, 400.px) |
| ) |
| val allOffsets = offsetsThatHit + offsetsThatMiss |
| val pointerInputEvent = |
| PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| (allOffsets.indices).map { |
| PointerInputEventData(it, 11L.millisecondsToTimestamp(), allOffsets[it], true) |
| } |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| val expectedChanges = |
| (offsetsThatHit.indices).map { |
| PointerInputChange( |
| id = it, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| offsetsThatHit[it] - PxPosition(100.px, 100.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| PointerEventPass.values().forEach { pointerEventPass -> |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges), |
| eq(pointerEventPass), |
| any() |
| ) |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler |
| ) |
| } |
| |
| /** |
| * 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_additionalOffsetExists_onlyCorrectPointersHit() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 2, 2)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| val offsetsThatHit = |
| listOf( |
| PxPosition(2.px, 2.px), |
| PxPosition(2.px, 1.px), |
| PxPosition(1.px, 2.px) |
| ) |
| val offsetsThatMiss = |
| listOf( |
| PxPosition(0.px, 0.px), |
| PxPosition(0.px, 1.px), |
| PxPosition(1.px, 0.px) |
| ) |
| val allOffsets = offsetsThatHit + offsetsThatMiss |
| val pointerInputEvent = |
| PointerInputEvent( |
| 11L.millisecondsToTimestamp(), |
| (allOffsets.indices).map { |
| PointerInputEventData(it, 11L.millisecondsToTimestamp(), allOffsets[it], true) |
| } |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition(1.ipx, 1.ipx)) |
| |
| // Assert |
| |
| val expectedChanges = |
| (offsetsThatHit.indices).map { |
| PointerInputChange( |
| id = it, |
| current = PointerInputData( |
| 11L.millisecondsToTimestamp(), |
| offsetsThatHit[it] - PxPosition(1.px, 1.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| } |
| PointerEventPass.values().forEach { pointerEventPass -> |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges), |
| eq(pointerEventPass), |
| any() |
| ) |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler |
| ) |
| } |
| |
| @Test |
| fun process_downOn3NestedPointerInputNodes_hitAndDispatchInfoAreCorrect() { |
| val childPointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(25, 50, 75, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, childPointerInputNode1) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode3: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, childPointerInputNode2) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, childPointerInputNode3) |
| } |
| |
| val offset1 = PxPosition(50.px, 75.px) |
| |
| val down = PointerInputEvent( |
| 7L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 7L.millisecondsToTimestamp(), offset1, true) |
| ) |
| ) |
| |
| val expectedChange = PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 7L.millisecondsToTimestamp(), |
| offset1 - PxPosition(25.px, 50.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(childPointerInputNode1.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| verify(childPointerInputNode2.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| verify(childPointerInputNode3.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange), |
| pointerEventPass, |
| IntPxSize(50.ipx, 50.ipx) |
| ) |
| } |
| verifyNoMoreInteractions(childPointerInputNode1.pointerInputHandler) |
| verifyNoMoreInteractions(childPointerInputNode2.pointerInputHandler) |
| verifyNoMoreInteractions(childPointerInputNode3.pointerInputHandler) |
| } |
| |
| @Test |
| fun process_downOnDeeplyNestedPointerInputNode_hitAndDispatchInfoAreCorrect() { |
| val layoutNode1 = LayoutNode(1, 5, 500, 500) |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, layoutNode1) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val layoutNode2: LayoutNode = LayoutNode(2, 6, 500, 500).apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| val layoutNode3: LayoutNode = LayoutNode(3, 7, 500, 500).apply { |
| emitInsertAt(0, layoutNode2) |
| } |
| val layoutNode4: LayoutNode = LayoutNode(4, 8, 500, 500).apply { |
| emitInsertAt(0, layoutNode3) |
| } |
| root.apply { |
| emitInsertAt(0, layoutNode4) |
| } |
| |
| val offset1 = PxPosition(499.px, 499.px) |
| |
| val downEvent = PointerInputEvent( |
| 7L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 7L.millisecondsToTimestamp(), offset1, true) |
| ) |
| ) |
| |
| val expectedChange = PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 7L.millisecondsToTimestamp(), |
| offset1 - PxPosition(1.px + 2.px + 3.px + 4.px, 5.px + 6.px + 7.px + 8.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(downEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange), |
| pointerEventPass, |
| IntPxSize(499.ipx, 495.ipx) |
| ) |
| } |
| verifyNoMoreInteractions(pointerInputNode.pointerInputHandler) |
| } |
| |
| @Test |
| fun process_downOnComplexPointerAndLayoutNodePath_hitAndDispatchInfoAreCorrect() { |
| val layoutNode1 = LayoutNode(1, 6, 500, 500) |
| val pointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, layoutNode1) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val pointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, pointerInputNode1) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val layoutNode2: LayoutNode = LayoutNode(2, 7, 500, 500).apply { |
| emitInsertAt(0, pointerInputNode2) |
| } |
| val layoutNode3: LayoutNode = LayoutNode(3, 8, 500, 500).apply { |
| emitInsertAt(0, layoutNode2) |
| } |
| val pointerInputNode3: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, layoutNode3) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val pointerInputNode4: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, pointerInputNode3) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val layoutNode4: LayoutNode = LayoutNode(4, 9, 500, 500).apply { |
| emitInsertAt(0, pointerInputNode4) |
| } |
| val layoutNode5: LayoutNode = LayoutNode(5, 10, 500, 500).apply { |
| emitInsertAt(0, layoutNode4) |
| } |
| root.apply { |
| emitInsertAt(0, layoutNode5) |
| } |
| |
| val offset1 = PxPosition(499.px, 499.px) |
| |
| val downEvent = PointerInputEvent( |
| 3L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData(0, 3L.millisecondsToTimestamp(), offset1, true) |
| ) |
| ) |
| |
| val expectedChange1 = PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 3L.millisecondsToTimestamp(), |
| offset1 - PxPosition( |
| 1.px + 2.px + 3.px + 4.px + 5.px, |
| 6.px + 7.px + 8.px + 9.px + 10.px |
| ), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| val expectedChange2 = PointerInputChange( |
| id = 0, |
| current = PointerInputData( |
| 3L.millisecondsToTimestamp(), |
| offset1 - PxPosition(3.px + 4.px + 5.px, 8.px + 9.px + 10.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(downEvent, IntPxPosition.Origin) |
| |
| // Assert |
| |
| for (pointerEventPass in PointerEventPass.values()) { |
| verify(pointerInputNode1.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange1), |
| pointerEventPass, |
| IntPxSize(499.ipx, 494.ipx) |
| ) |
| verify(pointerInputNode2.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange1), |
| pointerEventPass, |
| IntPxSize(499.ipx, 494.ipx) |
| ) |
| verify(pointerInputNode3.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange2), |
| pointerEventPass, |
| IntPxSize(497.ipx, 492.ipx) |
| ) |
| verify(pointerInputNode4.pointerInputHandler) |
| .invoke( |
| listOf(expectedChange2), |
| pointerEventPass, |
| IntPxSize(497.ipx, 492.ipx) |
| ) |
| } |
| verifyNoMoreInteractions(pointerInputNode1.pointerInputHandler) |
| verifyNoMoreInteractions(pointerInputNode2.pointerInputHandler) |
| verifyNoMoreInteractions(pointerInputNode3.pointerInputHandler) |
| verifyNoMoreInteractions(pointerInputNode4.pointerInputHandler) |
| } |
| |
| @Test |
| fun process_downOnCompletelyOverlappingPointerInputNodes_onlyTopPointerInputNodeReceives() { |
| val childPointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 100, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val childPointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 100, 100)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, childPointerInputNode1) |
| emitInsertAt(1, childPointerInputNode2) |
| } |
| |
| val down = PointerInputEvent( |
| 1, 0L.millisecondsToTimestamp(), PxPosition(50.px, 50.px), true |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| verify(childPointerInputNode2.pointerInputHandler, times(5)).invoke(any(), any(), any()) |
| verify(childPointerInputNode1.pointerInputHandler, never()).invoke(any(), any(), any()) |
| } |
| |
| @Test |
| fun process_downOnPointerInputNodeWrappingSemanticsNodeWrappingLayoutNode_downReceived() { |
| val semanticsComponentNode: SemanticsComponentNode = |
| SemanticsComponentNode( |
| container = false, |
| explicitChildNodes = false |
| ).apply { |
| emitInsertAt(0, LayoutNode(0, 0, 100, 100)) |
| } |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, semanticsComponentNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| |
| val down = PointerInputEvent( |
| 1, 0L.millisecondsToTimestamp(), PxPosition(50.px, 50.px), true |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| verify(pointerInputNode.pointerInputHandler, times(5)).invoke(any(), any(), any()) |
| } |
| |
| @Test |
| fun process_downOnPointerInputNodeWrappingDrawNode_downNotReceived() { |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, DrawNode()) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| |
| val down = PointerInputEvent( |
| 1, 0L.millisecondsToTimestamp(), PxPosition(50.px, 50.px), true |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| verify(pointerInputNode.pointerInputHandler, never()).invoke(any(), any(), any()) |
| } |
| |
| @Test |
| fun process_downOnPointerInputNodeWrappingSemanticsNode_downNotReceived() { |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt( |
| 0, SemanticsComponentNode( |
| container = false, |
| explicitChildNodes = false |
| ) |
| ) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| |
| val down = PointerInputEvent( |
| 1, 0L.millisecondsToTimestamp(), PxPosition(50.px, 50.px), true |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| verify(pointerInputNode.pointerInputHandler, never()).invoke(any(), any(), any()) |
| } |
| |
| @Test |
| fun process_downOnPointerInputNodeWrappingPointerInputNodeNode_downNotReceived() { |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, PointerInputNode()) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.apply { |
| emitInsertAt(0, pointerInputNode) |
| } |
| |
| val down = PointerInputEvent( |
| 1, 0L.millisecondsToTimestamp(), PxPosition(50.px, 50.px), true |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| |
| // Assert |
| verify(pointerInputNode.pointerInputHandler, never()).invoke(any(), any(), any()) |
| } |
| |
| @Test |
| fun process_pointerInputNodeRemovedDuringInput_correctPointerInputChangesReceived() { |
| |
| // Arrange |
| |
| val childLayoutNode = LayoutNode(0, 0, 100, 100) |
| val childPointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, childLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| val parentLayoutNode: LayoutNode = LayoutNode(0, 0, 100, 100).apply { |
| emitInsertAt(0, childPointerInputNode) |
| } |
| val parentPointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, parentLayoutNode) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| } |
| root.emitInsertAt(0, parentPointerInputNode) |
| |
| val offset = PxPosition(50.px, 50.px) |
| |
| val down = PointerInputEvent(0, 7L.millisecondsToTimestamp(), offset, true) |
| val up = PointerInputEvent(0, 11L.millisecondsToTimestamp(), null, false) |
| |
| val expectedDownChange = PointerInputChange( |
| id = 0, |
| current = PointerInputData(7L.millisecondsToTimestamp(), offset, true), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| val expectedUpChange = PointerInputChange( |
| id = 0, |
| current = PointerInputData(11L.millisecondsToTimestamp(), null, false), |
| previous = PointerInputData(7L.millisecondsToTimestamp(), offset, true), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| parentLayoutNode.emitRemoveAt(0, 1) |
| pointerInputEventProcessor.process(up, IntPxPosition.Origin) |
| |
| // Assert |
| |
| PointerEventPass.values().forEach { |
| verify(parentPointerInputNode.pointerInputHandler) |
| .invoke(eq(listOf(expectedDownChange)), eq(it), any()) |
| verify(childPointerInputNode.pointerInputHandler) |
| .invoke(eq(listOf(expectedDownChange)), eq(it), any()) |
| verify(parentPointerInputNode.pointerInputHandler) |
| .invoke(eq(listOf(expectedUpChange)), eq(it), any()) |
| } |
| verifyNoMoreInteractions(parentPointerInputNode.pointerInputHandler) |
| verifyNoMoreInteractions(childPointerInputNode.pointerInputHandler) |
| } |
| |
| @Test |
| fun processCancel_noPointers_doesntCrash() { |
| pointerInputEventProcessor.processCancel() |
| } |
| |
| @Test |
| fun processCancel_downThenCancel_pinOnlyReceivesCorrectDownThenCancel() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val pointerInputEvent = |
| PointerInputEvent( |
| 7, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(250.px, 250.px), |
| true |
| ) |
| |
| val expectedChange = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(250.px, 250.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| pointerInputEventProcessor.processCancel() |
| |
| // Assert |
| |
| inOrder(pointerInputNode.pointerInputHandler, pointerInputNode.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expectedChange)), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode.cancelHandler).invoke() |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler, |
| pointerInputNode.cancelHandler |
| ) |
| } |
| |
| @Test |
| fun processCancel_downDownOnSamePinThenCancel_pinOnlyReceivesCorrectChangesThenCancel() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val pointerInputEvent1 = |
| PointerInputEvent( |
| 7, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ) |
| |
| val pointerInputEvent2 = |
| PointerInputEvent( |
| 10L.millisecondsToTimestamp(), |
| listOf( |
| PointerInputEventData( |
| 7, |
| 10L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| PointerInputEventData( |
| 9, |
| 10L.millisecondsToTimestamp(), |
| PxPosition(300.px, 300.px), |
| true |
| ) |
| ) |
| ) |
| |
| val expectedChanges1 = |
| listOf( |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| ) |
| |
| val expectedChanges2 = |
| listOf( |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 10L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| previous = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| consumed = ConsumedData() |
| ), |
| PointerInputChange( |
| id = 9, |
| current = PointerInputData( |
| 10L.millisecondsToTimestamp(), |
| PxPosition(300.px, 300.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent1, IntPxPosition.Origin) |
| pointerInputEventProcessor.process(pointerInputEvent2, IntPxPosition.Origin) |
| pointerInputEventProcessor.processCancel() |
| |
| // Assert |
| |
| inOrder(pointerInputNode.pointerInputHandler, pointerInputNode.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges1), |
| eq(pass), |
| any() |
| ) |
| } |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(expectedChanges2), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode.cancelHandler).invoke() |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler, |
| pointerInputNode.cancelHandler |
| ) |
| } |
| |
| @Test |
| fun processCancel_downOn2DifferentPinsThenCancel_pinsOnlyReceiveCorrectDownsThenCancel() { |
| |
| // Arrange |
| |
| val pointerInputNode1: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 199, 199)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| val pointerInputNode2: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(200, 200, 399, 399)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode1) |
| root.emitInsertAt(1, pointerInputNode2) |
| |
| val pointerInputEventData1 = |
| PointerInputEventData( |
| 7, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(100.px, 100.px), |
| true |
| ) |
| |
| val pointerInputEventData2 = |
| PointerInputEventData( |
| 9, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(300.px, 300.px), |
| true |
| ) |
| |
| val pointerInputEvent = PointerInputEvent( |
| 5L.millisecondsToTimestamp(), |
| listOf(pointerInputEventData1, pointerInputEventData2) |
| ) |
| |
| val expectedChange1 = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(100.px, 100.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| val expectedChange2 = |
| PointerInputChange( |
| id = 9, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(100.px, 100.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(pointerInputEvent, IntPxPosition.Origin) |
| pointerInputEventProcessor.processCancel() |
| |
| // Assert |
| |
| inOrder(pointerInputNode1.pointerInputHandler, pointerInputNode1.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode1.pointerInputHandler).invoke( |
| eq(listOf(expectedChange1)), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode1.cancelHandler).invoke() |
| } |
| inOrder(pointerInputNode2.pointerInputHandler, pointerInputNode2.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode2.pointerInputHandler).invoke( |
| eq(listOf(expectedChange2)), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode2.cancelHandler).invoke() |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode1.pointerInputHandler, |
| pointerInputNode1.cancelHandler, |
| pointerInputNode2.pointerInputHandler, |
| pointerInputNode2.cancelHandler |
| ) |
| } |
| |
| @Test |
| fun processCancel_downMoveCancel_pinOnlyReceivesCorrectDownMoveCancel() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val down = |
| PointerInputEvent( |
| 7, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ) |
| |
| val move = |
| PointerInputEvent( |
| 7, |
| 10L.millisecondsToTimestamp(), |
| PxPosition(300.px, 300.px), |
| true |
| ) |
| |
| val expectedDown = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| val expectedMove = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 10L.millisecondsToTimestamp(), |
| PxPosition(300.px, 300.px), |
| true |
| ), |
| previous = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| pointerInputEventProcessor.process(move, IntPxPosition.Origin) |
| pointerInputEventProcessor.processCancel() |
| |
| // Assert |
| |
| inOrder(pointerInputNode.pointerInputHandler, pointerInputNode.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expectedDown)), |
| eq(pass), |
| any() |
| ) |
| } |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expectedMove)), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode.cancelHandler).invoke() |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler, |
| pointerInputNode.cancelHandler |
| ) |
| } |
| |
| @Test |
| fun processCancel_downCancelMoveUp_pinOnlyReceivesCorrectDownCancel() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val down = |
| PointerInputEvent( |
| 7, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ) |
| |
| val expectedDown = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down, IntPxPosition.Origin) |
| pointerInputEventProcessor.processCancel() |
| |
| // Assert |
| |
| inOrder(pointerInputNode.pointerInputHandler, pointerInputNode.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expectedDown)), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode.cancelHandler).invoke() |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler, |
| pointerInputNode.cancelHandler |
| ) |
| } |
| |
| @Test |
| fun processCancel_downCancelDown_pinOnlyReceivesCorrectDownCancelDown() { |
| |
| // Arrange |
| |
| val pointerInputNode: PointerInputNode = PointerInputNode().apply { |
| emitInsertAt(0, LayoutNode(0, 0, 500, 500)) |
| pointerInputHandler = spy(MyPointerInputHandler()) |
| cancelHandler = spy(MyCancelHandler()) |
| } |
| root.emitInsertAt(0, pointerInputNode) |
| |
| val down1 = |
| PointerInputEvent( |
| 7, |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ) |
| |
| val down2 = |
| PointerInputEvent( |
| 7, |
| 10L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ) |
| |
| val expectedDown1 = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 5L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| val expectedDown2 = |
| PointerInputChange( |
| id = 7, |
| current = PointerInputData( |
| 10L.millisecondsToTimestamp(), |
| PxPosition(200.px, 200.px), |
| true |
| ), |
| previous = PointerInputData(null, null, false), |
| consumed = ConsumedData() |
| ) |
| |
| // Act |
| |
| pointerInputEventProcessor.process(down1, IntPxPosition.Origin) |
| pointerInputEventProcessor.processCancel() |
| pointerInputEventProcessor.process(down2, IntPxPosition.Origin) |
| |
| // Assert |
| |
| inOrder(pointerInputNode.pointerInputHandler, pointerInputNode.cancelHandler) { |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expectedDown1)), |
| eq(pass), |
| any() |
| ) |
| } |
| verify(pointerInputNode.cancelHandler).invoke() |
| for (pass in PointerEventPass.values()) { |
| verify(pointerInputNode.pointerInputHandler).invoke( |
| eq(listOf(expectedDown2)), |
| eq(pass), |
| any() |
| ) |
| } |
| } |
| verifyNoMoreInteractions( |
| pointerInputNode.pointerInputHandler, |
| pointerInputNode.cancelHandler |
| ) |
| } |
| } |