diff --git a/app/src/androidTest/java/com/aurora/store/IsolatedTest.kt b/app/src/androidTest/java/com/aurora/store/IsolatedTest.kt
new file mode 100644
index 000000000..103c98714
--- /dev/null
+++ b/app/src/androidTest/java/com/aurora/store/IsolatedTest.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/androidTest/java/com/aurora/store/compose/composable/ErrorTest.kt b/app/src/androidTest/java/com/aurora/store/compose/composable/ErrorTest.kt
new file mode 100644
index 000000000..2cd0b9374
--- /dev/null
+++ b/app/src/androidTest/java/com/aurora/store/compose/composable/ErrorTest.kt
@@ -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()
+ }
+}
diff --git a/app/src/androidTest/java/com/aurora/store/compose/composable/InfoTest.kt b/app/src/androidTest/java/com/aurora/store/compose/composable/InfoTest.kt
new file mode 100644
index 000000000..134c985b9
--- /dev/null
+++ b/app/src/androidTest/java/com/aurora/store/compose/composable/InfoTest.kt
@@ -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()
+ }
+}
diff --git a/app/src/androidTest/java/com/aurora/store/compose/composable/PermissionListItemTest.kt b/app/src/androidTest/java/com/aurora/store/compose/composable/PermissionListItemTest.kt
new file mode 100644
index 000000000..632662a57
--- /dev/null
+++ b/app/src/androidTest/java/com/aurora/store/compose/composable/PermissionListItemTest.kt
@@ -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()
+ }
+}
diff --git a/app/src/androidTest/java/com/aurora/store/compose/composable/TopAppBarTest.kt b/app/src/androidTest/java/com/aurora/store/compose/composable/TopAppBarTest.kt
new file mode 100644
index 000000000..0dec80424
--- /dev/null
+++ b/app/src/androidTest/java/com/aurora/store/compose/composable/TopAppBarTest.kt
@@ -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()
+ }
+}
diff --git a/app/src/androidTest/java/com/aurora/store/compose/composable/app/AnimatedAppIconTest.kt b/app/src/androidTest/java/com/aurora/store/compose/composable/app/AnimatedAppIconTest.kt
new file mode 100644
index 000000000..86743e71d
--- /dev/null
+++ b/app/src/androidTest/java/com/aurora/store/compose/composable/app/AnimatedAppIconTest.kt
@@ -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))
+ }
+}
diff --git a/app/src/main/java/com/aurora/store/compose/composable/ContainedLoadingIndicator.kt b/app/src/main/java/com/aurora/store/compose/composable/ContainedLoadingIndicator.kt
index 18b9d0773..9978a7876 100644
--- a/app/src/main/java/com/aurora/store/compose/composable/ContainedLoadingIndicator.kt
+++ b/app/src/main/java/com/aurora/store/compose/composable/ContainedLoadingIndicator.kt
@@ -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 }
)
}
}
diff --git a/app/src/main/java/com/aurora/store/compose/composable/Error.kt b/app/src/main/java/com/aurora/store/compose/composable/Error.kt
index f9fc78b20..55793316a 100644
--- a/app/src/main/java/com/aurora/store/compose/composable/Error.kt
+++ b/app/src/main/java/com/aurora/store/compose/composable/Error.kt
@@ -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,
diff --git a/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt b/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt
index cd5db7713..951af76a3 100644
--- a/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt
+++ b/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt
@@ -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 }
)
}
}
diff --git a/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt b/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt
index 598618683..ac239c8e3 100644
--- a/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt
+++ b/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt
@@ -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,
diff --git a/app/src/main/java/com/aurora/store/compose/preview/PreviewTemplate.kt b/app/src/main/java/com/aurora/store/compose/preview/PreviewTemplate.kt
index b1891f078..1e42f5e7f 100644
--- a/app/src/main/java/com/aurora/store/compose/preview/PreviewTemplate.kt
+++ b/app/src/main/java/com/aurora/store/compose/preview/PreviewTemplate.kt
@@ -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
+ )
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3c564a9dc..417995243 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -546,4 +546,8 @@
Select an app for more details
No advertisements
No play services
+
+
+ Loading in progress
+ Page %1$d