Skip to content

Commit

Permalink
Add view model and ui test
Browse files Browse the repository at this point in the history
  • Loading branch information
sabercodic committed Nov 27, 2023
1 parent 2430bd0 commit e9386ac
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package net.mullvad.mullvadvpn.compose.screen

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.test.VOUCHER_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class RedeemVoucherDialogTest {
@get:Rule val composeTestRule = createComposeRule()

@Before
fun setup() {
mockkObject(VoucherRegexHelper)
}

@Test
fun testDismissDialog() {
// Arrange
val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState = VoucherDialogUiState.INITIAL,
onVoucherInputChange = {},
onRedeem = {},
onDismiss = mockedClickHandler
)
}

// Act
composeTestRule.onNodeWithText(CANCEL_BUTTON_TEXT).performClick()

// Assert
verify { mockedClickHandler.invoke(false) }
}

@Test
fun testDismissDialogAfterSuccessfulRedeem() {
// Arrange
val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState =
VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Success(0)),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = mockedClickHandler
)
}

// Act
composeTestRule.onNodeWithText(GOT_IT_BUTTON_TEXT).performClick()

// Assert
verify { mockedClickHandler.invoke(true) }
}

@Test
fun testInsertVoucher() {
// Arrange
val mockedClickHandler: (String) -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState = VoucherDialogUiState(),
onVoucherInputChange = mockedClickHandler,
onRedeem = {},
onDismiss = {}
)
}

// Act
composeTestRule.onNodeWithTag(VOUCHER_INPUT_TEST_TAG).performTextInput(DUMMY_VOUCHER)

// Assert
verify { mockedClickHandler.invoke(DUMMY_VOUCHER) }
}

@Test
fun testVerifyingState() {
// Arrange
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState =
VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Verifying),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}

// Assert
composeTestRule.onNodeWithText("Verifying voucher…").assertExists()
}

@Test
fun testSuccessState() {
// Arrange
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState =
VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Success(0)),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}

// Assert
composeTestRule.onNodeWithText("Voucher was successfully redeemed.").assertExists()
}

@Test
fun testErrorState() {
// Arrange
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState =
VoucherDialogUiState(
voucherViewModelState = VoucherDialogState.Error(ERROR_MESSAGE)
),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}

// Assert
composeTestRule.onNodeWithText(ERROR_MESSAGE).assertExists()
}

companion object {
private const val REDEEM_BUTTON_TEXT = "Redeem"
private const val CANCEL_BUTTON_TEXT = "Cancel"
private const val GOT_IT_BUTTON_TEXT = "Got it!"
private const val DUMMY_VOUCHER = "DUMMY____VOUCHER"
private const val ERROR_MESSAGE = "error_message"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
Expand All @@ -29,6 +30,7 @@ import net.mullvad.mullvadvpn.compose.button.VariantButton
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorSmall
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.test.VOUCHER_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.compose.textfield.CustomTextField
import net.mullvad.mullvadvpn.compose.util.MAX_VOUCHER_LENGTH
import net.mullvad.mullvadvpn.compose.util.vouchersVisualTransformation
Expand Down Expand Up @@ -230,7 +232,8 @@ private fun EnterVoucherBody(
keyboardType = KeyboardType.Password,
placeholderText = stringResource(id = R.string.voucher_hint),
visualTransformation = vouchersVisualTransformation(),
isDigitsOnlyAllowed = false
isDigitsOnlyAllowed = false,
modifier = Modifier.testTag(VOUCHER_INPUT_TEST_TAG)
)
Spacer(modifier = Modifier.height(Dimens.smallPadding))
Row(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ const val PLAY_PAYMENT_INFO_ICON_TEST_TAG = "play_payment_info_icon_test_tag"

const val LOGIN_TITLE_TEST_TAG = "login_title_test_tag"
const val LOGIN_INPUT_TEST_TAG = "login_input_test_tag"

// VoucherDialog
const val VOUCHER_INPUT_TEST_TAG = "voucher_input_test_tag"
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.state.LoginUiState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
Expand All @@ -33,7 +32,7 @@ class VoucherDialogViewModel(
) : ViewModel() {

private val vmState = MutableStateFlow<VoucherDialogState>(VoucherDialogState.Default)
private val voucherInput = MutableStateFlow(LoginUiState.INITIAL.accountNumberInput)
private val voucherInput = MutableStateFlow("")

private val _shared: SharedFlow<ServiceConnectionContainer> =
serviceConnectionManager.connectionState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package net.mullvad.mullvadvpn.utils

import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

private const val IS_ACCEPTED_FORMAT = true
private const val IS_UNACCEPTED_FORMAT = false

@RunWith(Parameterized::class)
class VoucherRegexHelperParameterizedTest(
private val isValid: Boolean,
private val voucher: String
) {
@get:Rule val testCoroutineRule = TestCoroutineRule()

@Test
fun testVoucherFormat() {
assertThat(VoucherRegexHelper.validate(voucher), equalTo(isValid))
}

companion object {
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Array<Any>> =
listOf(
arrayOf(IS_ACCEPTED_FORMAT, "1"),
arrayOf(IS_ACCEPTED_FORMAT, "a"),
arrayOf(IS_ACCEPTED_FORMAT, "A"),
arrayOf(IS_ACCEPTED_FORMAT, "AAAA"),
arrayOf(IS_ACCEPTED_FORMAT, "AAAABBBB11112222"),
arrayOf(IS_ACCEPTED_FORMAT, "AAAA BBBB 1111 2222"),
arrayOf(IS_ACCEPTED_FORMAT, "AAAA-AAAA-1111-2222\r"),
arrayOf(IS_ACCEPTED_FORMAT, "AAAA-AAAA-1111-2222\n"),
arrayOf(IS_UNACCEPTED_FORMAT, "@"),
arrayOf(IS_UNACCEPTED_FORMAT, "AAAABBBBCCCCDDDD\t"),
arrayOf(IS_UNACCEPTED_FORMAT, "AAAA_BBBB_CCCC_DDDD")
)
}
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,49 @@
package net.mullvad.mullvadvpn.viewmodel

import android.content.res.Resources
import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.model.VoucherSubmission
import net.mullvad.mullvadvpn.model.VoucherSubmissionError
import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import net.mullvad.mullvadvpn.ui.serviceconnection.VoucherRedeemer
import net.mullvad.mullvadvpn.ui.serviceconnection.voucherRedeemer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test

class VoucherDialogViewModelTest {
@get:Rule val testCoroutineRule = TestCoroutineRule()

private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockVoucherRedeemer: VoucherRedeemer = mockk()
private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
private val mockResources: Resources = mockk()
private val mockVoucherSubmission: VoucherSubmission = mockk()
private val serviceConnectionState =
MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)

private val mockVoucherSubmissionErrorResult: VoucherSubmissionResult =
VoucherSubmissionResult.Error(VoucherSubmissionError.OtherError)
private val mockVoucherRedeemer: VoucherRedeemer = mockk()
private val mockResources: Resources = mockk()

private lateinit var viewModel: VoucherDialogViewModel

@Before
fun setUp() {
mockkStatic(CACHE_EXTENSION_CLASS)
every { mockServiceConnectionManager.connectionState.value.readyContainer() } returns
mockServiceConnectionContainer
every { mockServiceConnectionContainer.voucherRedeemer } returns mockVoucherRedeemer
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState

viewModel =
VoucherDialogViewModel(
Expand All @@ -53,21 +58,73 @@ class VoucherDialogViewModelTest {
}

@Test
@Ignore("TODO: Fix this failing test and then enable it again.")
fun test_submit_invalid_voucher() = runTest {
fun testSubmitVoucher() = runTest {
val voucher = DUMMY_INVALID_VOUCHER
val dummyStringResource = DUMMY_STRING_RESOURCE

// Arrange
every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
every { mockVoucherSubmission.timeAdded } returns 0
coEvery { mockVoucherRedeemer.submit(voucher) } returns
VoucherSubmissionResult.Ok(mockVoucherSubmission)

// Act
assertIs<VoucherDialogState.Default>(viewModel.uiState.value.voucherViewModelState)
viewModel.onRedeem(voucher)

// Assert
coVerify(exactly = 1) { mockVoucherRedeemer.submit(voucher) }
}

@Test
fun testInsertInvalidVoucher() = runTest {
val voucher = DUMMY_INVALID_VOUCHER
val dummyStringResource = DUMMY_STRING_RESOURCE

// Arrange
every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
every { mockResources.getString(any()) } returns dummyStringResource
every { mockVoucherSubmission.timeAdded } returns 0
coEvery { mockVoucherRedeemer.submit(voucher) } returns
VoucherSubmissionResult.Error(VoucherSubmissionError.OtherError)

// Act, Assert
viewModel.uiState.test {
assertEquals(viewModel.uiState.value, awaitItem())
serviceConnectionState.value =
ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
viewModel.onRedeem(voucher)
assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Verifying }
assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Error }
}
}

@Test
fun testInsertValidVoucher() = runTest {
val voucher = DUMMY_VALID_VOUCHER
val dummyStringResource = DUMMY_STRING_RESOURCE

// Arrange
every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
every { mockResources.getString(any()) } returns dummyStringResource
coEvery { mockVoucherRedeemer.submit(voucher) } returns mockVoucherSubmissionErrorResult
every { mockVoucherSubmission.timeAdded } returns 0
coEvery { mockVoucherRedeemer.submit(voucher) } returns
VoucherSubmissionResult.Ok(VoucherSubmission(0, DUMMY_STRING_RESOURCE))

// Act, Assert
viewModel.onRedeem(voucher)
coVerify(exactly = 1) { mockVoucherRedeemer.submit(voucher) }
viewModel.uiState.test {
assertEquals(viewModel.uiState.value, awaitItem())
serviceConnectionState.value =
ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
viewModel.onRedeem(voucher)
assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Verifying }
assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Success }
}
}

companion object {
private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
private const val DUMMY_VALID_VOUCHER = "DUMMY_VALID_VOUCHER"
private const val DUMMY_STRING_RESOURCE = "DUMMY_STRING_RESOURCE"
private const val DUMMY_VALID_VOUCHER = "dummy_valid_voucher"
private const val DUMMY_INVALID_VOUCHER = "dummy_invalid_voucher"
private const val DUMMY_STRING_RESOURCE = "dummy_string_resource"
}
}

0 comments on commit e9386ac

Please sign in to comment.