diff --git a/app/build.gradle b/app/build.gradle index d656fcc3..d46273f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,6 +159,7 @@ dependencies { // Compose def composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) + testImplementation(composeBom) // Material Design 3 implementation libs.androidx.material3 @@ -167,6 +168,9 @@ dependencies { // Android Studio Preview support implementation libs.androidx.ui.tooling.preview debugImplementation libs.androidx.ui.tooling + // UI tests, run as Robolectric unit tests + testImplementation libs.androidx.ui.test.junit4 + debugImplementation libs.androidx.ui.test.manifest // Optional - Add full set of material icons implementation libs.androidx.material.icons.extended // Optional - Integration with activities diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt index 416835d5..66d4694d 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt @@ -32,8 +32,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.graphics.createBitmap @@ -66,7 +64,7 @@ fun AddAppSignaturePackageFilterDialog( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun AddAppSignaturePackageFilterDialog( +internal fun AddAppSignaturePackageFilterDialog( viewState: ViewState, closeDialog: () -> Unit = {}, appSelected: (AppInfo) -> Unit = {}, @@ -111,7 +109,7 @@ private fun AddAppSignaturePackageFilterDialog( modifier = Modifier .align(Alignment.End), onClick = closeDialog ) { - Text(text = stringResource(id = R.string.cancel).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.cancel)) } } ) { diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt index d39c6d7a..ce5b472e 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt @@ -25,8 +25,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -58,7 +56,7 @@ fun AddPackageNamePackageFilterDialog( } @Composable -private fun AddPackageNamePackageFilterDialog( +internal fun AddPackageNamePackageFilterDialog( viewState: AddPackageNamePackageFilterDialogViewModel.ViewState, closeDialog: () -> Unit = {}, addFilter: (String) -> Unit = {} @@ -98,10 +96,10 @@ private fun AddPackageNamePackageFilterDialog( Spacer(modifier = Modifier.size(8.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { TextButton(onClick = closeDialog) { - Text(text = stringResource(id = R.string.cancel).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.cancel)) } TextButton(onClick = { addFilter(text) }, enabled = text.isNotBlank()) { - Text(text = stringResource(id = R.string.add).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.add)) } } } diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/DeletePackageFilterDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/DeletePackageFilterDialog.kt index 20fba914..971a781b 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/DeletePackageFilterDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/DeletePackageFilterDialog.kt @@ -5,8 +5,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase import de.psdev.devdrawer.R @Composable @@ -21,12 +19,12 @@ fun DeletePackageFilterDialog( }, dismissButton = { TextButton(onClick = onDismiss) { - Text(text = stringResource(id = R.string.cancel).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.cancel)) } }, confirmButton = { TextButton(onClick = onConfirm) { - Text(text = stringResource(id = R.string.yes).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.yes)) } } ) diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterInfoDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterInfoDialog.kt index cccd81c9..9ee054ff 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterInfoDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterInfoDialog.kt @@ -19,8 +19,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.psdev.devdrawer.R @@ -65,7 +63,7 @@ fun PackageFilterInfoDialog( modifier = Modifier.align(Alignment.End), onClick = onDismiss ) { - Text(text = stringResource(id = R.string.close).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.close)) } } ) { diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt index de34bb92..5ee16f6b 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt @@ -20,8 +20,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.res.ResourcesCompat @@ -49,7 +47,7 @@ fun PackageFilterPreviewDialog( } @Composable -private fun PackageFilterPreviewDialog( +internal fun PackageFilterPreviewDialog( viewState: PackageFilterPreviewDialogViewModel.ViewState, closeDialog: () -> Unit = {} ) { @@ -67,7 +65,7 @@ private fun PackageFilterPreviewDialog( }, bottomContent = { TextButton(modifier = Modifier.align(Alignment.End), onClick = closeDialog) { - Text(text = stringResource(id = R.string.close).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.close)) } } ) { diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt index 35b1b676..f0acb450 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt @@ -43,8 +43,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -223,7 +221,7 @@ private sealed class WidgetProfileEditorDialogs { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun WidgetProfileEditor( +internal fun WidgetProfileEditor( viewState: WidgetProfileEditorViewState, modifier: Modifier = Modifier, onNameChange: (String) -> Unit = {}, @@ -266,7 +264,7 @@ private fun WidgetProfileEditor( contentDescription = stringResource(id = R.string.add_package_name) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(text = stringResource(id = R.string.add_package_name).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.add_package_name)) } Button( modifier = Modifier.weight(1f), @@ -278,7 +276,7 @@ private fun WidgetProfileEditor( contentDescription = stringResource(id = R.string.add_app_signature) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(text = stringResource(id = R.string.add_app_signature).toUpperCase(Locale.current)) + Text(text = stringResource(id = R.string.add_app_signature)) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbea42ca..e1d47c85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,7 +62,7 @@ Uninstall App details App signature - package name + Package name App icon Enter package name filter Select signature from app diff --git a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt new file mode 100644 index 00000000..8a80edc2 --- /dev/null +++ b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt @@ -0,0 +1,108 @@ +package de.psdev.devdrawer.profiles.ui.editor + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import de.psdev.devdrawer.database.FilterType +import de.psdev.devdrawer.database.PackageFilter +import de.psdev.devdrawer.ui.theme.DevDrawerTheme +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Dialog buttons use the string resources' sentence case ("Cancel"), not upper-cased labels ("CANCEL"). + */ +@RunWith(RobolectricTestRunner::class) +// SDK 36 requires Java 21; use SDK 33 which is compatible with the project's Java 17 toolchain. +@Config(sdk = [33]) +class DialogButtonLabelTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private val packageFilter = PackageFilter( + type = FilterType.SIGNATURE, + filter = "4f8d2c9e", + description = "Example App", + profileId = "profile" + ) + + @Test + fun `given the delete filter dialog, when shown, then its buttons are in sentence case`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { DeletePackageFilterDialog(onDismiss = {}, onConfirm = {}) } + } + + // Then + assertSentenceCase("Cancel", "Yes") + } + + @Test + fun `given the add package name filter dialog, when loaded, then its buttons are in sentence case`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { + AddPackageNamePackageFilterDialog( + viewState = AddPackageNamePackageFilterDialogViewModel.ViewState.Loaded(listOf("com.example.app")) + ) + } + } + + // Then + assertSentenceCase("Cancel", "Add") + } + + @Test + fun `given the add app signature filter dialog, when shown, then its button is in sentence case`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { + AddAppSignaturePackageFilterDialog( + viewState = AddAppSignaturePackageFilterDialogViewModel.ViewState.Loaded( + data = emptyList(), + showSystemApps = false + ) + ) + } + } + + // Then + assertSentenceCase("Cancel") + } + + @Test + fun `given the filter info dialog, when shown, then its button is in sentence case`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { PackageFilterInfoDialog(packageFilter = packageFilter, onDismiss = {}) } + } + + // Then + assertSentenceCase("Close") + } + + @Test + fun `given the filter preview dialog, when loaded, then its button is in sentence case`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { + PackageFilterPreviewDialog( + viewState = PackageFilterPreviewDialogViewModel.ViewState.Loaded(emptyList()) + ) + } + } + + // Then + assertSentenceCase("Close") + } + + private fun assertSentenceCase(vararg labels: String) { + labels.forEach { label -> + composeTestRule.onNodeWithText(label).assertExists() + composeTestRule.onNodeWithText(label.uppercase()).assertDoesNotExist() + } + } +} diff --git a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/FilterTypeLabelTest.kt b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/FilterTypeLabelTest.kt new file mode 100644 index 00000000..9d261044 --- /dev/null +++ b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/FilterTypeLabelTest.kt @@ -0,0 +1,73 @@ +package de.psdev.devdrawer.profiles.ui.editor + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import de.psdev.devdrawer.database.FilterType +import de.psdev.devdrawer.database.PackageFilter +import de.psdev.devdrawer.database.WidgetProfile +import de.psdev.devdrawer.ui.theme.DevDrawerTheme +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * The filter type labels ("Package name", "App signature") are shown in sentence case, + * neither upper-cased ("PACKAGE NAME") nor lower-cased ("package name"). + */ +@RunWith(RobolectricTestRunner::class) +// SDK 36 requires Java 21; use SDK 33 which is compatible with the project's Java 17 toolchain. +@Config(sdk = [33]) +class FilterTypeLabelTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `given the profile editor, when loaded, then the add filter buttons are in sentence case`() { + // Given + val widgetProfile = WidgetProfile(name = "Test profile") + + // When + composeTestRule.setContent { + DevDrawerTheme { + WidgetProfileEditor( + viewState = WidgetProfileEditorViewState( + widgetProfile = widgetProfile, + widgetName = widgetProfile.name + ) + ) + } + } + + // Then + assertSentenceCase("Package name", "App signature") + } + + @Test + fun `given a package name filter, when showing its info, then the title is in sentence case`() { + // Given + val packageFilter = PackageFilter( + type = FilterType.PACKAGE_NAME, + filter = "com.example.*", + profileId = "profile" + ) + + // When + composeTestRule.setContent { + DevDrawerTheme { PackageFilterInfoDialog(packageFilter = packageFilter, onDismiss = {}) } + } + + // Then + assertSentenceCase("Package name") + } + + private fun assertSentenceCase(vararg labels: String) { + labels.forEach { label -> + composeTestRule.onNodeWithText(label).assertExists() + composeTestRule.onNodeWithText(label.uppercase()).assertDoesNotExist() + composeTestRule.onNodeWithText(label.lowercase()).assertDoesNotExist() + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63887820..e89e1f86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,6 +53,8 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "playUpdate" } androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3" }