Add a TestableAlertDialog
This makes it easier to test classes that create and show an `AlertDialog`. If using `AlertDialog.Builder`, extra work is needed to use this. Test: atest TestableAlertDialogTest Bug: 260731518 Change-Id: I027cc96cbbaff569b2df33fcdcef0ad50800f56d
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.util
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.DialogInterface.BUTTON_NEGATIVE
|
||||
import android.content.DialogInterface.BUTTON_NEUTRAL
|
||||
import android.content.DialogInterface.BUTTON_POSITIVE
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
class TestableAlertDialogTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun dialogNotShowingWhenCreated() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dialogShownDoesntCrash() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dialogShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
|
||||
assertThat(dialog.isShowing).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showListenerCalled() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnShowListener = mock()
|
||||
dialog.setOnShowListener(listener)
|
||||
|
||||
dialog.show()
|
||||
|
||||
verify(listener).onShow(dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showListenerRemoved() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnShowListener = mock()
|
||||
dialog.setOnShowListener(listener)
|
||||
dialog.setOnShowListener(null)
|
||||
|
||||
dialog.show()
|
||||
|
||||
verify(listener, never()).onShow(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dialogHiddenNotShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
dialog.hide()
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dialogDismissNotShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
dialog.dismiss()
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dismissListenerCalled_ifShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnDismissListener = mock()
|
||||
dialog.setOnDismissListener(listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.dismiss()
|
||||
|
||||
verify(listener).onDismiss(dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dismissListenerNotCalled_ifNotShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnDismissListener = mock()
|
||||
dialog.setOnDismissListener(listener)
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
verify(listener, never()).onDismiss(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dismissListenerRemoved() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnDismissListener = mock()
|
||||
dialog.setOnDismissListener(listener)
|
||||
dialog.setOnDismissListener(null)
|
||||
|
||||
dialog.show()
|
||||
dialog.dismiss()
|
||||
|
||||
verify(listener, never()).onDismiss(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelListenerCalled_showing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnCancelListener = mock()
|
||||
dialog.setOnCancelListener(listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.cancel()
|
||||
|
||||
verify(listener).onCancel(dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelListenerCalled_notShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnCancelListener = mock()
|
||||
dialog.setOnCancelListener(listener)
|
||||
|
||||
dialog.cancel()
|
||||
|
||||
verify(listener).onCancel(dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dismissCalledOnCancel_showing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnDismissListener = mock()
|
||||
dialog.setOnDismissListener(listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.cancel()
|
||||
|
||||
verify(listener).onDismiss(dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dialogCancelNotShowing() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
dialog.cancel()
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelListenerRemoved() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnCancelListener = mock()
|
||||
dialog.setOnCancelListener(listener)
|
||||
dialog.setOnCancelListener(null)
|
||||
|
||||
dialog.show()
|
||||
dialog.cancel()
|
||||
|
||||
verify(listener, never()).onCancel(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun positiveButtonClick() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_POSITIVE, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_POSITIVE)
|
||||
|
||||
verify(listener).onClick(dialog, BUTTON_POSITIVE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun positiveButtonListener_noCalledWhenClickOtherButtons() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_POSITIVE, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_NEUTRAL)
|
||||
dialog.clickButton(BUTTON_NEGATIVE)
|
||||
|
||||
verify(listener, never()).onClick(any(), anyInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun negativeButtonClick() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_NEGATIVE, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_NEGATIVE)
|
||||
|
||||
verify(listener).onClick(dialog, DialogInterface.BUTTON_NEGATIVE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun negativeButtonListener_noCalledWhenClickOtherButtons() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_NEGATIVE, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_NEUTRAL)
|
||||
dialog.clickButton(BUTTON_POSITIVE)
|
||||
|
||||
verify(listener, never()).onClick(any(), anyInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun neutralButtonClick() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_NEUTRAL, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_NEUTRAL)
|
||||
|
||||
verify(listener).onClick(dialog, BUTTON_NEUTRAL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun neutralButtonListener_noCalledWhenClickOtherButtons() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_NEUTRAL, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_POSITIVE)
|
||||
dialog.clickButton(BUTTON_NEGATIVE)
|
||||
|
||||
verify(listener, never()).onClick(any(), anyInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sameClickListenerCalledCorrectly() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
val listener: DialogInterface.OnClickListener = mock()
|
||||
dialog.setButton(BUTTON_POSITIVE, "", listener)
|
||||
dialog.setButton(BUTTON_NEUTRAL, "", listener)
|
||||
dialog.setButton(BUTTON_NEGATIVE, "", listener)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_POSITIVE)
|
||||
dialog.clickButton(BUTTON_NEGATIVE)
|
||||
dialog.clickButton(BUTTON_NEUTRAL)
|
||||
|
||||
val inOrder = inOrder(listener)
|
||||
inOrder.verify(listener).onClick(dialog, BUTTON_POSITIVE)
|
||||
inOrder.verify(listener).onClick(dialog, BUTTON_NEGATIVE)
|
||||
inOrder.verify(listener).onClick(dialog, BUTTON_NEUTRAL)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun clickBadButton() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.clickButton(10000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clickButtonDismisses_positive() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_POSITIVE)
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clickButtonDismisses_negative() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_NEGATIVE)
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clickButtonDismisses_neutral() {
|
||||
val dialog = TestableAlertDialog(context)
|
||||
|
||||
dialog.show()
|
||||
dialog.clickButton(BUTTON_NEUTRAL)
|
||||
|
||||
assertThat(dialog.isShowing).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.util
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* [AlertDialog] that is easier to test. Due to [AlertDialog] being a class and not an interface,
|
||||
* there are some things that cannot be avoided, like the creation of a [Handler] on the main thread
|
||||
* (and therefore needing a prepared [Looper] in the test).
|
||||
*
|
||||
* It bypasses calls to show, clicks on buttons, cancel and dismiss so it all can happen bounded in
|
||||
* the test. It tries to be as close in behavior as a real [AlertDialog].
|
||||
*
|
||||
* It will only call [onCreate] as part of its lifecycle, but not any of the other lifecycle methods
|
||||
* in [Dialog].
|
||||
*
|
||||
* In order to test clicking on buttons, use [clickButton] instead of calling [View.callOnClick] on
|
||||
* the view returned by [getButton] to bypass the internal [Handler].
|
||||
*/
|
||||
class TestableAlertDialog(context: Context) : AlertDialog(context) {
|
||||
|
||||
private var _onDismissListener: DialogInterface.OnDismissListener? = null
|
||||
private var _onCancelListener: DialogInterface.OnCancelListener? = null
|
||||
private var _positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
private var _negativeButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
private var _neutralButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
private var _onShowListener: DialogInterface.OnShowListener? = null
|
||||
private var _dismissOverride: Runnable? = null
|
||||
|
||||
private var showing = false
|
||||
private var visible = false
|
||||
private var created = false
|
||||
|
||||
override fun show() {
|
||||
if (!created) {
|
||||
created = true
|
||||
onCreate(null)
|
||||
}
|
||||
if (isShowing) return
|
||||
showing = true
|
||||
visible = true
|
||||
_onShowListener?.onShow(this)
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
override fun isShowing(): Boolean {
|
||||
return visible && showing
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
if (!showing) {
|
||||
return
|
||||
}
|
||||
if (_dismissOverride != null) {
|
||||
_dismissOverride?.run()
|
||||
return
|
||||
}
|
||||
_onDismissListener?.onDismiss(this)
|
||||
showing = false
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
_onCancelListener?.onCancel(this)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) {
|
||||
_onDismissListener = listener
|
||||
}
|
||||
|
||||
override fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) {
|
||||
_onCancelListener = listener
|
||||
}
|
||||
|
||||
override fun setOnShowListener(listener: DialogInterface.OnShowListener?) {
|
||||
_onShowListener = listener
|
||||
}
|
||||
|
||||
override fun takeCancelAndDismissListeners(
|
||||
msg: String?,
|
||||
cancel: DialogInterface.OnCancelListener?,
|
||||
dismiss: DialogInterface.OnDismissListener?
|
||||
): Boolean {
|
||||
_onCancelListener = cancel
|
||||
_onDismissListener = dismiss
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setButton(
|
||||
whichButton: Int,
|
||||
text: CharSequence?,
|
||||
listener: DialogInterface.OnClickListener?
|
||||
) {
|
||||
super.setButton(whichButton, text, listener)
|
||||
when (whichButton) {
|
||||
DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener = listener
|
||||
DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener = listener
|
||||
DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener = listener
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click one of the buttons in the [AlertDialog] and call the corresponding listener.
|
||||
*
|
||||
* Button ids are from [DialogInterface].
|
||||
*/
|
||||
fun clickButton(whichButton: Int) {
|
||||
val listener =
|
||||
when (whichButton) {
|
||||
DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener
|
||||
DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener
|
||||
DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener
|
||||
else -> throw IllegalArgumentException("Wrong button $whichButton")
|
||||
}
|
||||
listener?.onClick(this, whichButton)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user