From 15d9f46d7ea7c6b0d32e05f50437fed8f71de47c Mon Sep 17 00:00:00 2001 From: Wadeewee Date: Mon, 6 Feb 2023 15:37:28 +0700 Subject: [PATCH 1/5] [#70] Add UnitTest for DetailScreen --- .../crypto/ui/screens/detail/Appbar.kt | 7 +- .../crypto/ui/screens/detail/DetailItem.kt | 14 +- .../crypto/ui/screens/detail/DetailScreen.kt | 46 +++-- .../ui/screens/detail/DetailScreenTest.kt | 178 ++++++++++++++++++ 4 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt index 98b2f187..e941f25e 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt @@ -6,12 +6,15 @@ import androidx.compose.material.* 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.tooling.preview.Preview import co.nimblehq.compose.crypto.R import co.nimblehq.compose.crypto.ui.theme.AppTheme import co.nimblehq.compose.crypto.ui.theme.ComposeTheme +const val TestTagDetailAppbarTitle = "AppbarTitle" + @Composable fun Appbar( modifier: Modifier, @@ -33,7 +36,9 @@ fun Appbar( title?.let { Text( - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .align(Alignment.Center) + .testTag(tag = TestTagDetailAppbarTitle), text = title, color = AppTheme.colors.text, style = AppTheme.styles.medium16 diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt index f47d4a29..d1b8f4f1 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt @@ -6,6 +6,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout @@ -14,6 +15,10 @@ import co.nimblehq.compose.crypto.R import co.nimblehq.compose.crypto.ui.common.price.PriceChange import co.nimblehq.compose.crypto.ui.theme.* +const val TestTagDetailItemTitle = "DetailItemTitle" +const val TestTagDetailItemPrice = "DetailItemPrice" +const val TestTagDetailItemPriceChange = "DetailItemPriceChange" + @Composable fun DetailItem( modifier: Modifier, @@ -35,7 +40,8 @@ fun DetailItem( .constrainAs(itemTitle) { top.linkTo(parent.top) start.linkTo(parent.start) - }, + } + .testTag(tag = TestTagDetailItemTitle), text = title, color = AppTheme.colors.coinNameText, style = AppTheme.styles.medium12 @@ -48,7 +54,8 @@ fun DetailItem( start.linkTo(itemTitle.start) top.linkTo(itemTitle.bottom) width = Dimension.preferredWrapContent - }, + } + .testTag(tag = TestTagDetailItemPrice), text = stringResource(R.string.coin_currency, price), color = AppTheme.colors.text, style = AppTheme.styles.medium16 @@ -61,7 +68,8 @@ fun DetailItem( end.linkTo(parent.end) top.linkTo(itemTitle.top) bottom.linkTo(itemPrice.bottom) - }, + } + .testTag(tag = TestTagDetailItemPriceChange), displayForDetailPage = true ) } diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt index 9089a6b4..19ed8adb 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +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 @@ -41,6 +42,15 @@ import me.bytebeats.views.charts.line.render.line.SolidLineDrawer import me.bytebeats.views.charts.line.render.point.EmptyPointDrawer import me.bytebeats.views.charts.simpleChartAnimation +const val TestTagDetailLogo = "DetailLogo" +const val TestTagDetailCurrentPrice = "DetailCurrentPrice" +const val TestTagDetailPriceChangePercent = "DetailPriceChangePercent" +const val TestTagDetailCircularProgress = "DetailCircularProgress" +const val TestTagDetailLineChart = "DetailLineChart" +const val TestTagDetailChartInterval = "DetailChartInterval" +const val TestTagDetailCoinInfo = "DetailCoinInfo" +const val TestTagDetailSellBuyGroup = "DetailSellBuyGroup" + @Composable fun DetailScreen( viewModel: DetailViewModel = hiltViewModel(), @@ -127,7 +137,8 @@ private fun DetailScreenContent( top.linkTo(appBar.bottom) linkTo(start = parent.start, end = parent.end) } - .padding(top = Dp8), + .padding(top = Dp8) + .testTag(tag = TestTagDetailLogo), painter = rememberAsyncImagePainter(coinDetailUiModel.image), contentDescription = null ) @@ -138,7 +149,8 @@ private fun DetailScreenContent( top.linkTo(logo.bottom) linkTo(start = parent.start, end = parent.end) } - .padding(vertical = Dp8), + .padding(vertical = Dp8) + .testTag(tag = TestTagDetailCurrentPrice), text = stringResource( R.string.coin_currency, coinDetailUiModel.currentPrice.toFormattedString() @@ -152,7 +164,8 @@ private fun DetailScreenContent( .constrainAs(priceChangePercent) { top.linkTo(currentPrice.bottom) linkTo(start = parent.start, end = parent.end) - }, + } + .testTag(tag = TestTagDetailPriceChangePercent), priceChangePercent = coinDetailUiModel.priceChangePercentage24hInCurrency, displayForDetailPage = true ) @@ -164,7 +177,8 @@ private fun DetailScreenContent( top.linkTo(priceChangePercent.bottom, margin = Dp24) start.linkTo(parent.start) end.linkTo(parent.end) - }, + } + .testTag(tag = TestTagDetailLineChart), lineChartData = LineChartData( points = coinPrices.map { coinPrice -> val price = stringResource( @@ -189,11 +203,13 @@ private fun DetailScreenContent( // Chart intervals ChartIntervalsButtonGroup( - modifier = Modifier.constrainAs(intervals) { - top.linkTo(graph.bottom, margin = Dp24) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, + modifier = Modifier + .constrainAs(intervals) { + top.linkTo(graph.bottom, margin = Dp24) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .testTag(tag = TestTagDetailChartInterval), onIntervalChanged = onTimeIntervalsChanged::invoke ) @@ -216,7 +232,8 @@ private fun DetailScreenContent( top = parent.top, bottom = parent.bottom ) - }, + } + .testTag(tag = TestTagDetailCircularProgress), ) } } @@ -225,7 +242,8 @@ private fun DetailScreenContent( Box( modifier = Modifier .fillMaxSize() - .navigationBarsPadding(), + .navigationBarsPadding() + .testTag(tag = TestTagDetailSellBuyGroup), contentAlignment = Alignment.BottomEnd ) { SellBuyGroup( @@ -244,7 +262,11 @@ private fun CoinInfo( sellBuyLayoutHeight: Dp, coinDetailUiModel: CoinDetailUiModel ) { - Column(modifier = modifier.padding(start = Dp16, end = Dp16, bottom = sellBuyLayoutHeight)) { + Column( + modifier = modifier + .padding(start = Dp16, end = Dp16, bottom = sellBuyLayoutHeight) + .testTag(tag = TestTagDetailCoinInfo) + ) { DetailItem( modifier = Modifier, title = stringResource(id = R.string.detail_market_cap_title), diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt new file mode 100644 index 00000000..61b29f50 --- /dev/null +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt @@ -0,0 +1,178 @@ +package co.nimblehq.compose.crypto.ui.screens.detail + +import androidx.activity.compose.setContent +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.* +import androidx.navigation.* +import co.nimblehq.compose.crypto.R +import co.nimblehq.compose.crypto.domain.usecase.GetCoinDetailUseCase +import co.nimblehq.compose.crypto.domain.usecase.GetCoinPricesUseCase +import co.nimblehq.compose.crypto.extension.toFormattedString +import co.nimblehq.compose.crypto.test.MockUtil +import co.nimblehq.compose.crypto.ui.navigation.AppDestination +import co.nimblehq.compose.crypto.ui.screens.BaseViewModelTest +import co.nimblehq.compose.crypto.ui.screens.MainActivity +import co.nimblehq.compose.crypto.ui.uimodel.toUiModel +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowToast +import kotlin.math.abs + +@RunWith(RobolectricTestRunner::class) +@ExperimentalCoroutinesApi +class DetailScreenTest : BaseViewModelTest() { + + @get:Rule + val composeAndroidTestRule = createAndroidComposeRule() + + private val marketCapTitle: String + get() = composeAndroidTestRule.activity.getString(R.string.detail_market_cap_title) + + private val allTimeHighTitle: String + get() = composeAndroidTestRule.activity.getString(R.string.detail_all_time_high_title) + + private val allTimeLowTitle: String + get() = composeAndroidTestRule.activity.getString(R.string.detail_all_time_low_title) + + private val errorGeneric: String + get() = composeAndroidTestRule.activity.getString(R.string.error_generic) + + private val mockGetCoinDetailUseCase = mockk() + private val mockGetCoinPricesUseCase = mockk() + + private lateinit var viewModel: DetailViewModel + + private var appDestination: AppDestination? = null + + private val coinDetailUiModel = MockUtil.coinDetail.toUiModel() + + @Before + fun setUp() { + every { mockGetCoinDetailUseCase.execute(any()) } returns flowOf(MockUtil.coinDetail) + + composeAndroidTestRule.activity.setContent { + DetailScreen( + viewModel = viewModel, + navigator = { destination -> appDestination = destination }, + coinId = "Bitcoin" + ) + } + } + + @Test + fun `When enter to DetailScreen, it shows the Loading properly`() { + initViewModel() + + composeAndroidTestRule.onNodeWithTag( + testTag = TestTagDetailCircularProgress + ).assertIsDisplayed() + } + + @Test + fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the HeaderDetail properly`() { + initViewModel() + + with(composeAndroidTestRule) { + onNodeWithTag(testTag = TestTagDetailAppbarTitle).assertTextEquals(coinDetailUiModel.coinName) + onNodeWithTag(testTag = TestTagDetailLogo).assertIsDisplayed() + onNodeWithTag(testTag = TestTagDetailCurrentPrice).assertTextEquals( + "$${coinDetailUiModel.currentPrice.toFormattedString()}" + ) + onNodeWithTag(testTag = TestTagDetailPriceChangePercent).assertTextEquals( + "${abs(coinDetailUiModel.priceChangePercentage24hInCurrency).toFormattedString()}%" + ) + } + } + + @Test + fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the Chart properly`() { + initViewModel() + + with(composeAndroidTestRule) { + onNodeWithTag(testTag = TestTagDetailLineChart).assertExists() + onNodeWithTag(testTag = TestTagDetailChartInterval).assertExists() + } + } + + @Test + fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the CoinInfo properly`() { + initViewModel() + + with(composeAndroidTestRule) { + onNodeWithTag(testTag = TestTagDetailCoinInfo).assertExists() + + with(onAllNodesWithTag(testTag = TestTagDetailItemTitle)) { + onFirst().assertTextEquals(marketCapTitle) + this[1].assertTextEquals(allTimeHighTitle) + onLast().assertTextEquals(allTimeLowTitle) + } + + with(onAllNodesWithTag(testTag = TestTagDetailItemPrice)) { + onFirst().assertTextEquals("$${coinDetailUiModel.marketCap.toFormattedString()}") + this[1].assertTextEquals("$${coinDetailUiModel.ath.toFormattedString()}") + onLast().assertTextEquals("$${coinDetailUiModel.atl.toFormattedString()}") + } + + with(onAllNodesWithTag( + testTag = TestTagDetailItemPriceChange + )) { + onFirst().onChild().assertTextEquals( + "${abs(coinDetailUiModel.marketCapChangePercentage24h).toFormattedString()}%" + ) + this[1].onChild().assertTextEquals( + "${abs(coinDetailUiModel.athChangePercentage).toFormattedString()}%" + ) + onLast().onChild().assertTextEquals( + "${abs(coinDetailUiModel.atlChangePercentage).toFormattedString()}%" + ) + } + } + } + + @Test + fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the SellBuyGroup properly`() { + initViewModel() + + composeAndroidTestRule.onNodeWithTag(testTag = TestTagDetailSellBuyGroup).assertExists() + } + + @Test + fun `When enter to DetailScreen and GetCoinDetail failed, it shows the Toast properly`() { + every { mockGetCoinDetailUseCase.execute(any()) } returns flow { + throw Throwable(errorGeneric) + } + + initViewModel() + + with(composeAndroidTestRule) { + onNodeWithTag(testTag = TestTagDetailAppbarTitle).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailLogo).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailCurrentPrice).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailPriceChangePercent).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailLineChart).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailChartInterval).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailCoinInfo).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailSellBuyGroup).assertDoesNotExist() + } + + ShadowToast.getTextOfLatestToast() shouldBe errorGeneric + } + + private fun initViewModel() { + viewModel = DetailViewModel( + dispatchers = testDispatcherProvider, + getCoinDetailUseCase = mockGetCoinDetailUseCase, + getCoinPricesUseCase = mockGetCoinPricesUseCase + ) + } +} From 78f2286e9b15af9bb78332387c1e2a251d6a72aa Mon Sep 17 00:00:00 2001 From: Wadeewee Date: Mon, 20 Feb 2023 17:13:30 +0700 Subject: [PATCH 2/5] [#70] Move initViewModel() to setup() --- .../ui/screens/detail/DetailScreenTest.kt | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt index 61b29f50..e20163ba 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt @@ -60,6 +60,8 @@ class DetailScreenTest : BaseViewModelTest() { fun setUp() { every { mockGetCoinDetailUseCase.execute(any()) } returns flowOf(MockUtil.coinDetail) + initViewModel() + composeAndroidTestRule.activity.setContent { DetailScreen( viewModel = viewModel, @@ -71,8 +73,6 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When enter to DetailScreen, it shows the Loading properly`() { - initViewModel() - composeAndroidTestRule.onNodeWithTag( testTag = TestTagDetailCircularProgress ).assertIsDisplayed() @@ -80,8 +80,6 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the HeaderDetail properly`() { - initViewModel() - with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailAppbarTitle).assertTextEquals(coinDetailUiModel.coinName) onNodeWithTag(testTag = TestTagDetailLogo).assertIsDisplayed() @@ -96,8 +94,6 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the Chart properly`() { - initViewModel() - with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailLineChart).assertExists() onNodeWithTag(testTag = TestTagDetailChartInterval).assertExists() @@ -106,8 +102,6 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the CoinInfo properly`() { - initViewModel() - with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailCoinInfo).assertExists() @@ -123,9 +117,11 @@ class DetailScreenTest : BaseViewModelTest() { onLast().assertTextEquals("$${coinDetailUiModel.atl.toFormattedString()}") } - with(onAllNodesWithTag( - testTag = TestTagDetailItemPriceChange - )) { + with( + onAllNodesWithTag( + testTag = TestTagDetailItemPriceChange + ) + ) { onFirst().onChild().assertTextEquals( "${abs(coinDetailUiModel.marketCapChangePercentage24h).toFormattedString()}%" ) @@ -141,8 +137,6 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the SellBuyGroup properly`() { - initViewModel() - composeAndroidTestRule.onNodeWithTag(testTag = TestTagDetailSellBuyGroup).assertExists() } @@ -152,8 +146,6 @@ class DetailScreenTest : BaseViewModelTest() { throw Throwable(errorGeneric) } - initViewModel() - with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailAppbarTitle).assertDoesNotExist() onNodeWithTag(testTag = TestTagDetailLogo).assertDoesNotExist() From 11a1f29958dde5309758244e93467b98a4cb36ac Mon Sep 17 00:00:00 2001 From: Wadeewee Date: Mon, 20 Feb 2023 17:37:39 +0700 Subject: [PATCH 3/5] [#70] Remove some TestTag for testing text --- .../crypto/ui/screens/detail/Appbar.kt | 7 +- .../crypto/ui/screens/detail/DetailItem.kt | 14 +--- .../crypto/ui/screens/detail/DetailScreen.kt | 8 +-- .../ui/screens/detail/DetailScreenTest.kt | 66 +++++++++---------- 4 files changed, 37 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt index e941f25e..98b2f187 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/Appbar.kt @@ -6,15 +6,12 @@ import androidx.compose.material.* 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.tooling.preview.Preview import co.nimblehq.compose.crypto.R import co.nimblehq.compose.crypto.ui.theme.AppTheme import co.nimblehq.compose.crypto.ui.theme.ComposeTheme -const val TestTagDetailAppbarTitle = "AppbarTitle" - @Composable fun Appbar( modifier: Modifier, @@ -36,9 +33,7 @@ fun Appbar( title?.let { Text( - modifier = Modifier - .align(Alignment.Center) - .testTag(tag = TestTagDetailAppbarTitle), + modifier = Modifier.align(Alignment.Center), text = title, color = AppTheme.colors.text, style = AppTheme.styles.medium16 diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt index d1b8f4f1..f47d4a29 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailItem.kt @@ -6,7 +6,6 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout @@ -15,10 +14,6 @@ import co.nimblehq.compose.crypto.R import co.nimblehq.compose.crypto.ui.common.price.PriceChange import co.nimblehq.compose.crypto.ui.theme.* -const val TestTagDetailItemTitle = "DetailItemTitle" -const val TestTagDetailItemPrice = "DetailItemPrice" -const val TestTagDetailItemPriceChange = "DetailItemPriceChange" - @Composable fun DetailItem( modifier: Modifier, @@ -40,8 +35,7 @@ fun DetailItem( .constrainAs(itemTitle) { top.linkTo(parent.top) start.linkTo(parent.start) - } - .testTag(tag = TestTagDetailItemTitle), + }, text = title, color = AppTheme.colors.coinNameText, style = AppTheme.styles.medium12 @@ -54,8 +48,7 @@ fun DetailItem( start.linkTo(itemTitle.start) top.linkTo(itemTitle.bottom) width = Dimension.preferredWrapContent - } - .testTag(tag = TestTagDetailItemPrice), + }, text = stringResource(R.string.coin_currency, price), color = AppTheme.colors.text, style = AppTheme.styles.medium16 @@ -68,8 +61,7 @@ fun DetailItem( end.linkTo(parent.end) top.linkTo(itemTitle.top) bottom.linkTo(itemPrice.bottom) - } - .testTag(tag = TestTagDetailItemPriceChange), + }, displayForDetailPage = true ) } diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt index 19ed8adb..51fdd8f7 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreen.kt @@ -43,8 +43,6 @@ import me.bytebeats.views.charts.line.render.point.EmptyPointDrawer import me.bytebeats.views.charts.simpleChartAnimation const val TestTagDetailLogo = "DetailLogo" -const val TestTagDetailCurrentPrice = "DetailCurrentPrice" -const val TestTagDetailPriceChangePercent = "DetailPriceChangePercent" const val TestTagDetailCircularProgress = "DetailCircularProgress" const val TestTagDetailLineChart = "DetailLineChart" const val TestTagDetailChartInterval = "DetailChartInterval" @@ -149,8 +147,7 @@ private fun DetailScreenContent( top.linkTo(logo.bottom) linkTo(start = parent.start, end = parent.end) } - .padding(vertical = Dp8) - .testTag(tag = TestTagDetailCurrentPrice), + .padding(vertical = Dp8), text = stringResource( R.string.coin_currency, coinDetailUiModel.currentPrice.toFormattedString() @@ -164,8 +161,7 @@ private fun DetailScreenContent( .constrainAs(priceChangePercent) { top.linkTo(currentPrice.bottom) linkTo(start = parent.start, end = parent.end) - } - .testTag(tag = TestTagDetailPriceChangePercent), + }, priceChangePercent = coinDetailUiModel.priceChangePercentage24hInCurrency, displayForDetailPage = true ) diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt index e20163ba..02f392fc 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt @@ -81,14 +81,15 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the HeaderDetail properly`() { with(composeAndroidTestRule) { - onNodeWithTag(testTag = TestTagDetailAppbarTitle).assertTextEquals(coinDetailUiModel.coinName) onNodeWithTag(testTag = TestTagDetailLogo).assertIsDisplayed() - onNodeWithTag(testTag = TestTagDetailCurrentPrice).assertTextEquals( + + onNodeWithText(coinDetailUiModel.coinName).assertIsDisplayed() + onNodeWithText( "$${coinDetailUiModel.currentPrice.toFormattedString()}" - ) - onNodeWithTag(testTag = TestTagDetailPriceChangePercent).assertTextEquals( + ).assertIsDisplayed() + onNodeWithText( "${abs(coinDetailUiModel.priceChangePercentage24hInCurrency).toFormattedString()}%" - ) + ).assertIsDisplayed() } } @@ -105,33 +106,23 @@ class DetailScreenTest : BaseViewModelTest() { with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailCoinInfo).assertExists() - with(onAllNodesWithTag(testTag = TestTagDetailItemTitle)) { - onFirst().assertTextEquals(marketCapTitle) - this[1].assertTextEquals(allTimeHighTitle) - onLast().assertTextEquals(allTimeLowTitle) - } - - with(onAllNodesWithTag(testTag = TestTagDetailItemPrice)) { - onFirst().assertTextEquals("$${coinDetailUiModel.marketCap.toFormattedString()}") - this[1].assertTextEquals("$${coinDetailUiModel.ath.toFormattedString()}") - onLast().assertTextEquals("$${coinDetailUiModel.atl.toFormattedString()}") - } - - with( - onAllNodesWithTag( - testTag = TestTagDetailItemPriceChange - ) - ) { - onFirst().onChild().assertTextEquals( - "${abs(coinDetailUiModel.marketCapChangePercentage24h).toFormattedString()}%" - ) - this[1].onChild().assertTextEquals( - "${abs(coinDetailUiModel.athChangePercentage).toFormattedString()}%" - ) - onLast().onChild().assertTextEquals( - "${abs(coinDetailUiModel.atlChangePercentage).toFormattedString()}%" - ) - } + onNodeWithText(marketCapTitle).assertIsDisplayed() + onNodeWithText(allTimeHighTitle).assertIsDisplayed() + onNodeWithText(allTimeLowTitle).assertIsDisplayed() + + onNodeWithText("$${coinDetailUiModel.marketCap.toFormattedString()}").assertIsDisplayed() + onNodeWithText("$${coinDetailUiModel.ath.toFormattedString()}").assertIsDisplayed() + onNodeWithText("$${coinDetailUiModel.atl.toFormattedString()}").assertIsDisplayed() + + onNodeWithText( + "${abs(coinDetailUiModel.marketCapChangePercentage24h).toFormattedString()}%" + ).assertIsDisplayed() + onNodeWithText( + "${abs(coinDetailUiModel.athChangePercentage).toFormattedString()}%" + ).assertIsDisplayed() + onNodeWithText( + "${abs(coinDetailUiModel.atlChangePercentage).toFormattedString()}%" + ).assertIsDisplayed() } } @@ -147,10 +138,15 @@ class DetailScreenTest : BaseViewModelTest() { } with(composeAndroidTestRule) { - onNodeWithTag(testTag = TestTagDetailAppbarTitle).assertDoesNotExist() + onNodeWithText(coinDetailUiModel.coinName).assertDoesNotExist() + onNodeWithText( + "$${coinDetailUiModel.currentPrice.toFormattedString()}" + ).assertDoesNotExist() + onNodeWithText( + "${abs(coinDetailUiModel.priceChangePercentage24hInCurrency).toFormattedString()}%" + ).assertDoesNotExist() + onNodeWithTag(testTag = TestTagDetailLogo).assertDoesNotExist() - onNodeWithTag(testTag = TestTagDetailCurrentPrice).assertDoesNotExist() - onNodeWithTag(testTag = TestTagDetailPriceChangePercent).assertDoesNotExist() onNodeWithTag(testTag = TestTagDetailLineChart).assertDoesNotExist() onNodeWithTag(testTag = TestTagDetailChartInterval).assertDoesNotExist() onNodeWithTag(testTag = TestTagDetailCoinInfo).assertDoesNotExist() From c23a6f01b7731827e6ac5eb0837e37e0b92cad93 Mon Sep 17 00:00:00 2001 From: Wadeewee Date: Mon, 20 Feb 2023 17:51:48 +0700 Subject: [PATCH 4/5] [#70] Add assertion for chart like UI Test and rename test cases --- .../ui/screens/detail/DetailScreenTest.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt index 02f392fc..fbfeb0d6 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt @@ -9,6 +9,7 @@ import co.nimblehq.compose.crypto.domain.usecase.GetCoinDetailUseCase import co.nimblehq.compose.crypto.domain.usecase.GetCoinPricesUseCase import co.nimblehq.compose.crypto.extension.toFormattedString import co.nimblehq.compose.crypto.test.MockUtil +import co.nimblehq.compose.crypto.ui.components.chartintervals.TimeIntervals import co.nimblehq.compose.crypto.ui.navigation.AppDestination import co.nimblehq.compose.crypto.ui.screens.BaseViewModelTest import co.nimblehq.compose.crypto.ui.screens.MainActivity @@ -72,14 +73,14 @@ class DetailScreenTest : BaseViewModelTest() { } @Test - fun `When enter to DetailScreen, it shows the Loading properly`() { + fun `When entering to the DetailScreen, it shows the loading properly`() { composeAndroidTestRule.onNodeWithTag( testTag = TestTagDetailCircularProgress ).assertIsDisplayed() } @Test - fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the HeaderDetail properly`() { + fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the HeaderDetail properly`() { with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailLogo).assertIsDisplayed() @@ -94,15 +95,21 @@ class DetailScreenTest : BaseViewModelTest() { } @Test - fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the Chart properly`() { + fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the Chart properly`() { with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailLineChart).assertExists() onNodeWithTag(testTag = TestTagDetailChartInterval).assertExists() + + onNodeWithText(TimeIntervals.ONE_DAY.text).assertIsDisplayed().assertHasClickAction() + onNodeWithText(TimeIntervals.ONE_WEEK.text).assertIsDisplayed().assertHasClickAction() + onNodeWithText(TimeIntervals.ONE_MONTH.text).assertIsDisplayed().assertHasClickAction() + onNodeWithText(TimeIntervals.ONE_YEAR.text).assertIsDisplayed().assertHasClickAction() + onNodeWithText(TimeIntervals.FIVE_YEAR.text).assertIsDisplayed().assertHasClickAction() } } @Test - fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the CoinInfo properly`() { + fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the CoinInfo properly`() { with(composeAndroidTestRule) { onNodeWithTag(testTag = TestTagDetailCoinInfo).assertExists() @@ -127,12 +134,12 @@ class DetailScreenTest : BaseViewModelTest() { } @Test - fun `When enter to DetailScreen and GetCoinDetail successfully, it renders the SellBuyGroup properly`() { + fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the SellBuyGroup properly`() { composeAndroidTestRule.onNodeWithTag(testTag = TestTagDetailSellBuyGroup).assertExists() } @Test - fun `When enter to DetailScreen and GetCoinDetail failed, it shows the Toast properly`() { + fun `When entering to the DetailScreen and GetCoinDetail failed, it shows the error message properly`() { every { mockGetCoinDetailUseCase.execute(any()) } returns flow { throw Throwable(errorGeneric) } From 63c027a0ff4b34d098fed943e27de5da7f892010 Mon Sep 17 00:00:00 2001 From: Wadeewee Date: Tue, 21 Feb 2023 13:34:16 +0700 Subject: [PATCH 5/5] [#70] Mockk answer for GetCoinPricesUseCase --- .../compose/crypto/ui/screens/detail/DetailScreenTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt index fbfeb0d6..515a1cf6 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/detail/DetailScreenTest.kt @@ -60,6 +60,7 @@ class DetailScreenTest : BaseViewModelTest() { @Before fun setUp() { every { mockGetCoinDetailUseCase.execute(any()) } returns flowOf(MockUtil.coinDetail) + every { mockGetCoinPricesUseCase.execute(any()) } returns flowOf(emptyList()) initViewModel() @@ -74,6 +75,8 @@ class DetailScreenTest : BaseViewModelTest() { @Test fun `When entering to the DetailScreen, it shows the loading properly`() { + every { mockGetCoinDetailUseCase.execute(any()) } returns flow { delay(100) } + composeAndroidTestRule.onNodeWithTag( testTag = TestTagDetailCircularProgress ).assertIsDisplayed()