From 2c1de5cf373ee297667d42b29409df913d38551d Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Fri, 23 Oct 2026 15:04:57 +0200 Subject: [PATCH] test: add accessibility tests for RecordsScreen and PlayerScreen --- feature/records/build.gradle.kts | 20 ++++ .../ui/PlayerScreenAccessibilityTest.kt | 100 ++++++++++++++++++ .../ui/RecordsScreenAccessibilityTest.kt | 67 ++++++++++++ gradle/libs.versions.toml | 8 ++ 4 files changed, 195 insertions(+) create mode 100644 feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreenAccessibilityTest.kt create mode 100644 feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/records/ui/RecordsScreenAccessibilityTest.kt diff --git a/feature/records/build.gradle.kts b/feature/records/build.gradle.kts index 5e74eb2..a2f36ff 100644 --- a/feature/records/build.gradle.kts +++ b/feature/records/build.gradle.kts @@ -6,6 +6,16 @@ plugins { kotlin { androidLibrary { namespace = "bzh.ajaury.chombev.records" + // Packages the Compose resources (strings of the screens) into the device test APK. + androidResources { + enable = true + } + + // Accessibility tests: they run the Accessibility Test Framework checks, which need an + // Android device or emulator on API 34+ (Robolectric does not support them). + withDeviceTestBuilder {}.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } } sourceSets { @@ -45,6 +55,16 @@ kotlin { implementation(libs.koin.core.viewmodel) implementation(libs.koin.compose.viewmodel) } + getByName("androidDeviceTest").dependencies { + implementation(libs.kotlin.test) + implementation(libs.androidx.testRunner) + // Espresso 3.5, which Compose UI tests depend on, breaks on API 37 (InputManager.getInstance). + implementation(libs.androidx.espressoCore) + implementation(libs.compose.uiTest) + implementation(libs.compose.uiTestAccessibility) + // Declares the activity the UI tests host their content in. + implementation(libs.compose.uiTestManifest) + } } } diff --git a/feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreenAccessibilityTest.kt b/feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreenAccessibilityTest.kt new file mode 100644 index 0000000..0e18bed --- /dev/null +++ b/feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreenAccessibilityTest.kt @@ -0,0 +1,100 @@ +package bzh.ajaury.chombev.player.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.accessibility.enableAccessibilityChecks +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.test.filters.SdkSuppress +import bzh.ajaury.chombev.core.model.Phrase +import bzh.ajaury.chombev.core.model.Record +import bzh.ajaury.chombev.player.model.PlaybackState +import bzh.ajaury.chombev.player.model.PlaybackTiming +import bzh.ajaury.chombev.player.model.PlayerState +import bzh.ajaury.chombev.player.ui.viewmodel.PlayerUiState +import bzh.ajaury.chombev.subtitle.model.SubtitleLine +import kotlin.test.Test +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Runs the Accessibility Test Framework checks (labels, touch targets, contrast, traversal order...) + * on the player, in each state that changes its controls. + */ +@OptIn(ExperimentalTestApi::class) +@SdkSuppress(minSdkVersion = 34) +class PlayerScreenAccessibilityTest { + + @Test + fun playingPlayerPassesAccessibilityChecks() = + assertPassesAccessibilityChecks(uiState(playbackState = PlaybackState.PLAYING)) + + @Test + fun pausedPlayerPassesAccessibilityChecks() = + assertPassesAccessibilityChecks(uiState(playbackState = PlaybackState.PAUSED)) + + @Test + fun endedPlayerPassesAccessibilityChecks() = + assertPassesAccessibilityChecks(uiState(playbackState = PlaybackState.ENDED)) + + @Test + fun playerOnFirstSentencePassesAccessibilityChecks() = + assertPassesAccessibilityChecks(uiState(position = 0.seconds, currentSubtitleIndex = 0)) + + @Test + fun playerOnLastSentencePassesAccessibilityChecks() = assertPassesAccessibilityChecks( + uiState(position = 8.seconds, currentSubtitleIndex = subtitleLines.lastIndex), + ) + + private fun assertPassesAccessibilityChecks(uiState: PlayerUiState) = runComposeUiTest { + enableAccessibilityChecks() + setContent { + MaterialTheme { + PlayerScreen(uiState = uiState) + } + } + + onRoot().tryPerformAccessibilityChecks() + } + + private companion object { + val record = Record( + id = 1, + index = 1, + emoji = "🙏", + title = Phrase(transcription = "An onestiz", translation = "La politesse"), + audioResourcePath = "", + subtitleResourcePath = "", + ) + + val subtitleLines = listOf( + SubtitleLine( + startTime = 0.seconds, + phrase = Phrase(transcription = "Demat deoc'h !", translation = "Bonjour !"), + ), + SubtitleLine( + startTime = 4.seconds, + phrase = Phrase(transcription = "Kenavo !", translation = "Au revoir !"), + ), + SubtitleLine( + startTime = 7.seconds, + phrase = Phrase(transcription = "Kenavo emberr !", translation = "À ce soir !"), + ), + ) + + fun uiState( + playbackState: PlaybackState = PlaybackState.PLAYING, + position: Duration = 5.seconds, + currentSubtitleIndex: Int = 1, + ) = PlayerUiState( + record = record, + playerState = PlayerState( + playbackState = playbackState, + playbackTiming = PlaybackTiming(position = position, duration = 10.seconds), + subtitleLines = subtitleLines, + currentSubtitleIndex = currentSubtitleIndex, + ), + ) + } +} diff --git a/feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/records/ui/RecordsScreenAccessibilityTest.kt b/feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/records/ui/RecordsScreenAccessibilityTest.kt new file mode 100644 index 0000000..ce7537f --- /dev/null +++ b/feature/records/src/androidDeviceTest/kotlin/bzh/ajaury/chombev/records/ui/RecordsScreenAccessibilityTest.kt @@ -0,0 +1,67 @@ +package bzh.ajaury.chombev.records.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.accessibility.enableAccessibilityChecks +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.test.filters.SdkSuppress +import bzh.ajaury.chombev.core.model.Phrase +import bzh.ajaury.chombev.core.model.Record +import kotlin.test.Test + +/** + * Runs the Accessibility Test Framework checks (labels, touch targets, contrast, traversal order...) + * on the list of records. + */ +@OptIn(ExperimentalTestApi::class) +@SdkSuppress(minSdkVersion = 34) +class RecordsScreenAccessibilityTest { + + @Test + fun recordsScreenPassesAccessibilityChecks() = runComposeUiTest { + enableAccessibilityChecks() + setContent { + MaterialTheme { + RecordsScreen( + records = records, + contentPadding = PaddingValues(), + onRecordClick = {}, + ) + } + } + + onRoot().tryPerformAccessibilityChecks() + } + + private companion object { + val records = listOf( + Record( + id = 1, + index = 1, + emoji = "🙏", + title = Phrase(transcription = "An onestiz", translation = "La politesse"), + audioResourcePath = "", + subtitleResourcePath = "", + ), + Record( + id = 2, + index = 2, + emoji = "👋", + title = Phrase(transcription = "En em brezantiñ", translation = "Se présenter"), + audioResourcePath = "", + subtitleResourcePath = "", + ), + Record( + id = 3, + index = 3, + emoji = "🌦️", + title = Phrase(transcription = "An amzer", translation = null), + audioResourcePath = "", + subtitleResourcePath = "", + ), + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 742a941..59b481a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,10 @@ androidCompileSdk = "37" # https://developer.android.com/tools/releas androidMinSdk = "24" # https://developer.android.com/tools/releases/platforms androidTargetSdk = "37" # https://developer.android.com/tools/releases/platforms androidxActivity = "1.13.0" # https://developer.android.com/jetpack/androidx/releases/activity +androidxComposeUi = "1.12.1" # https://developer.android.com/jetpack/androidx/releases/compose-ui +androidxEspresso = "3.7.0" # https://developer.android.com/jetpack/androidx/releases/test androidxLifecycle = "2.11.0" # https://developer.android.com/jetpack/androidx/releases/lifecycle +androidxTestRunner = "1.7.0" # https://developer.android.com/jetpack/androidx/releases/test androidxWebkit = "1.17.1" # https://developer.android.com/jetpack/androidx/releases/webkit composeIconsExtended = "1.7.3" # https://github.com/JetBrains/compose-multiplatform/releases composeMultiplatform = "1.12.1" # https://github.com/JetBrains/compose-multiplatform/releases @@ -34,6 +37,8 @@ composeCompiler-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" } +androidx-espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxEspresso" } +androidx-testRunner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidxWebkit" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } @@ -42,6 +47,9 @@ compose-material3 = { module = "org.jetbrains.compose.material3:material3", vers compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeIconsExtended" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +compose-uiTest = { module = "org.jetbrains.compose.ui:ui-test", version.ref = "composeMultiplatform" } +compose-uiTestAccessibility = { module = "androidx.compose.ui:ui-test-accessibility", version.ref = "androidxComposeUi" } +compose-uiTestManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidxComposeUi" } compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } jaad = { module = "com.tianscar.javasound:jaad", version.ref = "jaad" }