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.Modifier
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 com.aurora.store.R
import com.aurora.store.compose.preview.PreviewTemplate
@@ -30,8 +33,11 @@ fun ContainedLoadingIndicator(modifier: Modifier = Modifier) {
.padding(dimensionResource(R.dimen.padding_small)),
contentAlignment = Alignment.Center
) {
val description = stringResource(R.string.loading)
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,
message: String,
actionMessage: String? = null,
onAction: () -> Unit = {}
onAction: (() -> Unit)? = null
) {
Column(
modifier = modifier
@@ -65,7 +65,7 @@ fun Error(
)
if (actionMessage != null) {
Button(onClick = onAction) {
Button(onClick = { if (onAction != null) onAction() }, enabled = onAction != null) {
Text(
text = actionMessage,
maxLines = 1,

View File

@@ -22,6 +22,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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 com.aurora.store.R
import com.aurora.store.compose.preview.PreviewTemplate
@@ -44,6 +47,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
)
) {
repeat(totalPages) { iteration ->
val page = stringResource(R.string.page, iteration)
val isSelected = currentPage == iteration
val color by animateColorAsState(
targetValue = if (isSelected) {
@@ -67,6 +71,7 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
.size(size)
.clip(CircleShape)
.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.platform.LocalContext
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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -65,7 +67,9 @@ fun AnimatedAppIcon(
Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (inProgress) {
val indicatorModifier = Modifier.fillMaxSize()
val indicatorModifier = Modifier
.fillMaxSize()
.semantics { testTag = "progressIndicator" }
if (animatedProgress > 0) {
CircularProgressIndicator(
modifier = indicatorModifier,

View File

@@ -16,8 +16,9 @@ import com.aurora.store.compose.theme.AuroraTheme
@Composable
fun PreviewTemplate(content : @Composable () -> Unit) {
AuroraTheme {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
content()
}
CompositionLocalProvider(
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="action_filter_no_ads">No advertisements</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>