diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1229e7b5..734add5f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,6 +38,7 @@ android { targetSdk = Versions.ANDROID_TARGET_SDK_VERSION versionCode = Versions.ANDROID_VERSION_CODE versionName = Versions.ANDROID_VERSION_NAME + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -103,6 +104,12 @@ android { xmlOutput = file("build/reports/lint/lint-result.xml") } + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + } + testOptions { unitTests { isIncludeAndroidResources = true @@ -153,6 +160,11 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling:${Versions.COMPOSE_VERSION}") // Testing + androidTestImplementation("androidx.compose.ui:ui-test-junit4:${Versions.COMPOSE_VERSION}") + androidTestImplementation("io.mockk:mockk-android:${Versions.TEST_MOCKK_VERSION}") + androidTestImplementation("io.mockk:mockk-agent-android:${Versions.TEST_MOCKK_VERSION}") + androidTestImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}") + testImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}") testImplementation("junit:junit:${Versions.TEST_JUNIT_VERSION}") testImplementation("androidx.test:core:${Versions.TEST_ANDROIDX_CORE_VERSION}") @@ -164,8 +176,8 @@ dependencies { testImplementation("io.mockk:mockk:${Versions.TEST_MOCKK_VERSION}") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.TEST_COROUTINES_VERSION}") testImplementation("app.cash.turbine:turbine:${Versions.TEST_TURBINE_VERSION}") - testImplementation ("androidx.compose.ui:ui-test-junit4:${Versions.COMPOSE_VERSION}") - testImplementation ("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}") + testImplementation("androidx.compose.ui:ui-test-junit4:${Versions.COMPOSE_VERSION}") + testImplementation("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}") kaptTest("com.google.dagger:hilt-android-compiler:${Versions.HILT_VERSION}") testAnnotationProcessor("com.google.dagger:hilt-android-compiler:${Versions.HILT_VERSION}") diff --git a/app/src/androidTest/java/co/nimblehq/compose/crypto/test/MockUtil.kt b/app/src/androidTest/java/co/nimblehq/compose/crypto/test/MockUtil.kt new file mode 100644 index 00000000..dafdca8d --- /dev/null +++ b/app/src/androidTest/java/co/nimblehq/compose/crypto/test/MockUtil.kt @@ -0,0 +1,100 @@ +package co.nimblehq.compose.crypto.test + +import co.nimblehq.compose.crypto.domain.model.CoinDetail +import co.nimblehq.compose.crypto.domain.model.CoinItem +import java.math.BigDecimal + +object MockUtil { + + val myCoins = listOf( + CoinItem( + id = "bitcoin", + symbol = "btc", + coinName = "Bitcoin", + image = "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579", + currentPrice = BigDecimal(21953), + marketCap = BigDecimal(418632879244), + marketCapRank = 1, + fullyDilutedValuation = BigDecimal(394474286491), + totalVolume = BigDecimal(40284988945), + high24h = BigDecimal(23014), + low24h = BigDecimal(21175), + priceChange24h = BigDecimal(777.55), + priceChangePercentage24h = 3.67201, + marketCapChange24h = BigDecimal(15300446085.0), + marketCapChangePercentage24h = 3.79351, + circulatingSupply = BigDecimal(19143668), + totalSupply = BigDecimal(21000000), + maxSupply = BigDecimal(21000000), + ath = BigDecimal(69045), + athChangePercentage = -68.93253, + athDate = "2021-11-10T14:24:19.604Z", + atl = BigDecimal(0.0398177), + atlChangePercentage = 661256.26362, + atlDate = "2017-10-19T00:00:00.000Z", + roi = CoinItem.RoiItem( + times = BigDecimal(106.82921216576392), + currency = "btc", + percentage = 10682.921216576393 + ), + lastUpdated = "2022-09-07T05:38:22.556Z", + priceChangePercentage24hInCurrency = 3.672009841642702 + ) + ) + + val trendingCoins = myCoins + + val coinDetail = CoinDetail( + id = "bitcoin", + symbol = "btc", + coinName = "Bitcoin", + image = CoinDetail.Image( + large = "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579", + small = "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579", + thumb = "https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579" + ), + marketData = CoinDetail.MarketData( + currentPrice = mapOf("usd" to BigDecimal(19112.45)), + ath = mapOf("usd" to BigDecimal(69045)), + athChangePercentage = mapOf("usd" to -72.30426), + athDate = emptyMap(), + atl = mapOf("usd" to BigDecimal(67.81)), + atlChangePercentage = mapOf("usd" to 28100.4782), + atlDate = emptyMap(), + marketCap = mapOf("usd" to BigDecimal(366436890217)), + marketCapRank = 0, + fullyDilutedValuation = emptyMap(), + totalVolume = emptyMap(), + high24h = emptyMap(), + low24h = emptyMap(), + + priceChange24h = BigDecimal.ZERO, + priceChangePercentage24h = 0.0, + priceChangePercentage7d = 0.0, + priceChangePercentage14d = 0.0, + priceChangePercentage30d = 0.0, + priceChangePercentage60d = 0.0, + priceChangePercentage200d = 0.0, + priceChangePercentage1y = 0.0, + marketCapChange24h = BigDecimal.ZERO, + marketCapChangePercentage24h = 1.0166, + + priceChange24hInCurrency = emptyMap(), + priceChangePercentage24hInCurrency = mapOf("usd" to 0.74874), + priceChangePercentage7dInCurrency = emptyMap(), + priceChangePercentage14dInCurrency = emptyMap(), + priceChangePercentage30dInCurrency = emptyMap(), + priceChangePercentage60dInCurrency = emptyMap(), + priceChangePercentage200dInCurrency = emptyMap(), + priceChangePercentage1yInCurrency = emptyMap(), + marketCapChange24hInCurrency = emptyMap(), + marketCapChangePercentage24hInCurrency = emptyMap(), + + totalSupply = BigDecimal.ZERO, + maxSupply = BigDecimal.ZERO, + circulatingSupply = BigDecimal.ZERO, + + lastUpdated = "lastUpdated" + ) + ) +} diff --git a/app/src/androidTest/java/co/nimblehq/compose/crypto/test/TestDispatchersProvider.kt b/app/src/androidTest/java/co/nimblehq/compose/crypto/test/TestDispatchersProvider.kt new file mode 100644 index 00000000..6d9d144a --- /dev/null +++ b/app/src/androidTest/java/co/nimblehq/compose/crypto/test/TestDispatchersProvider.kt @@ -0,0 +1,21 @@ +package co.nimblehq.compose.crypto.test + +import co.nimblehq.compose.crypto.util.DispatchersProvider +import kotlinx.coroutines.* +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +@OptIn(ExperimentalCoroutinesApi::class) +object TestDispatchersProvider : DispatchersProvider { + + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() + + override val io: CoroutineDispatcher + get() = testDispatcher + + override val main: CoroutineDispatcher + get() = testDispatcher + + override val default: CoroutineDispatcher + get() = testDispatcher +} diff --git a/app/src/androidTest/java/co/nimblehq/compose/crypto/ui/screen/HomeScreenUITest.kt b/app/src/androidTest/java/co/nimblehq/compose/crypto/ui/screen/HomeScreenUITest.kt new file mode 100644 index 00000000..5db5bb08 --- /dev/null +++ b/app/src/androidTest/java/co/nimblehq/compose/crypto/ui/screen/HomeScreenUITest.kt @@ -0,0 +1,184 @@ +package co.nimblehq.compose.crypto.ui.screen + +import androidx.activity.compose.setContent +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import co.nimblehq.compose.crypto.test.MockUtil +import co.nimblehq.compose.crypto.R +import co.nimblehq.compose.crypto.domain.usecase.GetMyCoinsUseCase +import co.nimblehq.compose.crypto.domain.usecase.GetTrendingCoinsUseCase +import co.nimblehq.compose.crypto.extension.toFormattedString +import co.nimblehq.compose.crypto.test.TestDispatchersProvider +import co.nimblehq.compose.crypto.ui.navigation.AppDestination +import co.nimblehq.compose.crypto.ui.screens.MainActivity +import co.nimblehq.compose.crypto.ui.screens.home.* +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class HomeScreenUITest { + + @get:Rule + val composeAndroidTestRule = createAndroidComposeRule() + + private val homeTitle: String + get() = composeAndroidTestRule.activity.getString(R.string.home_title) + + private val totalCoinsLabel: String + get() = composeAndroidTestRule.activity.getString(R.string.portfolio_card_total_coin_label) + + private val todayProfitLabel: String + get() = composeAndroidTestRule.activity.getString(R.string.portfolio_card_today_profit_label) + + private val errorGeneric: String + get() = composeAndroidTestRule.activity.getString(R.string.error_generic) + + private val expectedPriceChange: String + get() = composeAndroidTestRule.activity.getString( + R.string.coin_profit_percent, + MockUtil.trendingCoins.first().priceChangePercentage24hInCurrency.toFormattedString() + ) + + private val mockGetMyCoinsUseCase = mockk() + private val mockGetTrendingCoinsUseCase = mockk() + + private lateinit var viewModel: HomeViewModel + + private var appDestination: AppDestination? = null + + @Before + fun setUp() { + composeAndroidTestRule.activity.setContent { + HomeScreen( + viewModel = viewModel, + navigator = { destination -> appDestination = destination } + ) + } + + every { mockGetMyCoinsUseCase.execute(any()) } returns flowOf(MockUtil.myCoins) + every { mockGetTrendingCoinsUseCase.execute(any()) } returns flowOf(MockUtil.trendingCoins) + } + + @Test + fun when_entering_HomeScreen__it_renders_the_PortfolioCard_properly() { + initViewModel() + + with(composeAndroidTestRule) { + onNodeWithText(homeTitle).assertIsDisplayed() + onNodeWithText(totalCoinsLabel).assertIsDisplayed() + onNodeWithText(todayProfitLabel).assertIsDisplayed() + onNodeWithText("$7,273,291").assertIsDisplayed() + onNodeWithText("$193,280").assertIsDisplayed() + } + } + + @Test + fun when_loading_MyCoins__it_renders_the_LoadingProgress_properly() { + every { mockGetMyCoinsUseCase.execute(any()) } returns flow { delay(500) } + + initViewModel() + + composeAndroidTestRule.onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed() + } + + @Test + fun when_loading_TrendingCoins__it_renders_the_LoadingProgress_properly() { + every { mockGetTrendingCoinsUseCase.execute(any()) } returns flow { delay(500) } + + initViewModel() + + composeAndroidTestRule.onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed() + } + + @Test + fun when_entering_HomeScreen_and_loading_MyCoins_successfully__it_renders_the_UI_properly() { + initViewModel() + + with(composeAndroidTestRule) { + with(MockUtil.myCoins.first()) { + onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed() + onAllNodesWithText(coinName).onFirst().assertIsDisplayed() + onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed() + } + } + } + + @Test + fun when_entering_to_the_HomeScreen_and_loading_TrendingCoins_successfully__it_renders_the_UI_properly() { + initViewModel() + + with(composeAndroidTestRule) { + with(MockUtil.trendingCoins.first()) { + onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed() + onAllNodesWithText(coinName).onFirst().assertIsDisplayed() + onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed() + } + } + } + + @Test + fun when_clicked_on_MyCoin_item__it_navigates_to_DetailScreen() { + initViewModel() + + composeAndroidTestRule.onAllNodesWithTag(testTag = TestTagCoinItem).onFirst().performClick() + + appDestination shouldBe AppDestination.CoinDetail + } + + @Test + fun when_clicked_on_TrendingCoin_item__it_navigates_to_DetailScreen() { + initViewModel() + + composeAndroidTestRule.onAllNodesWithTag( + testTag = TestTagTrendingItem + ).onFirst().performClick() + + appDestination shouldBe AppDestination.CoinDetail + } + + @Test + fun when_entering_to_the_HomeScreen_and_loading_MyCoins_failed__it_shows_the_error_message() { + every { mockGetMyCoinsUseCase.execute(any()) } returns flow { + throw Throwable(errorGeneric) + } + + initViewModel() + + composeAndroidTestRule.onNodeWithTag( + testTag = TestTagCoinItem, + useUnmergedTree = true + ).assertDoesNotExist() + + // TODO: Add the assertion for the error message + } + + @Test + fun when_entering_to_the_HomeScreen_and_loading_TrendingCoins_failed__it_shows_the_error_message() { + every { mockGetTrendingCoinsUseCase.execute(any()) } returns flow { + throw Throwable(errorGeneric) + } + + initViewModel() + + composeAndroidTestRule.onNodeWithTag( + testTag = TestTagTrendingItem, + useUnmergedTree = true + ).assertDoesNotExist() + + // TODO: Add the assertion for the error message + } + + private fun initViewModel() { + viewModel = HomeViewModel( + dispatchers = TestDispatchersProvider, + getMyCoinsUseCase = mockGetMyCoinsUseCase, + getTrendingCoinsUseCase = mockGetTrendingCoinsUseCase + ) + } +} diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/CoinItem.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/CoinItem.kt index 13d755ea..4fed6022 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/CoinItem.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/CoinItem.kt @@ -9,7 +9,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -23,11 +22,6 @@ import co.nimblehq.compose.crypto.ui.theme.* import co.nimblehq.compose.crypto.ui.uimodel.CoinItemUiModel import coil.compose.rememberAsyncImagePainter -const val TestTagCoinItemSymbol = "CoinItemCoinSymbol" -const val TestTagCoinItemCoinName = "CoinItemCoinName" -const val TestTagCoinItemPrice = "CoinItemPrice" -const val TestTagCoinItemPriceChange = "CoinItemPriceChange" - @Composable fun CoinItem( modifier: Modifier = Modifier, @@ -67,8 +61,7 @@ fun CoinItem( .constrainAs(coinSymbol) { top.linkTo(parent.top) start.linkTo(anchor = logo.end, margin = Dp16) - } - .testTag(tag = TestTagCoinItemSymbol), + }, text = coinItem.symbol.uppercase(), color = AppTheme.colors.text, style = AppTheme.styles.semiBold16 @@ -81,8 +74,7 @@ fun CoinItem( start.linkTo(coinSymbol.start) top.linkTo(coinSymbol.bottom) width = Dimension.preferredWrapContent - } - .testTag(tag = TestTagCoinItemCoinName), + }, text = coinItem.coinName, color = AppTheme.colors.coinNameText, style = AppTheme.styles.medium14 @@ -94,8 +86,7 @@ fun CoinItem( start.linkTo(logo.start) top.linkTo(anchor = coinName.bottom, margin = Dp14) width = Dimension.preferredWrapContent - } - .testTag(tag = TestTagCoinItemPrice), + }, text = stringResource( R.string.coin_currency, coinItem.currentPrice.toFormattedString() @@ -113,7 +104,6 @@ fun CoinItem( bottom.linkTo(parent.bottom) width = Dimension.preferredWrapContent } - .testTag(tag = TestTagCoinItemPriceChange) ) } } diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt index 052b1cea..36b02841 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt @@ -33,7 +33,6 @@ import timber.log.Timber private const val LIST_ITEM_LOAD_MORE_THRESHOLD = 0 -const val TestTagHomeTitle = "HomeTitle" const val TestTagTrendingItem = "TrendingItem" const val TestTagCoinItem = "CoinItem" const val TestTagCoinsLoader = "CoinsLoader" @@ -66,10 +65,14 @@ fun HomeScreen( myCoinsError?.let { error -> Toast.makeText(context, error.userReadableMessage(context), Toast.LENGTH_SHORT).show() + + viewModel.input.clearMyCoinsError() } trendingCoinsError?.let { error -> Toast.makeText(context, error.userReadableMessage(context), Toast.LENGTH_SHORT).show() + + viewModel.input.clearTrendingCoinsError() } HomeScreenContent( @@ -120,8 +123,7 @@ private fun HomeScreenContent( Text( modifier = Modifier .fillMaxWidth() - .padding(top = Dp16) - .testTag(TestTagHomeTitle), + .padding(top = Dp16), text = stringResource(id = R.string.home_title), textAlign = TextAlign.Center, style = AppTheme.styles.semiBold24, diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt index a75e3322..14d38678 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt @@ -27,6 +27,10 @@ interface Input : BaseInput { fun onMyCoinsItemClick(coin: CoinItemUiModel) fun onTrendingCoinsItemClick(coin: CoinItemUiModel) + + fun clearMyCoinsError() + + fun clearTrendingCoinsError() } interface Output : BaseOutput { @@ -39,9 +43,9 @@ interface Output : BaseOutput { val trendingCoins: StateFlow> - val myCoinsError: SharedFlow + val myCoinsError: StateFlow - val trendingCoinsError: SharedFlow + val trendingCoinsError: StateFlow } @HiltViewModel @@ -157,4 +161,16 @@ class HomeViewModel @Inject constructor( _navigator.emit(AppDestination.CoinDetail.buildDestination(coin.id)) } } + + override fun clearMyCoinsError() { + execute { + _myCoinsError.emit(null) + } + } + + override fun clearTrendingCoinsError() { + execute { + _trendingCoinsError.emit(null) + } + } } diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/PortfolioCard.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/PortfolioCard.kt index 567d43b0..69231b74 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/PortfolioCard.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/PortfolioCard.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout @@ -17,11 +16,6 @@ import co.nimblehq.compose.crypto.R import co.nimblehq.compose.crypto.ui.common.price.PriceChangeButton import co.nimblehq.compose.crypto.ui.theme.* -const val TestTagTotalCoinsLabel = "CardTotalCoinsLabel" -const val TestTagTodayCoinProfitLabel = "todayProfitLabel" -const val TestTagCardTotalCoins = "CardTotalCoins" -const val TestTagCardTodayProfit = "CardTodayProfit" - @Composable fun PortfolioCard( modifier: Modifier @@ -49,8 +43,7 @@ fun PortfolioCard( modifier = Modifier .constrainAs(totalCoinsLabel) { start.linkTo(parent.start) - } - .testTag(TestTagTotalCoinsLabel), + }, text = stringResource(R.string.portfolio_card_total_coin_label), style = AppTheme.styles.lightSilverMedium16 ) @@ -59,8 +52,7 @@ fun PortfolioCard( modifier = Modifier .constrainAs(totalCoins) { top.linkTo(totalCoinsLabel.bottom, margin = Dp8) - } - .testTag(tag = TestTagCardTotalCoins), + }, // TODO: Remove dummy value when work on Integrate. text = stringResource(R.string.coin_currency, "7,273,291"), style = AppTheme.styles.whiteSemiBold24 @@ -70,8 +62,7 @@ fun PortfolioCard( modifier = Modifier .constrainAs(todayProfitLabel) { top.linkTo(totalCoins.bottom, margin = Dp40) - } - .testTag(tag = TestTagTodayCoinProfitLabel), + }, text = stringResource(R.string.portfolio_card_today_profit_label), style = AppTheme.styles.lightSilverMedium16 ) @@ -80,8 +71,7 @@ fun PortfolioCard( modifier = Modifier .constrainAs(todayProfit) { top.linkTo(todayProfitLabel.bottom, margin = Dp8) - } - .testTag(tag = TestTagCardTodayProfit), + }, // TODO: Remove dummy value when work on Integrate. text = stringResource(R.string.coin_currency, "193,280"), style = AppTheme.styles.whiteSemiBold24 diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/TrendingItem.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/TrendingItem.kt index b619df14..f49e7444 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/TrendingItem.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/TrendingItem.kt @@ -9,7 +9,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.constraintlayout.compose.ConstraintLayout @@ -20,10 +19,6 @@ import co.nimblehq.compose.crypto.ui.theme.* import co.nimblehq.compose.crypto.ui.uimodel.CoinItemUiModel import coil.compose.rememberAsyncImagePainter -const val TestTagTrendingItemSymbol = "TrendingItemSymbol" -const val TestTagTrendingItemCoinName = "TrendingItemCoinName" -const val TestTagTrendingItemPriceChange = "TrendingItemPriceChange" - @Suppress("LongMethod") @Composable fun TrendingItem( @@ -69,8 +64,7 @@ fun TrendingItem( top.linkTo(parent.top) bottom.linkTo(coinName.top) start.linkTo(anchor = logo.end, margin = Dp16) - } - .testTag(tag = TestTagTrendingItemSymbol), + }, text = coinItem.symbol.uppercase(), color = AppTheme.colors.text, style = AppTheme.styles.semiBold16 @@ -83,8 +77,7 @@ fun TrendingItem( top.linkTo(coinSymbol.bottom) bottom.linkTo(parent.bottom) width = Dimension.preferredWrapContent - } - .testTag(tag = TestTagTrendingItemCoinName), + }, text = coinItem.coinName, color = AppTheme.colors.coinNameText, style = AppTheme.styles.medium14 @@ -99,7 +92,6 @@ fun TrendingItem( bottom.linkTo(coinName.bottom) width = Dimension.preferredWrapContent } - .testTag(tag = TestTagTrendingItemPriceChange) ) } } diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt index 298cceaf..4c08e397 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt @@ -74,11 +74,11 @@ class HomeScreenTest : BaseViewModelTest() { initViewModel() with(composeAndroidTestRule) { - onNodeWithTag(testTag = TestTagHomeTitle).assertTextEquals(homeTitle) - onNodeWithTag(testTag = TestTagTotalCoinsLabel).assertTextEquals(totalCoinsLabel) - onNodeWithTag(testTag = TestTagTodayCoinProfitLabel).assertTextEquals(todayProfitLabel) - onNodeWithTag(testTag = TestTagCardTotalCoins).assertTextEquals("$7,273,291") - onNodeWithTag(testTag = TestTagCardTodayProfit).assertTextEquals("$193,280") + onNodeWithText(homeTitle).assertIsDisplayed() + onNodeWithText(totalCoinsLabel).assertIsDisplayed() + onNodeWithText(todayProfitLabel).assertIsDisplayed() + onNodeWithText("$7,273,291").assertIsDisplayed() + onNodeWithText("$193,280").assertIsDisplayed() } } @@ -92,20 +92,9 @@ class HomeScreenTest : BaseViewModelTest() { onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed() with(MockUtil.myCoins.first()) { - onAllNodesWithTag( - testTag = TestTagCoinItemSymbol, - useUnmergedTree = true - ).onFirst().assertTextEquals(symbol.uppercase()) - - onAllNodesWithTag( - testTag = TestTagCoinItemCoinName, - useUnmergedTree = true - ).onFirst().assertTextEquals(coinName) - - onAllNodesWithTag( - testTag = TestTagCoinItemPriceChange, - useUnmergedTree = true - ).onFirst().onChild().assertTextEquals(expectedPriceChange) + onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed() + onAllNodesWithText(coinName).onFirst().assertIsDisplayed() + onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed() } } } @@ -120,20 +109,9 @@ class HomeScreenTest : BaseViewModelTest() { onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed() with(MockUtil.trendingCoins.first()) { - onAllNodesWithTag( - testTag = TestTagTrendingItemSymbol, - useUnmergedTree = true - ).onFirst().assertTextEquals(symbol.uppercase()) - - onAllNodesWithTag( - testTag = TestTagTrendingItemCoinName, - useUnmergedTree = true - ).onFirst().assertTextEquals(coinName) - - onAllNodesWithTag( - testTag = TestTagTrendingItemPriceChange, - useUnmergedTree = true - ).onFirst().onChild().assertTextEquals(expectedPriceChange) + onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed() + onAllNodesWithText(coinName).onFirst().assertIsDisplayed() + onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed() } } } @@ -202,20 +180,9 @@ class HomeScreenTest : BaseViewModelTest() { with(composeAndroidTestRule) { with(MockUtil.myCoins.first()) { - onAllNodesWithTag( - testTag = TestTagCoinItemSymbol, - useUnmergedTree = true - ).onFirst().assertTextEquals(symbol.uppercase()) - - onAllNodesWithTag( - testTag = TestTagCoinItemCoinName, - useUnmergedTree = true - ).onFirst().assertTextEquals(coinName) - - onAllNodesWithTag( - testTag = TestTagCoinItemPriceChange, - useUnmergedTree = true - ).onFirst().onChild().assertTextEquals(expectedPriceChange) + onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed() + onAllNodesWithText(coinName).onFirst().assertIsDisplayed() + onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed() } onRoot().performTouchInput { swipeDown() } @@ -234,20 +201,9 @@ class HomeScreenTest : BaseViewModelTest() { onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed() with(MockUtil.trendingCoins.first()) { - onAllNodesWithTag( - testTag = TestTagTrendingItemSymbol, - useUnmergedTree = true - ).onFirst().assertTextEquals(symbol.uppercase()) - - onAllNodesWithTag( - testTag = TestTagTrendingItemCoinName, - useUnmergedTree = true - ).onFirst().assertTextEquals(coinName) - - onAllNodesWithTag( - testTag = TestTagTrendingItemPriceChange, - useUnmergedTree = true - ).onFirst().onChild().assertTextEquals(expectedPriceChange) + onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed() + onAllNodesWithText(coinName).onFirst().assertIsDisplayed() + onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed() } onRoot().performTouchInput { swipeDown() } diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt index dd3a3d1c..4fcc9adf 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt @@ -119,6 +119,26 @@ class HomeViewModelTest : BaseViewModelTest() { } } + @Test + fun `When calling clearMyCoinsError, it should reset myCoinsError to null`() = + runBlockingTest { + initViewModel() + viewModel.output.myCoinsError.test { + viewModel.input.clearMyCoinsError() + expectMostRecentItem() shouldBe null + } + } + + @Test + fun `When calling clearTrendingCoinsError, it should reset trendingCoinsError to null`() = + runBlockingTest { + initViewModel() + viewModel.output.trendingCoinsError.test { + viewModel.input.clearTrendingCoinsError() + expectMostRecentItem() shouldBe null + } + } + private fun initViewModel() { viewModel = HomeViewModel( testDispatcherProvider, diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index dd1367d6..f15a98dd 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -50,7 +50,7 @@ object Versions { const val TEST_JUNIT_ANDROIDX_EXT_VERSION = "1.1.3" const val TEST_JUNIT_VERSION = "4.13.2" const val TEST_KOTEST_VERSION = "4.6.3" - const val TEST_MOCKK_VERSION = "1.10.6" + const val TEST_MOCKK_VERSION = "1.12.3" const val TEST_ROBOLECTRIC_VERSION = "4.8.2" const val TEST_RUNNER_VERSION = "1.3.0" const val TEST_TURBINE_VERSION = "0.7.0"