diff options
| author | 2022-12-02 13:50:45 -0500 | |
|---|---|---|
| committer | 2022-12-06 10:56:48 -0500 | |
| commit | 58e2149a080118c521b0ecf8c847b380fa00174c (patch) | |
| tree | a64ca755d52b5a2b77474d13fb94684e7c18ff45 | |
| parent | 52c10f48d0d2958c06bd0752f0e74a9f7e9f0830 (diff) | |
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
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt | 333 | ||||
| -rw-r--r-- | packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt | 141 |
2 files changed, 474 insertions, 0 deletions
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt new file mode 100644 index 000000000000..01dd60ae2200 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt @@ -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() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt new file mode 100644 index 000000000000..4d79554a79ce --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt @@ -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() + } +} |