[go: nahoru, domu]

blob: 8a1f817d52030a4561ac6f481bd9aba37ec1752c [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.fragment.app
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.test.EmptyFragmentTestActivity
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@SmallTest
class SpecialEffectsControllerTest {
@get:Rule
val rule = DetectLeaksAfterTestSuccess()
@Test
fun factoryCreateController() {
val map = mutableMapOf<ViewGroup, TestSpecialEffectsController>()
val factory = SpecialEffectsControllerFactory { container ->
TestSpecialEffectsController(container).also {
map[container] = it
}
}
val container = FrameLayout(InstrumentationRegistry.getInstrumentation().context)
val controller = factory.createController(container)
assertThat(controller)
.isEqualTo(map[container])
assertThat(controller.container)
.isEqualTo(container)
// Ensure that a new container gets a new controller
val secondContainer = FrameLayout(InstrumentationRegistry.getInstrumentation().context)
val secondController = factory.createController(secondContainer)
assertThat(secondController)
.isEqualTo(map[secondContainer])
assertThat(secondController)
.isNotEqualTo(controller)
}
@Test
fun getOrCreateController() {
var count = 0
val map = mutableMapOf<ViewGroup, TestSpecialEffectsController>()
val factory = SpecialEffectsControllerFactory { container ->
count++
TestSpecialEffectsController(container).also {
map[container] = it
}
}
val container = FrameLayout(InstrumentationRegistry.getInstrumentation().context)
val controller = SpecialEffectsController.getOrCreateController(container, factory)
assertThat(controller)
.isEqualTo(map[container])
assertThat(controller.container)
.isEqualTo(container)
assertThat(count)
.isEqualTo(1)
// Recreating the controller shouldn't cause the count to increase
val recreatedController = SpecialEffectsController.getOrCreateController(
container, factory
)
assertThat(recreatedController)
.isEqualTo(controller)
assertThat(recreatedController.container)
.isEqualTo(container)
assertThat(count)
.isEqualTo(1)
// But creating a controller for a different view returns a new instance
val secondContainer = FrameLayout(InstrumentationRegistry.getInstrumentation().context)
val secondController = SpecialEffectsController.getOrCreateController(
secondContainer, factory
)
assertThat(secondController)
.isEqualTo(map[secondContainer])
assertThat(secondController.container)
.isEqualTo(secondContainer)
assertThat(count)
.isEqualTo(2)
}
@Test
fun noExecuteIfEmpty() {
val container = FrameLayout(InstrumentationRegistry.getInstrumentation().context)
val controller = InstantSpecialEffectsController(container)
controller.executePendingOperations()
assertThat(controller.executeOperationsCallCount).isEqualTo(0)
}
@MediumTest
@Test
fun enqueueAddAndExecute() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
InstantSpecialEffectsController(it)
}
val fragment = StrictViewFragment()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment.mFragmentManager = fm
fragment.mAdded = true
fragment.mContainerId = android.R.id.content
fragmentStateManager.setFragmentManagerState(Fragment.ACTIVITY_CREATED)
onActivity {
// This moves the Fragment up to ACTIVITY_CREATED,
// calling enqueueAdd() under the hood
fragmentStateManager.moveToExpectedState()
}
assertThat(fragment.view)
.isNotNull()
// setFragmentManagerState() doesn't call moveToExpectedState() itself
fragmentStateManager.setFragmentManagerState(Fragment.STARTED)
val controller = SpecialEffectsController.getOrCreateController(container, fm)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
onActivity {
// However, executePendingOperations(), since we're using our
// TestSpecialEffectsController, does immediately call complete()
// which in turn calls moveToExpectedState()
controller.executePendingOperations()
}
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isNull()
// Assert that we actually moved to the STARTED state
assertThat(fragment.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
}
}
@Ignore // Ignore this test until we find a way to better test this scenario.
@MediumTest
@Test
fun ensureOnlyChangeContainerStatusForCompletedOperation() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
TestSpecialEffectsController(it)
}
val fragment1 = StrictViewFragment()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager1 = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment1
)
val fragment2 = StrictViewFragment()
val fragmentStateManager2 = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment2
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment1.mFragmentManager = fm
fragment1.mAdded = true
fragment1.mContainerId = android.R.id.content
fragment2.mFragmentManager = fm
fragment2.mAdded = true
fragment2.mContainerId = android.R.id.content
fragmentStateManager1.setFragmentManagerState(Fragment.ACTIVITY_CREATED)
fragmentStateManager2.setFragmentManagerState(Fragment.ACTIVITY_CREATED)
val controller = SpecialEffectsController.getOrCreateController(container, fm)
as TestSpecialEffectsController
onActivity {
// This moves the Fragment up to ACTIVITY_CREATED,
// calling enqueueAdd() under the hood
fragmentStateManager1.moveToExpectedState()
fragmentStateManager2.moveToExpectedState()
controller.executePendingOperations()
}
assertThat(fragment1.view)
.isNotNull()
// setFragmentManagerState() doesn't call moveToExpectedState() itself
fragmentStateManager1.setFragmentManagerState(Fragment.STARTED)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager1))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
fragmentStateManager2.setFragmentManagerState(Fragment.STARTED)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager2))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
val operation2 = controller.operationsToExecute[1]
var awaitingChanges = true
operation2.addCompletionListener {
awaitingChanges = operation2.isAwaitingContainerChanges
}
val operation = controller.operationsToExecute[0]
onActivity {
operation.complete()
}
assertThat(awaitingChanges).isTrue()
}
}
@MediumTest
@Test
fun enqueueRemoveAndExecute() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
InstantSpecialEffectsController(it)
}
val fragment = StrictViewFragment()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment.mFragmentManager = fm
fragment.mAdded = true
fragment.mContainerId = android.R.id.content
fragmentStateManager.setFragmentManagerState(Fragment.STARTED)
val controller = SpecialEffectsController.getOrCreateController(container, fm)
onActivity {
// moveToExpectedState() first to call enqueueAdd()
fragmentStateManager.moveToExpectedState()
// Then executePendingOperations() to clear that out
controller.executePendingOperations()
}
assertThat(fragment.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
// setFragmentManagerState() doesn't call moveToExpectedState() itself
fragmentStateManager.setFragmentManagerState(Fragment.CREATED)
onActivity {
fragmentStateManager.moveToExpectedState()
}
// setFragmentManagerState() doesn't call moveToExpectedState() itself
fragmentStateManager.setFragmentManagerState(Fragment.ATTACHED)
onActivity {
// However, executePendingOperations(), since we're using our
// TestSpecialEffectsController, does immediately call complete()
// which in turn calls moveToExpectedState()
controller.executePendingOperations()
}
// Assert that we actually moved to the ATTACHED state
assertThat(fragment.calledOnDestroy)
.isTrue()
assertThat(fragment.calledOnDetach)
.isFalse()
}
}
@MediumTest
@Test
@Ignore("b/308684873")
fun enqueueAddAndCancel() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
TestSpecialEffectsController(it)
}
val fragment = StrictViewFragment()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment.mFragmentManager = fm
fragment.mAdded = true
fragment.mContainerId = android.R.id.content
fragmentStateManager.setFragmentManagerState(Fragment.STARTED)
val controller = SpecialEffectsController
.getOrCreateController(container, fm) as TestSpecialEffectsController
onActivity {
// This moves the Fragment up to STARTED,
// calling enqueueAdd() under the hood
fragmentStateManager.moveToExpectedState()
controller.executePendingOperations()
}
assertThat(fragment.view)
.isNotNull()
val operations = controller.operationsToExecute
assertThat(operations)
.hasSize(1)
val firstOperation = operations[0]
assertThat(firstOperation.lifecycleImpact)
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
assertThat(firstOperation.fragment)
.isSameInstanceAs(fragment)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
fragmentStateManager.setFragmentManagerState(Fragment.CREATED)
onActivity {
// move the Fragment's state back down, which
// cancels the ADD operation
fragmentStateManager.moveToExpectedState()
controller.executePendingOperations()
}
assertThat(firstOperation.isCanceled)
.isTrue()
assertThat(controller.operationsToExecute)
.doesNotContain(firstOperation)
assertThat(controller.operationsToExecute)
.hasSize(1)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.REMOVING)
onActivity {
controller.completeAllOperations()
}
assertThat(controller.operationsToExecute)
.isEmpty()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isNull()
assertThat(fragment.lifecycle.currentState)
.isEqualTo(Lifecycle.State.CREATED)
}
}
@MediumTest
@Test
fun enqueueAddAndForceCompleteAllPending() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
TestSpecialEffectsController(it)
}
val fragment = StrictViewFragment()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment.mFragmentManager = fm
fragment.mAdded = true
fragment.mContainerId = android.R.id.content
fragmentStateManager.setFragmentManagerState(Fragment.STARTED)
val controller = SpecialEffectsController
.getOrCreateController(container, fm) as TestSpecialEffectsController
onActivity {
// This moves the Fragment up to STARTED,
// calling enqueueAdd() under the hood
fragmentStateManager.moveToExpectedState()
}
assertThat(controller.operationsToExecute)
.isEmpty()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
onActivity {
// Now force all operations to immediately complete
controller.forceCompleteAllOperations()
}
assertThat(controller.operationsToExecute)
.isEmpty()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isNull()
}
}
@MediumTest
@Test
fun enqueueAddAndForceCompleteAllExecuting() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
TestSpecialEffectsController(it)
}
val fragment = StrictViewFragment()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment.mFragmentManager = fm
fragment.mAdded = true
fragment.mContainerId = android.R.id.content
fragmentStateManager.setFragmentManagerState(Fragment.STARTED)
val controller = SpecialEffectsController
.getOrCreateController(container, fm) as TestSpecialEffectsController
onActivity {
// This moves the Fragment up to STARTED,
// calling enqueueAdd() under the hood
fragmentStateManager.moveToExpectedState()
controller.executePendingOperations()
}
val operations = controller.operationsToExecute
assertThat(operations)
.hasSize(1)
val firstOperation = operations[0]
assertThat(firstOperation.lifecycleImpact)
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
assertThat(firstOperation.fragment)
.isSameInstanceAs(fragment)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
var lifecycleImpactOnCompletion:
SpecialEffectsController.Operation.LifecycleImpact? = null
firstOperation.addCompletionListener {
lifecycleImpactOnCompletion = controller.getAwaitingCompletionLifecycleImpact(
fragmentStateManager
)
}
onActivity {
// Now force all operations to immediately complete
controller.forceCompleteAllOperations()
}
assertThat(firstOperation.isCanceled)
.isTrue()
assertThat(lifecycleImpactOnCompletion).isNull()
assertThat(controller.operationsToExecute)
.isEmpty()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isNull()
}
}
@MediumTest
@Test
fun enqueueAddAndPostpone() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
val container = withActivity { findViewById<ViewGroup>(android.R.id.content) }
val fm = withActivity { supportFragmentManager }
fm.specialEffectsControllerFactory = SpecialEffectsControllerFactory {
TestSpecialEffectsController(it)
}
val fragment = StrictViewFragment()
fragment.postponeEnterTransition()
val fragmentStore = FragmentStore()
fragmentStore.nonConfig = FragmentManagerViewModel(true)
val fragmentStateManager = FragmentStateManager(
fm.lifecycleCallbacksDispatcher,
fragmentStore, fragment
)
// Set up the Fragment and FragmentStateManager as if the Fragment was
// added to the container via a FragmentTransaction
fragment.mFragmentManager = fm
fragment.mAdded = true
fragment.mContainerId = android.R.id.content
fragmentStateManager.setFragmentManagerState(Fragment.STARTED)
val controller = SpecialEffectsController
.getOrCreateController(container, fm) as TestSpecialEffectsController
onActivity {
// This moves the Fragment up to STARTED,
// calling enqueueAdd() under the hood
fragmentStateManager.moveToExpectedState()
}
assertThat(controller.operationsToExecute)
.isEmpty()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
// Mark the postponed state
controller.markPostponedState()
controller.executePendingOperations()
// Verify that executePendingOperations() didn't actually execute
// anything since we are postponed
assertThat(controller.operationsToExecute)
.isEmpty()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
onActivity {
fragment.startPostponedEnterTransition()
}
// Wait for idle thread to handle the post() that startPostponedEnterTransition() does.
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
// Verify that the operation was sent for execution
assertThat(controller.operationsToExecute)
.hasSize(1)
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isEqualTo(SpecialEffectsController.Operation.LifecycleImpact.ADDING)
controller.completeAllOperations()
assertThat(controller.getAwaitingCompletionLifecycleImpact(fragmentStateManager))
.isNull()
// Assert that we actually moved to the STARTED state
assertThat(fragment.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
}
}
}
internal class TestSpecialEffectsController(
container: ViewGroup
) : SpecialEffectsController(container) {
val operationsToExecute = mutableListOf<Operation>()
override fun collectEffects(operations: List<Operation>, isPop: Boolean) {
operationsToExecute.addAll(operations)
operations.forEach { operation ->
val effect = object : Effect() {
override fun onCancel(container: ViewGroup) {
operation.completeEffect(this)
}
}
operation.addEffect(effect)
operation.addCompletionListener {
operationsToExecute.remove(operation)
operation.isAwaitingContainerChanges = false
}
}
}
fun completeAllOperations() {
operationsToExecute.forEach { operation ->
operation.effects.forEach { effect ->
operation.completeEffect(effect)
}
}
operationsToExecute.clear()
}
}
internal class InstantSpecialEffectsController(
container: ViewGroup
) : SpecialEffectsController(container) {
var executeOperationsCallCount = 0
override fun collectEffects(operations: List<Operation>, isPop: Boolean) {
executeOperationsCallCount++
}
}