commit 25143143789adcf099983cc09562f945383b6b71
parent 0648f78d41aa83ec2c4ef1696210d0db655e5372
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Tue, 29 Jul 2025 20:33:19 +0200
Accessibility fixes and tests
Diffstat:
8 files changed, 179 insertions(+), 74 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
@@ -0,0 +1,33 @@
+name: Test Build
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ - name: Build and run unit tests
+ run: ./gradlew test
+ - name: Build and run instrumented tests (UI & Accessibility)
+ run: ./gradlew connectedAndroidTest
+ env:
+ # Set up a virtual display for instrumented tests
+ DISPLAY: ':99.0'
+ - name: Upload test reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-reports
+ path: |
+ app/build/reports/
+ app/build/outputs/androidTest-results/
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
@@ -84,6 +84,8 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
+ androidTestImplementation(libs.androidx.compose.ui.accessibility.test)
+ androidTestImplementation(libs.androidx.test.rules)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/space/midnightthoughts/nordiccalendar/AccessibilityTest.kt b/app/src/androidTest/java/space/midnightthoughts/nordiccalendar/AccessibilityTest.kt
@@ -0,0 +1,42 @@
+package space.midnightthoughts.nordiccalendar
+
+import android.Manifest
+import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.tryPerformAccessibilityChecks
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.GrantPermissionRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AccessibilityTest {
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule<MainActivity>()
+
+ @get:Rule
+ var mRuntimePermissionRule: GrantPermissionRule? =
+ GrantPermissionRule.grant(
+ Manifest.permission.READ_CALENDAR,
+ Manifest.permission.WRITE_CALENDAR,
+ Manifest.permission.INTERNET,
+ )
+
+ @Test
+ fun calendarScreen_accessibilityCheck() {
+ composeTestRule.enableAccessibilityChecks()
+
+ // Navigate to the day screen
+ composeTestRule.onNodeWithTag("tab_2")
+ .performClick()
+
+
+ // Any action (such as performClick) will perform accessibility checks too:
+ composeTestRule.onRoot().tryPerformAccessibilityChecks()
+ }
+}
+
diff --git a/app/src/androidTest/java/space/midnightthoughts/nordiccalendar/ExampleInstrumentedTest.kt b/app/src/androidTest/java/space/midnightthoughts/nordiccalendar/ExampleInstrumentedTest.kt
@@ -1,24 +0,0 @@
-package space.midnightthoughts.nordiccalendar
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("space.midnightthoughts.nordiccalendar", appContext.packageName)
- }
-}
-\ No newline at end of file
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt
@@ -51,6 +51,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import space.midnightthoughts.nordiccalendar.components.AppScaffold
@@ -73,7 +75,7 @@ sealed class Destinations(val route: String) {
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
- @OptIn(ExperimentalMaterial3Api::class)
+ @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val currentVersion = try {
@@ -83,50 +85,66 @@ class MainActivity : ComponentActivity() {
}
val showOnboarding = OnboardingPrefs.isOnboardingNeeded(this, currentVersion)
+
setContent {
NordicCalendarTheme {
val navController = rememberNavController()
- NavHost(
- navController = navController,
- startDestination = if (showOnboarding) Destinations.Intro.route else Destinations.Calendar.route
- ) {
- composable(Destinations.Intro.route) {
- IntroScreen(navController) { // Callback nach Abschluss
- OnboardingPrefs.setOnboardingDone(this@MainActivity, currentVersion)
- navController.navigate(Destinations.Calendar.route) {
- popUpTo(Destinations.Intro.route) { inclusive = true }
- }
- }
+
+ // TODO: Recheck calendar permissions and show dialog if not granted
+ val permissionsState = rememberMultiplePermissionsState(
+ listOf(
+ android.Manifest.permission.READ_CALENDAR,
+ android.Manifest.permission.WRITE_CALENDAR
+ ),
+ )
+
+ if (!permissionsState.allPermissionsGranted) {
+ LaunchedEffect(Unit) {
+ permissionsState.launchMultiplePermissionRequest()
}
- composable(
- route = Destinations.Calendar.route,
- arguments = listOf(
- navArgument("tab") {
- type = NavType.IntType
- defaultValue = 0
+ } else {
+ NavHost(
+ navController = navController,
+ startDestination = if (showOnboarding) Destinations.Intro.route else Destinations.Calendar.route
+ ) {
+ composable(Destinations.Intro.route) {
+ IntroScreen(navController) { // Callback nach Abschluss
+ OnboardingPrefs.setOnboardingDone(this@MainActivity, currentVersion)
+ navController.navigate(Destinations.Calendar.route) {
+ popUpTo(Destinations.Intro.route) { inclusive = true }
+ }
}
- )
- ) { backStackEntry ->
- CalendarView(
- backStackEntry = backStackEntry,
- navController = navController
- )
- }
- composable(Destinations.EventDetails.route) { backStackEntry ->
- EventDetailsView(
- backStackEntry = backStackEntry,
- navController = navController
- )
- }
- composable(Destinations.Settings.route) { backStackEntry ->
- SettingsView(
- navController = navController
- )
- }
- composable(Destinations.About.route) { backStackEntry ->
- AboutView(
- navController = navController
- )
+ }
+ composable(
+ route = Destinations.Calendar.route,
+ arguments = listOf(
+ navArgument("tab") {
+ type = NavType.IntType
+ defaultValue = 0
+ }
+ )
+ ) { backStackEntry ->
+ CalendarView(
+ backStackEntry = backStackEntry,
+ navController = navController
+ )
+ }
+ composable(Destinations.EventDetails.route) { backStackEntry ->
+ EventDetailsView(
+ backStackEntry = backStackEntry,
+ navController = navController
+ )
+ }
+ composable(Destinations.Settings.route) { backStackEntry ->
+ SettingsView(
+ navController = navController
+ )
+ }
+ composable(Destinations.About.route) { backStackEntry ->
+ AboutView(
+ navController = navController
+ )
+ }
}
}
}
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/components/AppScaffold.kt
@@ -1,6 +1,7 @@
package space.midnightthoughts.nordiccalendar.components
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
@@ -20,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import space.midnightthoughts.nordiccalendar.R
@@ -64,17 +66,23 @@ fun AppScaffold(
},
navigationIcon = {
if (isBackButtonVisible) {
- IconButton(onClick = {
- scope.launch { drawerState.close() }
- navController.popBackStack()
- }) {
+ IconButton(
+ onClick = {
+ scope.launch { drawerState.close() }
+ navController.popBackStack()
+ },
+ modifier = Modifier.size(48.dp)
+ ) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
} else {
- IconButton(onClick = { scope.launch { drawerState.open() } }) {
+ IconButton(
+ onClick = { scope.launch { drawerState.open() } },
+ modifier = Modifier.size(48.dp)
+ ) {
Icon(
Icons.Default.Menu,
contentDescription = stringResource(R.string.menu)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -54,6 +55,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.CollectionItemInfo
@@ -349,12 +351,27 @@ fun CalendarTabBar(selectedTab: Int, onTabSelected: (Int) -> Unit) {
stringResource(R.string.tab_week),
stringResource(R.string.tab_day)
)
- TabRow(selectedTabIndex = selectedTab) {
+ TabRow(
+ selectedTabIndex = selectedTab,
+ modifier = Modifier.semantics {
+ collectionInfo = CollectionInfo(
+ rowCount = 1,
+ columnCount = tabTitles.size,
+ )
+ }
+ ) {
tabTitles.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { onTabSelected(index) },
- text = { Text(title) }
+ text = { Text(title) },
+ modifier = Modifier
+ .semantics {
+ collectionItemInfo = CollectionItemInfo(
+ 0, 0, index, 0,
+ )
+ }
+ .testTag("tab_$index")
)
}
}
@@ -433,7 +450,10 @@ fun DateRangeHeader(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
- IconButton(onClick = onPrev) {
+ IconButton(
+ onClick = onPrev,
+ modifier = Modifier.size(48.dp)
+ ) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.previous_period)
@@ -448,7 +468,10 @@ fun DateRangeHeader(
}
}
}
- IconButton(onClick = onNext) {
+ IconButton(
+ onClick = onNext,
+ modifier = Modifier.size(48.dp)
+ ) {
Icon(
Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = stringResource(R.string.next_period)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
@@ -18,6 +18,8 @@ hiltNavigationCompose = "1.2.0"
maplibreCompose = "0.10.4"
ktorVersion = "3.2.3"
preferenceVersion = "2.1.0"
+accessibilityTestVersion = "1.8.3"
+testRulesVersion = "1.6.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -48,6 +50,8 @@ ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serializatio
ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktorVersion" }
ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktorVersion" }
compose-preference = { group = "me.zhanghai.compose.preference", name = "preference", version.ref = "preferenceVersion" }
+androidx-compose-ui-accessibility-test = { group = "androidx.compose.ui", name = "ui-test-junit4-accessibility", version.ref = "accessibilityTestVersion" }
+androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "testRulesVersion" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }