[go: nahoru, domu]

blob: f487429e2f68b98e62f9e8a014dfeec683ad7156 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.fragment.app
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.os.Looper
import androidx.fragment.app.test.EmptyFragmentTestActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import leakcanary.DetectLeaksAfterTestSuccess
import leakcanary.SkipLeakDetection
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
* Class representing the different ways of dismissing a [DialogFragment]
*/
sealed class Operation {
abstract fun run(dialogFragment: DialogFragment)
override fun toString(): String = this.javaClass.simpleName
}
object ActivityFinish : Operation() {
override fun run(dialogFragment: DialogFragment) {
dialogFragment.requireActivity().finish()
}
}
object FragmentDismiss : Operation() {
override fun run(dialogFragment: DialogFragment) {
dialogFragment.dismiss()
}
}
object DialogDismiss : Operation() {
override fun run(dialogFragment: DialogFragment) {
dialogFragment.requireDialog().dismiss()
}
}
object DialogCancel : Operation() {
override fun run(dialogFragment: DialogFragment) {
dialogFragment.requireDialog().cancel()
}
}
object FragmentDismissNow : Operation() {
override fun run(dialogFragment: DialogFragment) {
dialogFragment.dismissNow()
}
}
@LargeTest
@RunWith(Parameterized::class)
class DialogFragmentDismissTest(
private val operation: Operation,
private val mainThread: Boolean
) {
companion object {
@JvmStatic
@Parameterized.Parameters(name = "operation={0}, mainThread={1}")
fun data() = mutableListOf<Array<Any>>().apply {
arrayOf(
ActivityFinish,
FragmentDismiss,
DialogDismiss,
DialogCancel
).forEach { operation ->
// Run the operation on the main thread
add(arrayOf(operation, true))
// Run the operation off the main thread
add(arrayOf(operation, false))
}
// dismissNow can only be run on the main thread
add(arrayOf(FragmentDismissNow, true))
}
}
@Suppress("DEPRECATION")
val activityTestRule =
androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
// Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
.around(activityTestRule)
@SkipLeakDetection("There is a platform ViewRootImpl leak this is triggered on this test")
@Test
fun testDialogFragmentDismiss() {
// Due to b/157955883, we need to early return if API == 30.
// Otherwise, this test flakes.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
return
}
val fragment = TestDialogFragment()
activityTestRule.runOnUiThread {
fragment.showNow(activityTestRule.activity.supportFragmentManager, null)
}
assertWithMessage("Dialog was not being shown")
.that(fragment.dialog?.isShowing)
.isTrue()
var dialogIsNonNull = false
var isShowing = false
var onDismissCalledCount = 0
val countDownLatch = CountDownLatch(3)
activityTestRule.runOnUiThread {
fragment.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_STOP) {
val dialog = fragment.dialog
dialogIsNonNull = dialog != null
isShowing = dialog != null && dialog.isShowing
countDownLatch.countDown()
} else if (event == Lifecycle.Event.ON_DESTROY) {
onDismissCalledCount = fragment.onDismissCalledCount
countDownLatch.countDown()
}
}
)
}
var dismissOnMainThread = false
var dismissCalled = false
fragment.dismissCallback = {
dismissCalled = true
dismissOnMainThread = Looper.myLooper() == Looper.getMainLooper()
countDownLatch.countDown()
}
if (mainThread) {
activityTestRule.runOnUiThread {
operation.run(fragment)
}
} else {
operation.run(fragment)
}
assertWithMessage("Timed out waiting for ON_DESTROY")
.that(countDownLatch.await(5, TimeUnit.SECONDS))
.isTrue()
assertWithMessage("Dialog should be dismissed")
.that(dismissCalled)
.isTrue()
assertWithMessage("Dismiss should always be called on the main thread")
.that(dismissOnMainThread)
.isTrue()
assertWithMessage("onDismiss() should be called before onDestroy()")
.that(onDismissCalledCount)
.isEqualTo(1)
assertWithMessage("Dialog should not be null in onStop()")
.that(dialogIsNonNull)
.isTrue()
if (operation is ActivityFinish) {
assertWithMessage(
"Dialog should still be showing in onStop() during " +
"the normal lifecycle"
)
.that(isShowing)
.isTrue()
} else {
assertWithMessage("Dialog should not be showing in onStop() when manually dismissed")
.that(isShowing)
.isFalse()
assertWithMessage("Dialog should be null after dismiss()")
.that(fragment.dialog)
.isNull()
}
}
@Test
fun testDismissDestroyedDialog() {
val dialogFragment = TestDialogFragment()
val fm = activityTestRule.activity.supportFragmentManager
activityTestRule.runOnUiThread {
fm.beginTransaction()
.add(dialogFragment, null)
.commitNow()
}
val dialog = dialogFragment.requireDialog()
activityTestRule.runOnUiThread {
dialog.dismiss()
fm.beginTransaction()
.remove(dialogFragment)
.commitNow()
}
activityTestRule.runOnUiThread {
assertWithMessage("onDismiss should only have been called once")
.that(dialogFragment.onDismissCalledCount)
.isEqualTo(1)
}
}
class TestDialogFragment : DialogFragment() {
var onDismissCalledCount = 0
var dismissCallback: () -> Unit = {}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(context)
.setTitle("Test")
.setMessage("Message")
.setPositiveButton("Button", null)
.create()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismissCalledCount++
dismissCallback.invoke()
}
}
}