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