Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 58e2149a authored by Fabian Kozynski's avatar Fabian Kozynski
Browse files

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
parent 52c10f48
Loading
Loading
Loading
Loading
+333 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}
+141 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}