androidTest: Add tests for reusable composable

* Add semantics wherever required to make composable easy to test

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-11-10 15:45:22 +08:00
parent da7890cffe
commit 6de6498418
12 changed files with 260 additions and 7 deletions

View File

@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.createComposeRule
import com.aurora.store.compose.theme.AuroraTheme
import org.junit.Rule
/**
* Class that provides helper methods to test isolated composable
*/
abstract class IsolatedTest {
@get:Rule
val composeTestRule = createComposeRule()
/**
* Sets given composable as content with default theme
*/
fun setContent(content: @Composable () -> Unit) {
composeTestRule.setContent {
AuroraTheme(content = content)
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithText
import com.aurora.store.IsolatedTest
import com.aurora.store.R
import org.junit.Test
class ErrorTest : IsolatedTest() {
@Test
fun testErrorWithoutActionHandling() {
setContent {
Error(
painter = painterResource(R.drawable.ic_apps_outage),
message = "An error occurred!",
actionMessage = "Retry"
)
}
composeTestRule.onNodeWithText("Retry")
.assertIsDisplayed()
.assertHasClickAction()
.assertIsNotEnabled()
}
}

View File

@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.text.AnnotatedString
import com.aurora.store.IsolatedTest
import com.aurora.store.R
import org.junit.Test
class InfoTest: IsolatedTest() {
@Test
fun testInfoWithoutClickHandling() {
setContent {
Info(title = AnnotatedString(text = stringResource(R.string.app_name)))
}
composeTestRule.onNodeWithText("Aurora Store")
.assertIsDisplayed()
.assertHasClickAction()
.assertIsNotEnabled()
}
}

View File

@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composable
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithText
import com.aurora.store.IsolatedTest
import com.aurora.store.R
import com.aurora.store.data.model.Permission
import com.aurora.store.data.model.PermissionType
import org.junit.Test
class PermissionListItemTest : IsolatedTest() {
private val permission: Permission
@Composable
get() = Permission(
PermissionType.STORAGE_MANAGER,
stringResource(R.string.onboarding_permission_esm),
stringResource(R.string.onboarding_permission_esa_desc),
)
@Test
fun testPermissionNotGranted() {
setContent {
PermissionListItem(permission = permission.copy(isGranted = false))
}
composeTestRule.onNodeWithText("Grant")
.assertIsDisplayed()
.assertIsEnabled()
}
@Test
fun testPermissionGranted() {
setContent {
PermissionListItem(permission = permission.copy(isGranted = true))
}
composeTestRule.onNodeWithText("Granted")
.assertIsDisplayed()
.assertIsNotEnabled()
}
}

View File

@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composable
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import com.aurora.store.IsolatedTest
import org.junit.Test
class TopAppBarTest : IsolatedTest() {
@Test
fun testTitleWithNoNavigationAction() {
setContent {
TopAppBar(title = "About", onNavigateUp = null)
}
composeTestRule.onNodeWithText("About")
.assertIsDisplayed()
composeTestRule.onNodeWithContentDescription("Back")
.assertDoesNotExist()
}
}

View File

@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composable.app
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertRangeInfoEquals
import androidx.compose.ui.test.onNodeWithTag
import com.aurora.store.IsolatedTest
import org.junit.Test
class AnimatedAppIconTest: IsolatedTest() {
@Test
fun testAnimatedAppIconNoProgress() {
setContent {
AnimatedAppIcon(
iconUrl = "https://example.com/icon.png",
inProgress = false
)
}
composeTestRule.onNodeWithTag("progressIndicator")
.assertIsNotDisplayed()
}
@Test
fun testAnimatedAppIconProgressAt0() {
setContent {
AnimatedAppIcon(
iconUrl = "https://example.com/icon.png",
progress = 0F,
inProgress = true
)
}
composeTestRule.onNodeWithTag("progressIndicator")
.assertIsDisplayed()
.assertRangeInfoEquals(ProgressBarRangeInfo.Indeterminate)
}
@Test
fun testAnimatedAppIconProgressAt50() {
setContent {
AnimatedAppIcon(
iconUrl = "https://example.com/icon.png",
progress = 50F,
inProgress = true
)
}
composeTestRule.onNodeWithTag("progressIndicator")
.assertIsDisplayed()
.assertRangeInfoEquals(ProgressBarRangeInfo(0.5F, 0.00F..1.00F))
}
}

View File

@@ -14,6 +14,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.preview.PreviewTemplate import com.aurora.store.compose.preview.PreviewTemplate
@@ -30,8 +33,11 @@ fun ContainedLoadingIndicator(modifier: Modifier = Modifier) {
.padding(dimensionResource(R.dimen.padding_small)), .padding(dimensionResource(R.dimen.padding_small)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val description = stringResource(R.string.loading)
ContainedLoadingIndicator( ContainedLoadingIndicator(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_small)) modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_small))
.semantics { stateDescription = description }
) )
} }
} }

View File

@@ -41,7 +41,7 @@ fun Error(
painter: Painter, painter: Painter,
message: String, message: String,
actionMessage: String? = null, actionMessage: String? = null,
onAction: () -> Unit = {} onAction: (() -> Unit)? = null
) { ) {
Column( Column(
modifier = modifier modifier = modifier
@@ -65,7 +65,7 @@ fun Error(
) )
if (actionMessage != null) { if (actionMessage != null) {
Button(onClick = onAction) { Button(onClick = { if (onAction != null) onAction() }, enabled = onAction != null) {
Text( Text(
text = actionMessage, text = actionMessage,
maxLines = 1, maxLines = 1,

View File

@@ -22,6 +22,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.preview.PreviewTemplate import com.aurora.store.compose.preview.PreviewTemplate
@@ -44,6 +47,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
) )
) { ) {
repeat(totalPages) { iteration -> repeat(totalPages) { iteration ->
val page = stringResource(R.string.page, iteration)
val isSelected = currentPage == iteration val isSelected = currentPage == iteration
val color by animateColorAsState( val color by animateColorAsState(
targetValue = if (isSelected) { targetValue = if (isSelected) {
@@ -67,6 +71,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
.size(size) .size(size)
.clip(CircleShape) .clip(CircleShape)
.background(color = color) .background(color = color)
.semantics { stateDescription = page }
) )
} }
} }

View File

@@ -23,6 +23,8 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -65,7 +67,9 @@ fun AnimatedAppIcon(
Box(modifier = modifier, contentAlignment = Alignment.Center) { Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (inProgress) { if (inProgress) {
val indicatorModifier = Modifier.fillMaxSize() val indicatorModifier = Modifier
.fillMaxSize()
.semantics { testTag = "progressIndicator" }
if (animatedProgress > 0) { if (animatedProgress > 0) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = indicatorModifier, modifier = indicatorModifier,

View File

@@ -16,8 +16,9 @@ import com.aurora.store.compose.theme.AuroraTheme
@Composable @Composable
fun PreviewTemplate(content : @Composable () -> Unit) { fun PreviewTemplate(content : @Composable () -> Unit) {
AuroraTheme { AuroraTheme {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) { CompositionLocalProvider(
content() value = LocalAsyncImagePreviewHandler provides coilPreviewProvider,
} content = content
)
} }
} }

View File

@@ -546,4 +546,8 @@
<string name="select_app_for_details">Select an app for more details</string> <string name="select_app_for_details">Select an app for more details</string>
<string name="action_filter_no_ads">No advertisements</string> <string name="action_filter_no_ads">No advertisements</string>
<string name="action_filter_no_gms">No play services</string> <string name="action_filter_no_gms">No play services</string>
<!-- Misc -->
<string name="loading">Loading in progress</string>
<string name="page">Page <xliff:g id="number">%1$d</xliff:g></string>
</resources> </resources>