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:
Fabian Kozynski
2022-12-02 13:50:45 -05:00
parent 52c10f48d0
commit 58e2149a08
2 changed files with 474 additions and 0 deletions

View File

@@ -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()
}
}

View File

@@ -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()
}
}