| /* |
| * 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.AlignmentLine |
| import androidx.ui.core.Constraints |
| import androidx.ui.core.CustomEvent |
| import androidx.ui.core.CustomEventDispatcher |
| import androidx.ui.core.LayoutCoordinates |
| import androidx.ui.core.PointerEventPass |
| import androidx.ui.core.PointerId |
| import androidx.ui.core.PointerInputChange |
| import androidx.ui.core.PointerInputHandler |
| import androidx.ui.core.consumeDownChange |
| import androidx.ui.core.consumePositionChange |
| import androidx.ui.core.positionChange |
| import androidx.ui.testutils.down |
| import androidx.ui.testutils.moveTo |
| import androidx.ui.unit.IntSize |
| import androidx.ui.unit.PxBounds |
| import androidx.ui.geometry.Offset |
| 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.mock |
| import com.nhaarman.mockitokotlin2.never |
| 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 |
| import kotlin.math.roundToInt |
| |
| @SmallTest |
| @RunWith(JUnit4::class) |
| class HitPathTrackerTest { |
| |
| private lateinit var hitPathTracker: HitPathTracker |
| |
| @Before |
| fun setup() { |
| hitPathTracker = HitPathTracker() |
| } |
| |
| @Test |
| fun addHitPath_emptyHitResult_resultIsCorrect() { |
| val pif1: PointerInputFilter = mock() |
| val pif2: PointerInputFilter = mock() |
| val pif3: PointerInputFilter = mock() |
| val pointerId = PointerId(1) |
| |
| hitPathTracker.addHitPath(pointerId, listOf(pif1, pif2, pif3)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif1).apply { |
| pointerIds.add(pointerId) |
| children.add(Node(pif2).apply { |
| pointerIds.add(pointerId) |
| children.add(Node(pif3).apply { |
| pointerIds.add(pointerId) |
| }) |
| }) |
| }) |
| } |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| @Test |
| fun addHitPath_existingNonMatchingTree_resultIsCorrect() { |
| val pif1: PointerInputFilter = mock() |
| val pif2: PointerInputFilter = mock() |
| val pif3: PointerInputFilter = mock() |
| val pif4: PointerInputFilter = mock() |
| val pif5: PointerInputFilter = mock() |
| val pif6: PointerInputFilter = mock() |
| val pointerId1 = PointerId(1) |
| val pointerId2 = PointerId(2) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3)) |
| hitPathTracker.addHitPath(pointerId2, listOf(pif4, pif5, pif6)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(pif2).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(pif3).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| children.add(Node(pif4).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(pif5).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(pif6).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| @Test |
| fun addHitPath_completeMatchingTree_resultIsCorrect() { |
| val pif1: PointerInputFilter = mock() |
| val pif2: PointerInputFilter = mock() |
| val pif3: PointerInputFilter = mock() |
| val pointerId1 = PointerId(1) |
| val pointerId2 = PointerId(2) |
| hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3)) |
| |
| hitPathTracker.addHitPath(pointerId2, listOf(pif1, pif2, pif3)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif1).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| children.add(Node(pif2).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| children.add(Node(pif3).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| @Test |
| fun addHitPath_partiallyMatchingTree_resultIsCorrect() { |
| val pif1: PointerInputFilter = mock() |
| val pif2: PointerInputFilter = mock() |
| val pif3: PointerInputFilter = mock() |
| val pif4: PointerInputFilter = mock() |
| val pif5: PointerInputFilter = mock() |
| val pointerId1 = PointerId(1) |
| val pointerId2 = PointerId(2) |
| hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3)) |
| |
| hitPathTracker.addHitPath(pointerId2, listOf(pif1, pif4, pif5)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif1).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| children.add(Node(pif2).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(pif3).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| children.add(Node(pif4).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(pif5).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| @Test |
| fun addHitPath_1NodeAdded_initHandlerCalledWithValidCustomMessageDispatcher() { |
| val pif: PointerInputFilter = mock() |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(pif)) |
| |
| verify(pif).onInit(any()) |
| } |
| |
| @Test |
| fun addHitPath_3NodesAdded_allIitHandlersCalledWithValidCustomMessageDispatcher() { |
| val pifParent: PointerInputFilter = mock() |
| val pifMiddle: PointerInputFilter = mock() |
| val pifChild: PointerInputFilter = mock() |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(pifParent, pifMiddle, pifChild)) |
| |
| verify(pifParent).onInit(any()) |
| verify(pifMiddle).onInit(any()) |
| verify(pifChild).onInit(any()) |
| } |
| |
| @Test |
| fun dispatchChanges_noNodes_doesNotCrash() { |
| hitPathTracker.dispatchChanges(listOf(down(0))) |
| } |
| |
| @Test |
| fun dispatchChanges_hitResultHasSingleMatch_pointerInputHandlerCalled() { |
| val pif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif)) |
| |
| hitPathTracker.dispatchChanges(listOf(down(13))) |
| |
| // Verify call count |
| verify(pif, times(5)).onPointerInput(any(), any(), any()) |
| // Verify call values |
| PointerEventPass.values().forEach { |
| verify(pif).onPointerInput( |
| eq(listOf(down(13))), |
| eq(it), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun dispatchChanges_hitResultHasMultipleMatches_pointerInputHandlersCalledInCorrectOrder() { |
| val pif1: PointerInputFilter = PointerInputFilterMock() |
| val pif2: PointerInputFilter = PointerInputFilterMock() |
| val pif3: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif1, pif2, pif3)) |
| |
| hitPathTracker.dispatchChanges(listOf(down(13))) |
| |
| // Verify call count |
| verify(pif1, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif2, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif3, times(5)).onPointerInput(any(), any(), any()) |
| // Verify call order and values |
| inOrder(pif1, pif2, pif3) { |
| verify(pif1).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun dispatchChanges_hasDownAndUpPath_pointerInputHandlersCalledInCorrectOrder() { |
| val pif1: PointerInputFilter = PointerInputFilterMock() |
| val pif2: PointerInputFilter = PointerInputFilterMock() |
| val pif3: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif1, pif2, pif3)) |
| |
| hitPathTracker.dispatchChanges(listOf(down(13))) |
| |
| // Verify call count |
| verify(pif1, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif2, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif3, times(5)).onPointerInput(any(), any(), any()) |
| // Verify call order and values |
| inOrder(pif1, pif2, pif3) { |
| verify(pif1).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif1).onPointerInput( |
| eq(listOf(down(13))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun dispatchChanges_2IndependentBranchesFromRoot_eventsSplitCorrectlyAndCallOrderCorrect() { |
| val pif1: PointerInputFilter = PointerInputFilterMock() |
| val pif2: PointerInputFilter = PointerInputFilterMock() |
| val pif3: PointerInputFilter = PointerInputFilterMock() |
| val pif4: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(pif1, pif2)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(pif3, pif4)) |
| val event1 = down(3) |
| val event2 = down(5).moveTo(10.milliseconds, 7f, 9f) |
| |
| hitPathTracker.dispatchChanges( |
| listOf(event1, event2) |
| ) |
| |
| // Verify call count |
| verify(pif1, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif2, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif3, times(5)).onPointerInput(any(), any(), any()) |
| verify(pif4, times(5)).onPointerInput(any(), any(), any()) |
| // Verify call order and values |
| inOrder(pif1, pif2) { |
| verify(pif1).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif1).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| inOrder(pif3, pif4) { |
| verify(pif3).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif4).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif4).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun dispatchChanges_2BranchesWithSharedParent_eventsSplitCorrectlyAndCallOrderCorrect() { |
| val parent: PointerInputFilter = PointerInputFilterMock() |
| val child1: PointerInputFilter = PointerInputFilterMock() |
| val child2: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(parent, child1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(parent, child2)) |
| val event1 = down(3) |
| val event2 = down(5).moveTo(10.milliseconds, 7f, 9f) |
| |
| hitPathTracker.dispatchChanges( |
| listOf(event1, event2) |
| ) |
| |
| // Verify call count |
| verify(parent, times(5)).onPointerInput(any(), any(), any()) |
| verify(child1, times(5)).onPointerInput(any(), any(), any()) |
| verify(child2, times(5)).onPointerInput(any(), any(), any()) |
| |
| // Verifies that the events traverse between parent and child1 in the correct order. |
| inOrder( |
| parent, |
| child1 |
| ) { |
| verify(parent).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(parent).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| |
| // Verifies that the events traverse between parent and child2 in the correct order. |
| inOrder( |
| parent, |
| child2 |
| ) { |
| verify(parent).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(parent).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun dispatchChanges_2PointersShareCompletePath_eventsDoNotSplitAndCallOrderCorrect() { |
| val child1: PointerInputFilter = PointerInputFilterMock() |
| val child2: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(child1, child2)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(child1, child2)) |
| val event1 = down(3) |
| val event2 = down(5).moveTo(10.milliseconds, 7f, 9f) |
| |
| hitPathTracker.dispatchChanges( |
| listOf(event1, event2) |
| ) |
| |
| // Verify call count |
| verify(child1, times(5)).onPointerInput(any(), any(), any()) |
| verify(child2, times(5)).onPointerInput(any(), any(), any()) |
| |
| // Verify that order is correct for child1. |
| inOrder( |
| child1 |
| ) { |
| verify(child1).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| |
| // Verify that order is correct for child2. |
| inOrder( |
| child2 |
| ) { |
| verify(child2).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| |
| // Verify that first pass hits child1 before second pass hits child2 |
| inOrder( |
| child1, |
| child2 |
| ) { |
| verify(child1).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| |
| // Verify that first pass hits child2 before second pass hits child1 |
| inOrder( |
| child1, |
| child2 |
| ) { |
| verify(child2).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| } |
| } |
| |
| @Test |
| fun dispatchChanges_noNodes_nothingChanges() { |
| val (result, _) = hitPathTracker.dispatchChanges(listOf(down(5))) |
| |
| assertThat(result).isEqualTo(listOf(down(5))) |
| } |
| |
| @Test |
| fun dispatchChanges_hitResultHasSingleMatch_changesAreUpdatedCorrectly() { |
| val pif1: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, _, _ -> |
| changes.map { it.consumeDownChange() } |
| }) |
| ) |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif1)) |
| |
| val (result, _) = hitPathTracker.dispatchChanges(listOf(down(13))) |
| |
| assertThat(result).isEqualTo(listOf(down(13).consumeDownChange())) |
| } |
| |
| @Test |
| fun dispatchChanges_hitResultHasMultipleMatchesAndDownAndUpPaths_changesAreUpdatedCorrectly() { |
| val pif1: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 2f |
| PointerEventPass.PreUp -> 64f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| val pif2: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 4f |
| PointerEventPass.PreUp -> 32f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| val pif3: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 8f |
| PointerEventPass.PreUp -> 16f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif1, pif2, pif3)) |
| val change = down(13).moveTo(10.milliseconds, 0f, 130f) |
| |
| val (result, _) = hitPathTracker.dispatchChanges(listOf(change)) |
| |
| verify(pif1).onPointerInput( |
| eq(listOf(change)), eq(PointerEventPass.InitialDown), any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(change.consumePositionChange(0f, 2f))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(change.consumePositionChange(0f, 6f))), // 2 + 4 |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(change.consumePositionChange(0f, 14f))), // 2 + 4 + 8 |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(change.consumePositionChange(0f, 30f))), // 2 + 4 + 8 + 16 |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif1).onPointerInput( |
| eq(listOf(change.consumePositionChange(0f, 62f))), // 2 + 4 + 8 + 16 + 32 |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| assertThat(result) |
| .isEqualTo( |
| listOf( |
| change.consumePositionChange( |
| 0f, |
| 126f |
| ) |
| ) |
| ) // 2 + 4 + 8 + 16 + 32 + 64 |
| } |
| |
| @Test |
| fun dispatchChanges_2IndependentBranchesFromRoot_changesAreUpdatedCorrectly() { |
| val pif1: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 2f |
| PointerEventPass.PreUp -> 12f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| val pif2: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 3f |
| PointerEventPass.PreUp -> 6f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| val pif3: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> -2f |
| PointerEventPass.PreUp -> -12f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| val pif4: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> -3f |
| PointerEventPass.PreUp -> -6f |
| else -> 0f |
| } |
| it.consumePositionChange(0f, yConsume) |
| } |
| }) |
| ) |
| hitPathTracker.addHitPath(PointerId(3), listOf(pif1, pif2)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(pif3, pif4)) |
| val event1 = down(3).moveTo(10.milliseconds, 0f, 24f) |
| val event2 = down(5).moveTo(10.milliseconds, 0f, -24f) |
| |
| val (result, _) = hitPathTracker.dispatchChanges( |
| listOf(event1, event2) |
| ) |
| |
| verify(pif1).onPointerInput( |
| eq(listOf(event1)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(event1.consumePositionChange(0f, 2f))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif2).onPointerInput( |
| eq(listOf(event1.consumePositionChange(0f, 5f))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif1).onPointerInput( |
| eq(listOf(event1.consumePositionChange(0f, 11f))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| |
| verify(pif3).onPointerInput( |
| eq(listOf(event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif4).onPointerInput( |
| eq(listOf(event2.consumePositionChange(0f, -2f))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(pif4).onPointerInput( |
| eq(listOf(event2.consumePositionChange(0f, -5f))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(pif3).onPointerInput( |
| eq(listOf(event2.consumePositionChange(0f, -11f))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| |
| assertThat(result).hasSize(2) |
| assertThat(result).contains(event1.consumePositionChange(0f, 23f)) |
| assertThat(result).contains(event2.consumePositionChange(0f, -23f)) |
| } |
| |
| @Test |
| fun dispatchChanges_2BranchesWithSharedParent_changesAreUpdatedCorrectly() { |
| val parent = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 2 |
| PointerEventPass.PreUp -> 3 |
| else -> Int.MAX_VALUE |
| } |
| it.consumePositionChange( |
| 0f, |
| (it.positionChange().y.roundToInt() / yConsume).toFloat() |
| ) |
| } |
| }) |
| ) |
| val child1: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 5 |
| PointerEventPass.PreUp -> 7 |
| else -> Int.MAX_VALUE |
| } |
| it.consumePositionChange( |
| 0f, |
| (it.positionChange().y.roundToInt() / yConsume).toFloat() |
| ) |
| } |
| }) |
| ) |
| val child2: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 11 |
| PointerEventPass.PreUp -> 13 |
| else -> Int.MAX_VALUE |
| } |
| it.consumePositionChange( |
| 0f, |
| (it.positionChange().y.roundToInt() / yConsume).toFloat() |
| ) |
| } |
| }) |
| ) |
| hitPathTracker.addHitPath(PointerId(3), listOf(parent, child1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(parent, child2)) |
| val event1 = down(3).moveTo(10.milliseconds, 0f, 1000f) |
| val event2 = down(5).moveTo(10.milliseconds, 0f, -1000f) |
| |
| val (result, _) = hitPathTracker.dispatchChanges( |
| listOf(event1, event2) |
| ) |
| |
| verify(parent).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq(listOf(event1.consumePositionChange(0f, 500f))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq(listOf(event2.consumePositionChange(0f, -500f))), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq(listOf(event1.consumePositionChange(0f, 600f))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq(listOf(event2.consumePositionChange(0f, -545f))), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(parent).onPointerInput( |
| eq( |
| listOf( |
| event1.consumePositionChange(0f, 657f), |
| event2.consumePositionChange(0f, -580f) |
| ) |
| ), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| |
| assertThat(result).hasSize(2) |
| assertThat(result).contains(event1.consumePositionChange(0f, 771f)) |
| assertThat(result).contains(event2.consumePositionChange(0f, -720f)) |
| } |
| |
| @Test |
| fun dispatchChanges_2PointersShareCompletePath_changesAreUpdatedCorrectly() { |
| val child1: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 2 |
| PointerEventPass.PreUp -> 3 |
| else -> Int.MAX_VALUE |
| } |
| it.consumePositionChange( |
| 0f, |
| (it.positionChange().y.roundToInt() / yConsume).toFloat() |
| ) |
| } |
| }) |
| ) |
| val child2: PointerInputFilter = PointerInputFilterMock( |
| pointerInputHandler = |
| spy(StubPointerInputHandler { changes, pass, _ -> |
| changes.map { |
| val yConsume = |
| when (pass) { |
| PointerEventPass.InitialDown -> 5 |
| PointerEventPass.PreUp -> 7 |
| else -> Int.MAX_VALUE |
| } |
| it.consumePositionChange( |
| 0f, |
| (it.positionChange().y.roundToInt() / yConsume).toFloat() |
| ) |
| } |
| }) |
| ) |
| hitPathTracker.addHitPath(PointerId(3), listOf(child1, child2)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(child1, child2)) |
| val event1 = down(3).moveTo(10.milliseconds, 0f, 1000f) |
| val event2 = down(5).moveTo(10.milliseconds, 0f, -1000f) |
| |
| val (result, _) = hitPathTracker.dispatchChanges( |
| listOf(event1, event2) |
| ) |
| |
| verify(child1).onPointerInput( |
| eq(listOf(event1, event2)), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| verify(child2).onPointerInput( |
| eq( |
| listOf( |
| event1.consumePositionChange(0f, 500f), |
| event2.consumePositionChange(0f, -500f) |
| ) |
| ), |
| eq(PointerEventPass.InitialDown), |
| any() |
| ) |
| |
| verify(child2).onPointerInput( |
| eq( |
| listOf( |
| event1.consumePositionChange(0f, 600f), |
| event2.consumePositionChange(0f, -600f) |
| ) |
| ), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| verify(child1).onPointerInput( |
| eq( |
| listOf( |
| event1.consumePositionChange(0f, 657f), |
| event2.consumePositionChange(0f, -657f) |
| ) |
| ), |
| eq(PointerEventPass.PreUp), |
| any() |
| ) |
| |
| assertThat(result).hasSize(2) |
| assertThat(result).contains(event1.consumePositionChange(0f, 771f)) |
| assertThat(result).contains(event2.consumePositionChange(0f, -771f)) |
| } |
| |
| @Test |
| fun removeDetachedPointerInputFilters_noNodes_hitResultJustHasRootAndDoesNotCrash() { |
| val throwable = catchThrowable { |
| hitPathTracker.removeDetachedPointerInputFilters() |
| } |
| |
| assertThat(throwable).isNull() |
| assertThat(areEqual(hitPathTracker.root, NodeParent())) |
| } |
| |
| @Test |
| fun removeDetachedPointerInputFilters_complexNothingDetached_nothingRemovedNoCancelsCalled() { |
| |
| // Arrange. |
| |
| val pif1 = PointerInputFilterMock(isAttached = true) |
| val pif2 = PointerInputFilterMock(isAttached = true) |
| val pif3 = PointerInputFilterMock(isAttached = true) |
| val pif4 = PointerInputFilterMock(isAttached = true) |
| val pif5 = PointerInputFilterMock(isAttached = true) |
| val pif6 = PointerInputFilterMock(isAttached = true) |
| val pif7 = PointerInputFilterMock(isAttached = true) |
| val pif8 = PointerInputFilterMock(isAttached = true) |
| val pif9 = PointerInputFilterMock(isAttached = true) |
| |
| val pointerId1 = PointerId(1) |
| val pointerId2 = PointerId(2) |
| val pointerId3 = PointerId(3) |
| val pointerId4 = PointerId(4) |
| val pointerId5 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(pif1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(pif3, pif2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(pif6, pif5, pif4)) |
| hitPathTracker.addHitPath(pointerId4, listOf(pif9, pif7)) |
| hitPathTracker.addHitPath(pointerId5, listOf(pif9, pif8)) |
| |
| // Act. |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| // Assert. |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| children.add(Node(pif3).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(pif2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| children.add(Node(pif6).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(pif5).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(pif4).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| }) |
| children.add(Node(pif9).apply { |
| pointerIds.add(pointerId4) |
| pointerIds.add(pointerId5) |
| children.add(Node(pif7).apply { |
| pointerIds.add(pointerId4) |
| }) |
| children.add(Node(pif8).apply { |
| pointerIds.add(pointerId5) |
| }) |
| }) |
| } |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| |
| verify(pif1, never()).onCancel() |
| verify(pif2, never()).onCancel() |
| verify(pif3, never()).onCancel() |
| verify(pif4, never()).onCancel() |
| verify(pif5, never()).onCancel() |
| verify(pif7, never()).onCancel() |
| verify(pif8, never()).onCancel() |
| verify(pif9, never()).onCancel() |
| } |
| |
| // compositionRoot, root -> middle -> leaf |
| @Test |
| fun removeDetachedPointerInputFilters_1PathRootDetached_allRemovedAndCorrectCancels() { |
| val root = PointerInputFilterMock(isAttached = false) |
| val middle = PointerInputFilterMock(isAttached = false) |
| val leaf = PointerInputFilterMock(isAttached = false) |
| |
| hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| inOrder(leaf, middle, root) { |
| verify(leaf).onCancel() |
| verify(middle).onCancel() |
| verify(root).onCancel() |
| } |
| } |
| |
| // compositionRoot -> root, middle -> child |
| @Test |
| fun removeDetachedPointerInputFilters_1PathMiddleDetached_removesAndCancelsCorrect() { |
| val root = PointerInputFilterMock(isAttached = true) |
| val middle = PointerInputFilterMock(isAttached = false) |
| val child = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId = PointerId(0) |
| hitPathTracker.addHitPath(pointerId, listOf(root, middle, child)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(child, middle) { |
| verify(child).onCancel() |
| verify(middle).onCancel() |
| } |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root -> middle, leaf |
| @Test |
| fun removeDetachedPointerInputFilters_1PathLeafDetached_removesAndCancelsCorrect() { |
| val root = PointerInputFilterMock(isAttached = true) |
| val middle = PointerInputFilterMock(isAttached = true) |
| val leaf = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId = PointerId(0) |
| hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf).onCancel() |
| verify(middle, never()).onCancel() |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root1 -> middle1 -> leaf1 |
| // compositionRoot -> root2 -> middle2 -> leaf2 |
| // compositionRoot, root3 -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots1Detached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = true) |
| val leaf1 = PointerInputFilterMock(isAttached = true) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = true) |
| |
| val root3 = PointerInputFilterMock(isAttached = false) |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(leaf2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| |
| verify(leaf1, never()).onCancel() |
| verify(middle1, never()).onCancel() |
| verify(root1, never()).onCancel() |
| verify(leaf2, never()).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(root2, never()).onCancel() |
| inOrder(leaf3, middle3, root3) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| verify(root3).onCancel() |
| } |
| } |
| |
| // compositionRoot -> root1, middle1 -> leaf1 |
| // compositionRoot -> root2 -> middle2 -> leaf2 |
| // compositionRoot -> root3 -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots1MiddleDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = true) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = true) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(leaf2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(leaf3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| } |
| verify(root1, never()).onCancel() |
| verify(leaf2, never()).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(root2, never()).onCancel() |
| verify(leaf3, never()).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot -> root1 -> middle1 -> leaf1 |
| // compositionRoot -> root2 -> middle2, leaf2 |
| // compositionRoot -> root3 -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots1LeafDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = true) |
| val leaf1 = PointerInputFilterMock(isAttached = true) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = true) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(leaf3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf1, never()).onCancel() |
| verify(middle1, never()).onCancel() |
| verify(root1, never()).onCancel() |
| verify(leaf2).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(root2, never()).onCancel() |
| verify(leaf3, never()).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot, root1 -> middle1 -> leaf1 |
| // compositionRoot -> root2 -> middle2 -> leaf2 |
| // compositionRoot, root3 -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots2Detached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = false) |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = true) |
| |
| val root3 = PointerInputFilterMock(isAttached = false) |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(leaf2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| |
| inOrder(leaf1, middle1, root1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| verify(root1).onCancel() |
| } |
| verify(leaf2, never()).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(root2, never()).onCancel() |
| inOrder(leaf3, middle3, root3) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| verify(root3).onCancel() |
| } |
| } |
| |
| // compositionRoot -> root1, middle1 -> leaf1 |
| // compositionRoot -> root2, middle2 -> leaf2 |
| // compositionRoot -> root3 -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots2MiddlesDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = true) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(leaf3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| |
| inOrder(leaf1, middle1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| } |
| verify(root1, never()).onCancel() |
| inOrder(leaf2, middle2) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| } |
| verify(root2, never()).onCancel() |
| verify(leaf3, never()).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot -> root1 -> middle1 -> leaf1 |
| // compositionRoot -> root2 -> middle2, leaf2 |
| // compositionRoot -> root3 -> middle3, leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots2LeafsDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = true) |
| val leaf1 = PointerInputFilterMock(isAttached = true) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf1, never()).onCancel() |
| verify(middle1, never()).onCancel() |
| verify(root1, never()).onCancel() |
| verify(leaf2).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(root2, never()).onCancel() |
| verify(leaf3).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot, root1 -> middle1 -> leaf1 |
| // compositionRoot, root2 -> middle2 -> leaf2 |
| // compositionRoot, root3 -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots3Detached_allRemovedAndCancelsCorrect() { |
| val root1 = PointerInputFilterMock(isAttached = false) |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = false) |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = false) |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(PointerId(7), listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent() |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1, root1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| verify(root1).onCancel() |
| } |
| inOrder(leaf2, middle2, root2) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| verify(root2).onCancel() |
| } |
| inOrder(leaf3, middle3, root3) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| verify(root3).onCancel() |
| } |
| } |
| |
| // compositionRoot -> root1, middle1 -> leaf1 |
| // compositionRoot -> root2, middle2 -> leaf2 |
| // compositionRoot -> root3, middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots3MiddlesDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| } |
| verify(root1, never()).onCancel() |
| inOrder(leaf2, middle2) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| } |
| verify(root2, never()).onCancel() |
| inOrder(leaf3, middle3) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| } |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot -> root1 -> middle1, leaf1 |
| // compositionRoot -> root2 -> middle2, leaf2 |
| // compositionRoot -> root3 -> middle3, leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3Roots3LeafsDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = true) |
| val middle1 = PointerInputFilterMock(isAttached = true) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf1).onCancel() |
| verify(middle1, never()).onCancel() |
| verify(root1, never()).onCancel() |
| verify(leaf2).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(root2, never()).onCancel() |
| verify(leaf3).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot, root1 -> middle1 -> leaf1 |
| // compositionRoot -> root2, middle2, leaf2 |
| // compositionRoot -> root3 -> middle3, leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_3RootsStaggeredDetached_removesAndCancelsCorrect() { |
| |
| val root1 = PointerInputFilterMock(isAttached = false) |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val root2 = PointerInputFilterMock(isAttached = true) |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val root3 = PointerInputFilterMock(isAttached = true) |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| children.add(Node(root3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1, root1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| verify(root1).onCancel() |
| } |
| inOrder(leaf2, middle2) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| } |
| verify(root2, never()).onCancel() |
| verify(leaf3).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root3, never()).onCancel() |
| } |
| |
| // compositionRoot, root -> |
| // middle1 -> leaf1 |
| // middle2 -> leaf2 |
| // middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_rootWith3MiddlesDetached_allRemovedAndCorrectCancels() { |
| val root = PointerInputFilterMock(isAttached = false) |
| |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(root, middle1, leaf1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2)) |
| hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent() |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1, root) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| verify(root).onCancel() |
| } |
| inOrder(leaf2, middle2, root) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| verify(root).onCancel() |
| } |
| inOrder(leaf3, middle3, root) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| verify(root).onCancel() |
| } |
| } |
| |
| // compositionRoot -> root |
| // -> middle1 -> leaf1 |
| // -> middle2 -> leaf2 |
| // , middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_rootWith3Middles1Detached_removesAndCancelsCorrect() { |
| |
| val root = PointerInputFilterMock(isAttached = true) |
| |
| val middle1 = PointerInputFilterMock(isAttached = true) |
| val leaf1 = PointerInputFilterMock(isAttached = true) |
| |
| val middle2 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = true) |
| |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(middle1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| children.add(Node(middle2).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(leaf2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf3, middle3) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| } |
| verify(leaf2, never()).onCancel() |
| verify(middle2, never()).onCancel() |
| verify(leaf1, never()).onCancel() |
| verify(middle1, never()).onCancel() |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root |
| // , middle1 -> leaf1 |
| // , middle2 -> leaf2 |
| // -> middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_rootWith3Middles2Detached_removesAndCancelsCorrect() { |
| |
| val root = PointerInputFilterMock(isAttached = true) |
| |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val middle3 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = true) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(middle3).apply { |
| pointerIds.add(pointerId3) |
| children.add(Node(leaf3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| } |
| inOrder(leaf2, middle2) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| } |
| verify(leaf3, never()).onCancel() |
| verify(middle3, never()).onCancel() |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root |
| // , middle1 -> leaf1 |
| // , middle2 -> leaf2 |
| // , middle3 -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_rootWith3MiddlesAllDetached_allMiddlesRemoved() { |
| |
| val root = PointerInputFilterMock(isAttached = true) |
| |
| val middle1 = PointerInputFilterMock(isAttached = false) |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| |
| val middle2 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| |
| val middle3 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| inOrder(leaf1, middle1) { |
| verify(leaf1).onCancel() |
| verify(middle1).onCancel() |
| } |
| inOrder(leaf2, middle2) { |
| verify(leaf2).onCancel() |
| verify(middle2).onCancel() |
| } |
| inOrder(leaf3, middle3) { |
| verify(leaf3).onCancel() |
| verify(middle3).onCancel() |
| } |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root -> middle |
| // -> leaf1 |
| // , leaf2 |
| // -> leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_middleWith3Leafs1Detached_correctLeafRemoved() { |
| |
| val root = PointerInputFilterMock(isAttached = true) |
| |
| val middle = PointerInputFilterMock(isAttached = true) |
| |
| val leaf1 = PointerInputFilterMock(isAttached = true) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = true) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf2)) |
| hitPathTracker.addHitPath(pointerId3, listOf(root, middle, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(leaf1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| children.add(Node(leaf3).apply { |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf1, never()).onCancel() |
| verify(leaf2).onCancel() |
| verify(leaf3, never()).onCancel() |
| verify(middle, never()).onCancel() |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root -> middle |
| // , leaf1 |
| // -> leaf2 |
| // , leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_middleWith3Leafs2Detached_correctLeafsRemoved() { |
| |
| val root = PointerInputFilterMock(isAttached = true) |
| |
| val middle = PointerInputFilterMock(isAttached = true) |
| |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = true) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(root, middle, leaf1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2)) |
| hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(leaf2).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf1).onCancel() |
| verify(leaf2, never()).onCancel() |
| verify(leaf3).onCancel() |
| verify(middle, never()).onCancel() |
| verify(root, never()).onCancel() |
| } |
| |
| // compositionRoot -> root -> middle |
| // , leaf1 |
| // , leaf2 |
| // , leaf3 |
| @Test |
| fun removeDetachedPointerInputFilters_middleWith3LeafsAllDetached_allLeafsRemoved() { |
| |
| val root = PointerInputFilterMock(isAttached = true) |
| |
| val middle = PointerInputFilterMock(isAttached = true) |
| |
| val leaf1 = PointerInputFilterMock(isAttached = false) |
| val leaf2 = PointerInputFilterMock(isAttached = false) |
| val leaf3 = PointerInputFilterMock(isAttached = false) |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| val pointerId3 = PointerId(7) |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(root, middle, leaf1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2)) |
| hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3)) |
| |
| hitPathTracker.removeDetachedPointerInputFilters() |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId1) |
| pointerIds.add(pointerId2) |
| pointerIds.add(pointerId3) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| verify(leaf1).onCancel() |
| verify(leaf2).onCancel() |
| verify(leaf3).onCancel() |
| verify(middle, never()).onCancel() |
| verify(root, never()).onCancel() |
| } |
| |
| // arrange: root(3) -> middle(3) -> leaf(3) |
| // act: 3 is removed |
| // assert: no path |
| @Test |
| fun removeHitPath_onePathPointerIdRemoved_hitTestResultIsEmpty() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId = PointerId(3) |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(root, middle, leaf)) |
| |
| hitPathTracker.removeHitPath(pointerId) |
| |
| val expectedRoot = NodeParent() |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // arrange: root(3) -> middle(3) -> leaf(3) |
| // act: 99 is removed |
| // assert: root(3) -> middle(3) -> leaf(3) |
| @Test |
| fun removeHitPath_onePathOtherPointerIdRemoved_hitTestResultIsNotChanged() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(99) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf)) |
| |
| hitPathTracker.removeHitPath(pointerId2) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // Arrange: |
| // root(3) -> middle(3) -> leaf(3) |
| // root(5) -> middle(5) -> leaf(5) |
| // |
| // Act: |
| // 5 is removed |
| // |
| // Act: |
| // root(3) -> middle(3) -> leaf(3) |
| @Test |
| fun removeHitPath_2IndependentPaths1PointerIdRemoved_resultContainsRemainingPath() { |
| val root1: PointerInputFilter = mock() |
| val middle1: PointerInputFilter = mock() |
| val leaf1: PointerInputFilter = mock() |
| |
| val root2: PointerInputFilter = mock() |
| val middle2: PointerInputFilter = mock() |
| val leaf2: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root1, middle1, leaf1)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2)) |
| |
| hitPathTracker.removeHitPath(pointerId2) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle1).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf1).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // root(3,5) -> middle(3,5) -> leaf(3,5) |
| // 3 is removed |
| // root(5) -> middle(5) -> leaf(5) |
| @Test |
| fun removeHitPath_2PathsShareNodes1PointerIdRemoved_resultContainsRemainingPath() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf)) |
| |
| hitPathTracker.removeHitPath(pointerId1) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(leaf).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // Arrange: root(3,5) -> middle(3,5) -> leaf(3) |
| // Act: 3 is removed |
| // Assert: root(5) -> middle(5) |
| @Test |
| fun removeHitPath_2PathsShare2NodesLongPathPointerIdRemoved_resultJustHasShortPath() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle)) |
| |
| hitPathTracker.removeHitPath(pointerId1) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId2) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId2) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // Arrange: root(3,5) -> middle(3,5) -> leaf(3) |
| // Act: 5 is removed |
| // Assert: root(3) -> middle(3) -> leaf(3) |
| @Test |
| fun removeHitPath_2PathsShare2NodesShortPathPointerIdRemoved_resultJustHasLongPath() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root, middle)) |
| |
| hitPathTracker.removeHitPath(pointerId2) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // Arrange: root(3,5) -> middle(3) -> leaf(3) |
| // Act: 3 is removed |
| // Assert: root(5) |
| @Test |
| fun removeHitPath_2PathsShare1NodeLongPathPointerIdRemoved_resultJustHasShortPath() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root)) |
| |
| hitPathTracker.removeHitPath(pointerId1) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId2) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| // Arrange: root(3,5) -> middle(3) -> leaf(3) |
| // Act: 5 is removed |
| // Assert: root(3) -> middle(3) -> leaf(3) |
| @Test |
| fun removeHitPath_2PathsShare1NodeShortPathPointerIdRemoved_resultJustHasLongPath() { |
| val root: PointerInputFilter = mock() |
| val middle: PointerInputFilter = mock() |
| val leaf: PointerInputFilter = mock() |
| |
| val pointerId1 = PointerId(3) |
| val pointerId2 = PointerId(5) |
| |
| hitPathTracker.addHitPath(pointerId1, listOf(root, middle, leaf)) |
| hitPathTracker.addHitPath(pointerId2, listOf(root)) |
| |
| hitPathTracker.removeHitPath(pointerId2) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(root).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(middle).apply { |
| pointerIds.add(pointerId1) |
| children.add(Node(leaf).apply { |
| pointerIds.add(pointerId1) |
| }) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| @Test |
| fun processCancel_nothingTracked_doesNotCrash() { |
| hitPathTracker.processCancel() |
| } |
| |
| // Pin -> Ln |
| @Test |
| fun processCancel_singlePin_cancelHandlerIsCalled() { |
| val pif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(pif)) |
| |
| hitPathTracker.processCancel() |
| |
| verify(pif).onCancel() |
| } |
| |
| // Pin -> Pin -> Pin |
| @Test |
| fun processCancel_3Pins_cancelHandlersCalledOnceInOrder() { |
| val childPif: PointerInputFilter = PointerInputFilterMock() |
| val middlePif: PointerInputFilter = PointerInputFilterMock() |
| val parentPif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath( |
| PointerId(3), |
| listOf(parentPif, middlePif, childPif) |
| ) |
| |
| hitPathTracker.processCancel() |
| |
| inOrder( |
| parentPif, |
| middlePif, |
| childPif |
| ) { |
| verify(childPif).onCancel() |
| verify(middlePif).onCancel() |
| verify(parentPif).onCancel() |
| } |
| } |
| |
| // PIN -> PIN |
| // PIN -> PIN |
| @Test |
| fun processCancel_2IndependentPathsFromRoot_cancelHandlersCalledOnceInOrder() { |
| val pifParent1: PointerInputFilter = PointerInputFilterMock() |
| val pifChild1: PointerInputFilter = PointerInputFilterMock() |
| val pifParent2: PointerInputFilter = PointerInputFilterMock() |
| val pifChild2: PointerInputFilter = PointerInputFilterMock() |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(pifParent1, pifChild1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(pifParent2, pifChild2)) |
| |
| hitPathTracker.processCancel() |
| |
| inOrder(pifParent1, pifChild1) { |
| verify(pifChild1).onCancel() |
| verify(pifParent1).onCancel() |
| } |
| inOrder(pifParent2, pifChild2) { |
| verify(pifChild2).onCancel() |
| verify(pifParent2).onCancel() |
| } |
| } |
| |
| // PIN -> PIN |
| // -> PIN |
| @Test |
| fun processCancel_2BranchingPaths_cancelHandlersCalledOnceInOrder() { |
| val pifParent: PointerInputFilter = PointerInputFilterMock() |
| val pifChild1: PointerInputFilter = PointerInputFilterMock() |
| val pifChild2: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(pifParent, pifChild1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(pifParent, pifChild2)) |
| |
| hitPathTracker.processCancel() |
| |
| inOrder(pifParent, pifChild1) { |
| verify(pifChild1).onCancel() |
| verify(pifParent).onCancel() |
| } |
| inOrder(pifParent, pifChild2) { |
| verify(pifChild2).onCancel() |
| verify(pifParent).onCancel() |
| } |
| } |
| |
| // Pin -> Ln |
| @Test |
| fun processCancel_singlePin_cleared() { |
| val pif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(pif)) |
| |
| hitPathTracker.processCancel() |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| } |
| |
| // Pin -> Pin -> Pin |
| @Test |
| fun processCancel_3Pins_cleared() { |
| val childPif: PointerInputFilter = PointerInputFilterMock() |
| val middlePif: PointerInputFilter = PointerInputFilterMock() |
| val parentPif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath( |
| PointerId(3), |
| listOf(parentPif, middlePif, childPif) |
| ) |
| |
| hitPathTracker.processCancel() |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| } |
| |
| // PIN -> PIN |
| // PIN -> PIN |
| @Test |
| fun processCancel_2IndependentPathsFromRoot_cleared() { |
| val pifParent1: PointerInputFilter = PointerInputFilterMock() |
| val pifChild1: PointerInputFilter = PointerInputFilterMock() |
| val pifParent2: PointerInputFilter = PointerInputFilterMock() |
| val pifChild2: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(pifParent1, pifChild1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(pifParent2, pifChild2)) |
| |
| hitPathTracker.processCancel() |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| } |
| |
| // PIN -> PIN |
| // -> PIN |
| @Test |
| fun processCancel_2BranchingPaths_cleared() { |
| val pifParent: PointerInputFilter = PointerInputFilterMock() |
| val pifChild1: PointerInputFilter = PointerInputFilterMock() |
| val pifChild2: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(3), listOf(pifParent, pifChild1)) |
| hitPathTracker.addHitPath(PointerId(5), listOf(pifParent, pifChild2)) |
| |
| hitPathTracker.processCancel() |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1NodeItDispatches_nothingReceivesDispatch() { |
| |
| // Arrange |
| |
| lateinit var dispatcher: CustomEventDispatcher |
| |
| val pif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(pif)) |
| |
| val event = TestCustomEvent("test") |
| |
| // Act |
| |
| dispatcher.dispatchCustomEvent(event) |
| |
| // Assert |
| |
| verify(pif, never()).onCustomEvent(any(), any()) |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1Path3NodesParentDispatches_dispatchCorrect() { |
| dispatchCustomEvent_1Path3Nodes_dispatchCorrect(DispatchingPif.Parent) |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1Path3NodesMiddleDispatches_dispatchCorrect() { |
| dispatchCustomEvent_1Path3Nodes_dispatchCorrect(DispatchingPif.Middle) |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1Path3NodesChildDispatches_dispatchCorrect() { |
| dispatchCustomEvent_1Path3Nodes_dispatchCorrect(DispatchingPif.Child) |
| } |
| |
| private enum class DispatchingPif { |
| Parent, Middle, Child |
| } |
| |
| private fun dispatchCustomEvent_1Path3Nodes_dispatchCorrect( |
| dispatchingPif: DispatchingPif |
| ) { |
| // Arrange |
| |
| lateinit var dispatcher: CustomEventDispatcher |
| lateinit var parentPif: PointerInputFilter |
| lateinit var middlePif: PointerInputFilter |
| lateinit var childPif: PointerInputFilter |
| |
| lateinit var pifThatDispatches: PointerInputFilter |
| lateinit var seniorPif: PointerInputFilter |
| lateinit var juniorPif: PointerInputFilter |
| val dispatcherInitHandler: (CustomEventDispatcher) -> Unit = { dispatcher = it } |
| |
| when (dispatchingPif) { |
| DispatchingPif.Parent -> { |
| parentPif = PointerInputFilterMock( |
| initHandler = dispatcherInitHandler |
| ) |
| pifThatDispatches = parentPif |
| middlePif = PointerInputFilterMock() |
| seniorPif = middlePif |
| childPif = PointerInputFilterMock() |
| juniorPif = childPif |
| } |
| DispatchingPif.Middle -> { |
| parentPif = PointerInputFilterMock() |
| seniorPif = parentPif |
| middlePif = PointerInputFilterMock( |
| initHandler = dispatcherInitHandler |
| ) |
| pifThatDispatches = middlePif |
| childPif = PointerInputFilterMock() |
| juniorPif = childPif |
| } |
| DispatchingPif.Child -> { |
| parentPif = PointerInputFilterMock() |
| seniorPif = parentPif |
| middlePif = PointerInputFilterMock() |
| juniorPif = middlePif |
| childPif = PointerInputFilterMock( |
| initHandler = dispatcherInitHandler |
| ) |
| pifThatDispatches = childPif |
| } |
| } |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(parentPif, middlePif, childPif)) |
| |
| val event = TestCustomEvent("test") |
| |
| // Act |
| |
| dispatcher.dispatchCustomEvent(event) |
| |
| // Assert |
| |
| verify(seniorPif, times(5)).onCustomEvent(any(), any()) |
| verify(juniorPif, times(5)).onCustomEvent(any(), any()) |
| inOrder(seniorPif, juniorPif) { |
| verify(seniorPif).onCustomEvent(event, PointerEventPass.InitialDown) |
| verify(juniorPif).onCustomEvent(event, PointerEventPass.InitialDown) |
| verify(juniorPif).onCustomEvent(event, PointerEventPass.PreUp) |
| verify(seniorPif).onCustomEvent(event, PointerEventPass.PreUp) |
| verify(seniorPif).onCustomEvent(event, PointerEventPass.PreDown) |
| verify(juniorPif).onCustomEvent(event, PointerEventPass.PreDown) |
| verify(juniorPif).onCustomEvent(event, PointerEventPass.PostUp) |
| verify(seniorPif).onCustomEvent(event, PointerEventPass.PostUp) |
| verify(seniorPif).onCustomEvent(event, PointerEventPass.PostDown) |
| verify(juniorPif).onCustomEvent(event, PointerEventPass.PostDown) |
| } |
| verify(pifThatDispatches, never()).onCustomEvent(any(), any()) |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1Parent2ChildrenParentDispatches_dispatchCorrect() { |
| |
| lateinit var dispatcher: CustomEventDispatcher |
| |
| val parentPin = PointerInputFilterMock(initHandler = { dispatcher = it }) |
| val childPin1 = PointerInputFilterMock() |
| val childPin2 = PointerInputFilterMock() |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(parentPin, childPin1)) |
| hitPathTracker.addHitPath(PointerId(4), listOf(parentPin, childPin2)) |
| |
| val event = TestCustomEvent("test") |
| |
| // Act |
| |
| dispatcher.dispatchCustomEvent(event) |
| |
| // Assert |
| inOrder(childPin1) { |
| verify(childPin1).onCustomEvent(event, PointerEventPass.InitialDown) |
| verify(childPin1).onCustomEvent(event, PointerEventPass.PreUp) |
| verify(childPin1).onCustomEvent(event, PointerEventPass.PreDown) |
| verify(childPin1).onCustomEvent(event, PointerEventPass.PostUp) |
| verify(childPin1).onCustomEvent(event, PointerEventPass.PostDown) |
| } |
| inOrder(childPin2) { |
| verify(childPin2).onCustomEvent(event, PointerEventPass.InitialDown) |
| verify(childPin2).onCustomEvent(event, PointerEventPass.PreUp) |
| verify(childPin2).onCustomEvent(event, PointerEventPass.PreDown) |
| verify(childPin2).onCustomEvent(event, PointerEventPass.PostUp) |
| verify(childPin2).onCustomEvent(event, PointerEventPass.PostDown) |
| } |
| verify(parentPin, never()).onCustomEvent(any(), any()) |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1Parent2ChildrenChild1Dispatches_dispatchCorrect() { |
| dispatchCustomEvent_1Parent2ChildrenChildDispatches_dispatchCorrect( |
| true |
| ) |
| } |
| |
| @Test |
| fun dispatchCustomEvent_1Parent2ChildrenChild2Dispatches_dispatchCorrect() { |
| dispatchCustomEvent_1Parent2ChildrenChildDispatches_dispatchCorrect( |
| false |
| ) |
| } |
| |
| private fun dispatchCustomEvent_1Parent2ChildrenChildDispatches_dispatchCorrect( |
| firstChildDispatches: Boolean |
| ) { |
| // Arrange |
| |
| val parentPif = PointerInputFilterMock() |
| lateinit var childPif1: PointerInputFilter |
| lateinit var childPif2: PointerInputFilter |
| |
| lateinit var dispatcher: CustomEventDispatcher |
| val initHandler: (CustomEventDispatcher) -> Unit = { dispatcher = it } |
| |
| if (firstChildDispatches) { |
| childPif1 = PointerInputFilterMock( |
| initHandler = initHandler |
| ) |
| childPif2 = PointerInputFilterMock() |
| } else { |
| childPif1 = PointerInputFilterMock() |
| childPif2 = PointerInputFilterMock( |
| initHandler = initHandler |
| ) |
| } |
| |
| hitPathTracker.addHitPath(PointerId(3), listOf(parentPif, childPif1)) |
| hitPathTracker.addHitPath(PointerId(4), listOf(parentPif, childPif2)) |
| |
| val event = TestCustomEvent("test") |
| |
| // Act |
| |
| dispatcher.dispatchCustomEvent(event) |
| |
| // Assert |
| inOrder(parentPif) { |
| verify(parentPif).onCustomEvent(event, PointerEventPass.InitialDown) |
| verify(parentPif).onCustomEvent(event, PointerEventPass.PreUp) |
| verify(parentPif).onCustomEvent(event, PointerEventPass.PreDown) |
| verify(parentPif).onCustomEvent(event, PointerEventPass.PostUp) |
| verify(parentPif).onCustomEvent(event, PointerEventPass.PostDown) |
| } |
| verify(childPif1, never()).onCustomEvent(any(), any()) |
| verify(childPif1, never()).onCustomEvent(any(), any()) |
| } |
| |
| // Tests related to reporting whether or not a pointer input filter was dispatched to. |
| |
| @Test |
| fun dispatchChanges_noNodes_reportsWasDispatchedToNothing() { |
| val (_, hitSomething) = hitPathTracker.dispatchChanges(listOf(down(0))) |
| assertThat(hitSomething).isFalse() |
| } |
| |
| @Test |
| fun dispatchChanges_1NodeDispatchToNode_reportsWasDispatchedToSomething() { |
| val pif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif)) |
| |
| val (_, hitSomething) = hitPathTracker.dispatchChanges(listOf(down(13))) |
| |
| assertThat(hitSomething).isTrue() |
| } |
| |
| @Test |
| fun dispatchChanges_1NodeDispatchToDifferentNode_reportsWasDispatchedToNothing() { |
| val pif: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif)) |
| |
| val (_, hitSomething) = hitPathTracker.dispatchChanges(listOf(down(69))) |
| |
| assertThat(hitSomething).isFalse() |
| } |
| |
| // Tests related to retaining and releasing hit paths. |
| |
| /** |
| * Verifies that if a hit path is added and retained, when it is removed, it is not actually |
| * removed. |
| */ |
| @Test |
| fun removeHitPath_idRetained_nodeIsRetained() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val pif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif)) |
| dispatcher.retainHitPaths(setOf(PointerId(13))) |
| |
| hitPathTracker.removeHitPath(PointerId(13)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif).apply { |
| pointerIds.add(PointerId(13)) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| /** |
| * Verifies that if there are 2 branching hit paths, 1 is retained, and both are removed, the |
| * 1 that was retained remains. |
| */ |
| @Test |
| fun removeHitPath_2Branches1RetainedBothRemoved_retainedBranchRemains() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val parentPif: PointerInputFilter = PointerInputFilterMock() |
| val childPif1: PointerInputFilter = PointerInputFilterMock() |
| val childPif2: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| |
| hitPathTracker.addHitPath(PointerId(1), listOf(parentPif, childPif1)) |
| hitPathTracker.addHitPath(PointerId(2), listOf(parentPif, childPif2)) |
| dispatcher.retainHitPaths(setOf(PointerId(1))) |
| |
| hitPathTracker.removeHitPath(PointerId(1)) |
| hitPathTracker.removeHitPath(PointerId(2)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(parentPif).apply { |
| pointerIds.add(PointerId(1)) |
| children.add(Node(childPif1).apply { |
| pointerIds.add(PointerId(1)) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| /** |
| * Verifies that if there are 2 branching hit paths, both are retained, and both are removed, |
| * that both remain. |
| */ |
| @Test |
| fun removeHitPath_2Branches2RetainedBothRemoved_bothBranchesRemain() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val parentPif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| val childPif1: PointerInputFilter = PointerInputFilterMock() |
| val childPif2: PointerInputFilter = PointerInputFilterMock() |
| |
| hitPathTracker.addHitPath(PointerId(1), listOf(parentPif, childPif1)) |
| hitPathTracker.addHitPath(PointerId(2), listOf(parentPif, childPif2)) |
| dispatcher.retainHitPaths(setOf(PointerId(1), PointerId(2))) |
| |
| hitPathTracker.removeHitPath(PointerId(1)) |
| hitPathTracker.removeHitPath(PointerId(2)) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(parentPif).apply { |
| pointerIds.addAll(listOf(PointerId(1), PointerId(2))) |
| children.add(Node(childPif1).apply { |
| pointerIds.add(PointerId(1)) |
| }) |
| children.add(Node(childPif2).apply { |
| pointerIds.add(PointerId(2)) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| /** |
| * Verifies that if a hit path is retained, then removed, then released, it is removed. |
| */ |
| @Test |
| fun releasePointerId_idRetainedAndPathRemoved_nodeIsRemoved() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val pif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif)) |
| dispatcher.retainHitPaths(setOf(PointerId(13))) |
| hitPathTracker.removeHitPath(PointerId(13)) |
| |
| dispatcher.releaseHitPaths(setOf(PointerId(13))) |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| } |
| |
| /** |
| * Verifies that if there are 2 hit paths, both are retained, both are removed, and |
| * then 1 is released, the other retained branch remains. |
| */ |
| @Test |
| fun releasePointerId_2Branches2RetainedBothRemoved1Released_correctBranchRemains() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val parentPif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| val childPif1: PointerInputFilter = PointerInputFilterMock() |
| val childPif2: PointerInputFilter = PointerInputFilterMock() |
| |
| hitPathTracker.addHitPath(PointerId(1), listOf(parentPif, childPif1)) |
| hitPathTracker.addHitPath(PointerId(2), listOf(parentPif, childPif2)) |
| dispatcher.retainHitPaths(setOf(PointerId(1), PointerId(2))) |
| |
| hitPathTracker.removeHitPath(PointerId(1)) |
| hitPathTracker.removeHitPath(PointerId(2)) |
| |
| dispatcher.releaseHitPaths(setOf(PointerId(1))) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(parentPif).apply { |
| pointerIds.add(PointerId(2)) |
| children.add(Node(childPif2).apply { |
| pointerIds.add(PointerId(2)) |
| }) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| /** |
| * Verifies that if there are 2 hit paths, 1 is retained, they are both removed, and |
| * then the one that was retained is released, that no branches remain. |
| */ |
| @Test |
| fun releasePointerId_2Branches1RetainedBothRemoved1Released_noBranchesRemain() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val parentPif: PointerInputFilter = PointerInputFilterMock() |
| val childPif1: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| val childPif2: PointerInputFilter = PointerInputFilterMock() |
| |
| hitPathTracker.addHitPath(PointerId(1), listOf(parentPif, childPif1)) |
| hitPathTracker.addHitPath(PointerId(2), listOf(parentPif, childPif2)) |
| dispatcher.retainHitPaths(setOf(PointerId(1))) |
| |
| hitPathTracker.removeHitPath(PointerId(1)) |
| hitPathTracker.removeHitPath(PointerId(2)) |
| |
| dispatcher.releaseHitPaths(setOf(PointerId(1))) |
| |
| assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue() |
| } |
| |
| /** |
| * Verifies that if a hit path is retained and then released (without it ever actually being |
| * removed) then the hit path remains. |
| */ |
| @Test |
| fun releasePointerId_idRetainedButPathNotRemoved_pathNotRemoved() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val pif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif)) |
| dispatcher.retainHitPaths(setOf(PointerId(13))) |
| |
| dispatcher.releaseHitPaths(setOf(PointerId(13))) |
| |
| val expectedRoot = NodeParent().apply { |
| children.add(Node(pif).apply { |
| pointerIds.add(PointerId(13)) |
| }) |
| } |
| |
| assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue() |
| } |
| |
| /** |
| * Verifies that if a hit path is retained, and then removed, and a dispatch of a |
| * custom event occurs, it will be dispatched to the retained path. |
| */ |
| @Test |
| fun dispatchCustomEvent_idRetainedAndPathRemoved_customEventReachesNode() { |
| lateinit var dispatcher: CustomEventDispatcher |
| val pif: PointerInputFilter = PointerInputFilterMock( |
| initHandler = { dispatcher = it } |
| ) |
| val pif2: PointerInputFilter = PointerInputFilterMock() |
| hitPathTracker.addHitPath(PointerId(13), listOf(pif, pif2)) |
| dispatcher.retainHitPaths(setOf(PointerId(13))) |
| hitPathTracker.removeHitPath(PointerId(13)) |
| val event = TestCustomEvent("87483") |
| |
| dispatcher.dispatchCustomEvent(event) |
| |
| inOrder(pif2) { |
| verify(pif2).onCustomEvent(event, PointerEventPass.InitialDown) |
| verify(pif2).onCustomEvent(event, PointerEventPass.PreUp) |
| verify(pif2).onCustomEvent(event, PointerEventPass.PreDown) |
| verify(pif2).onCustomEvent(event, PointerEventPass.PostUp) |
| verify(pif2).onCustomEvent(event, PointerEventPass.PostDown) |
| } |
| } |
| |
| private fun areEqual(actualNode: NodeParent, expectedNode: NodeParent): Boolean { |
| var check = true |
| |
| if (actualNode.children.size != expectedNode.children.size) { |
| return false |
| } |
| for (child in actualNode.children) { |
| check = check && expectedNode.children.any { |
| areEqual(child, it) |
| } |
| } |
| |
| return check |
| } |
| |
| private fun areEqual(actualNode: Node, expectedNode: Node): Boolean { |
| if (actualNode.pointerInputFilter !== expectedNode.pointerInputFilter) { |
| return false |
| } |
| |
| if (actualNode.pointerIds.size != expectedNode.pointerIds.size) { |
| return false |
| } |
| var check = true |
| actualNode.pointerIds.forEach { |
| check = check && expectedNode.pointerIds.contains(it) |
| } |
| if (!check) { |
| return false |
| } |
| |
| if (actualNode.children.size != expectedNode.children.size) { |
| return false |
| } |
| for (child in actualNode.children) { |
| check = check && expectedNode.children.any { |
| areEqual(child, it) |
| } |
| } |
| |
| return check |
| } |
| } |
| |
| fun PointerInputFilterMock( |
| initHandler: (CustomEventDispatcher) -> Unit = mock(), |
| pointerInputHandler: PointerInputHandler = spy(StubPointerInputHandler()), |
| isAttached: Boolean = true |
| ): PointerInputFilter = |
| spy( |
| PointerInputFilterStub( |
| pointerInputHandler, |
| initHandler |
| ).apply { |
| layoutCoordinates = LayoutCoordinatesStub(isAttached) |
| } |
| ) |
| |
| open class PointerInputFilterStub( |
| val pointerInputHandler: PointerInputHandler, |
| val initHandler: (CustomEventDispatcher) -> Unit |
| ) : PointerInputFilter() { |
| |
| override fun onPointerInput( |
| changes: List<PointerInputChange>, |
| pass: PointerEventPass, |
| bounds: IntSize |
| ): List<PointerInputChange> { |
| return pointerInputHandler(changes, pass, bounds) |
| } |
| |
| override fun onCancel() {} |
| |
| override fun onInit(customEventDispatcher: CustomEventDispatcher) { |
| initHandler(customEventDispatcher) |
| } |
| |
| override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {} |
| } |
| |
| internal data class TestCustomEvent(val value: String) : CustomEvent |
| |
| class LayoutCoordinatesStub( |
| override val isAttached: Boolean = true |
| ) : LayoutCoordinates { |
| |
| override val size: IntSize |
| get() = IntSize(Constraints.Infinity, Constraints.Infinity) |
| |
| override val providedAlignmentLines: Set<AlignmentLine> |
| get() = TODO("not implemented") |
| |
| override val parentCoordinates: LayoutCoordinates? |
| get() = TODO("not implemented") |
| |
| override fun globalToLocal(global: Offset): Offset { |
| TODO("not implemented") |
| } |
| |
| override fun localToGlobal(local: Offset): Offset { |
| return local |
| } |
| |
| override fun localToRoot(local: Offset): Offset { |
| TODO("not implemented") |
| } |
| |
| override fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset { |
| TODO("not implemented") |
| } |
| |
| override fun childBoundingBox(child: LayoutCoordinates): PxBounds { |
| TODO("not implemented") |
| } |
| |
| override fun get(line: AlignmentLine): Int? { |
| TODO("not implemented") |
| } |
| } |