From 7904aabbc3e71591ee473d57b94ed3e27e1ac6ea Mon Sep 17 00:00:00 2001 From: soraefir Date: Sun, 6 Sep 2026 02:55:11 +0200 Subject: [PATCH] Accessibility --- app/build.gradle | 22 +++++++++++++++++++ .../cowspent/android/about/AboutScreen.kt | 2 +- .../android/account/AccountActivity.kt | 2 +- .../android/bill_edit/EditBillActivity.kt | 2 +- .../android/bill_label/LabelBillsActivity.kt | 2 +- .../android/bill_label/LabelBillsScreen.kt | 6 ++++- .../currencies/ManageCurrenciesActivity.kt | 2 +- .../currencies/ManageCurrenciesScreen.kt | 10 ++++++--- .../android/label/LabelManagementActivity.kt | 2 +- .../android/label/LabelManagementScreen.kt | 2 +- .../cowspent/android/main/BillsListScreen.kt | 11 ++++++---- .../android/main/BillsListViewActivity.kt | 2 +- .../project/create/NewProjectActivity.kt | 2 +- .../project/edit/EditProjectActivity.kt | 2 +- .../project/member/MemberEditDialog.kt | 2 ++ .../member/MemberManagementActivity.kt | 2 +- .../android/settings/SettingsScreen.kt | 10 ++------- .../statistics/ProjectStatisticsActivity.kt | 2 +- .../helcel/cowspent/util/NextcloudClient.kt | 8 +++---- .../util/VersatileProjectSyncClient.kt | 8 +++++++ app/src/main/res/values/strings.xml | 3 +++ gradle.properties | 2 +- 22 files changed, 73 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4b80502..3af1e98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,8 @@ android { targetSdk = 37 versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4" versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { @@ -74,6 +76,18 @@ android { testOptions { unitTests { includeAndroidResources = true + all { + maxParallelForks = 2 + forkEvery = 50 + maxHeapSize = "1024m" + // Robolectric loads classes through its own sandbox classloader, which leaves them + // without a code-source location. Without this JaCoCo skips them entirely and the + // report only sees the handful of plain JVM classes. + jacoco { + includeNoLocationClasses = true + excludes = ["jdk.internal.*"] + } + } } } @@ -136,6 +150,14 @@ dependencies { testImplementation 'org.robolectric:robolectric:4.16.1' testImplementation 'androidx.test:core:1.7.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0' + testImplementation 'androidx.compose.ui:ui-test-junit4' + + androidTestImplementation platform('androidx.compose:compose-bom:2026.08.00') + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + androidTestImplementation 'io.mockk:mockk-android:1.14.11' + debugImplementation 'androidx.compose.ui:ui-test-manifest' } tasks.register('jacocoTestReport', JacocoReport) { diff --git a/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt b/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt index aa34814..bcd70ff 100644 --- a/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt @@ -48,7 +48,7 @@ fun AboutScreen( title = { Text(stringResource(R.string.title_about)) }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } }, backgroundColor = MaterialTheme.colors.primary, diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt index e02bcbc..6fdd291 100644 --- a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt @@ -48,7 +48,7 @@ import java.util.Locale class AccountActivity : AppCompatActivity() { - private val viewModel: AccountViewModel by viewModels() + internal val viewModel: AccountViewModel by viewModels() companion object { private val TAG = AccountActivity::class.java.simpleName diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt index 843b225..cb9a8f6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -34,7 +34,7 @@ import java.util.Calendar class EditBillActivity : AppCompatActivity() { - private val viewModel: EditBillViewModel by viewModels() + internal val viewModel: EditBillViewModel by viewModels() private lateinit var db: CowspentSQLiteOpenHelper private lateinit var bill: DBBill private var projectType: ProjectType = ProjectType.LOCAL diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt index 681fd8e..b366094 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt @@ -17,7 +17,7 @@ import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.ProjectType class LabelBillsActivity : AppCompatActivity() { - private val viewModel: LabelBillsViewModel by viewModels() + internal val viewModel: LabelBillsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt index 4e1889b..da5d970 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -223,7 +224,10 @@ fun BillSummaryCard(bill: DBBill, members: List, remainingCount: Int) fun CategoryButton(icon: String, name: String, onClick: () -> Unit) { OutlinedButton( onClick = onClick, - modifier = Modifier.fillMaxWidth().height(60.dp), + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .testTag("CategoryButton_$name"), contentPadding = PaddingValues(2.dp) ) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt index ad30126..0602d75 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt @@ -23,7 +23,7 @@ import net.helcel.cowspent.util.ICallback class ManageCurrenciesActivity : AppCompatActivity() { - private val viewModel: ManageCurrenciesViewModel by viewModels() + internal val viewModel: ManageCurrenciesViewModel by viewModels() private var db: CowspentSQLiteOpenHelper? = null private var selectedProjectID: Long = -1 diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt index 9124992..979789f 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt @@ -41,6 +41,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -216,7 +218,7 @@ fun ManageCurrenciesScreen( ) { Icon( imageVector = if (isEditing) Icons.Default.Done else Icons.Default.Add, - contentDescription = null, + contentDescription = if (isEditing) "Done" else "Add", tint = if (viewModel.isAddEnabled()) { if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary } else MaterialTheme.colors.onSurface.copy(alpha = 0.2f) @@ -274,7 +276,9 @@ fun CurrencyRow( shape = RoundedCornerShape(12.dp), elevation = if (isEditing) 4.dp else 1.dp, border = if (isEditing) BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.5f)) else null, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag("CurrencyRow_${currency.name}") ) { Row( modifier = Modifier @@ -320,7 +324,7 @@ fun CurrencyRow( IconButton(onClick = onDelete) { Icon( imageVector = Icons.Default.Delete, - contentDescription = null, + contentDescription = "Delete", tint = MaterialTheme.colors.error.copy(alpha = 0.7f), modifier = Modifier.size(20.dp) ) diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt index d6bfad0..1abf8e0 100644 --- a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt @@ -10,7 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import net.helcel.cowspent.theme.ThemeUtils class LabelManagementActivity : AppCompatActivity() { - private val viewModel: LabelManagementViewModel by viewModels() + internal val viewModel: LabelManagementViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt index dcfedf8..3fbb5f1 100644 --- a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt @@ -107,7 +107,7 @@ fun LabelManagementScreenContent( showEditDialog = true } }) { - Icon(Icons.Default.Add, contentDescription = null) + Icon(Icons.Default.Add, contentDescription = "Add Label") } } ) { padding -> diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt index f5e8ebb..51a5261 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt @@ -293,7 +293,7 @@ fun BillsListScreen( isSearchExpanded = false viewModel.searchQuery = "" }) { - Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.onPrimary) + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear_search), tint = MaterialTheme.colors.onPrimary) } } ) @@ -322,13 +322,13 @@ fun BillsListScreen( isSearchExpanded = false viewModel.searchQuery = "" }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_close_search)) } } else { IconButton(onClick = { scope.launch { scaffoldState.drawerState.open() } }) { - Icon(Icons.Default.Menu, contentDescription = null) + Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.action_open_menu)) } } }, @@ -343,7 +343,10 @@ fun BillsListScreen( } } IconButton(onClick = { isSearchExpanded = true }) { - Icon(Icons.Default.Search, contentDescription = null) + Icon( + Icons.Default.Search, + contentDescription = stringResource(R.string.action_search) + ) } } }, diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt index 6b8a4b8..c9b4f97 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -63,7 +63,7 @@ class BillsListViewActivity : AppCompatActivity(), IRefreshBillsListCallback { - private val viewModel: BillsListViewModel by viewModels() + internal val viewModel: BillsListViewModel by viewModels() companion object { var DEBUG = false diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt index d611f14..0e054b6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt @@ -31,7 +31,7 @@ import net.helcel.cowspent.android.project.ProjectImportHelper class NewProjectActivity : AppCompatActivity() { - private val viewModel: NewProjectViewModel by viewModels() + internal val viewModel: NewProjectViewModel by viewModels() private lateinit var db: CowspentSQLiteOpenHelper override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt index 08422be..1839fa2 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt @@ -23,7 +23,7 @@ import net.helcel.cowspent.android.main.MainConstants class EditProjectActivity : AppCompatActivity() { - private val viewModel: EditProjectViewModel by viewModels() + internal val viewModel: EditProjectViewModel by viewModels() private lateinit var db: CowspentSQLiteOpenHelper private lateinit var project: DBProject diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt index 642a841..f4ff871 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization @@ -100,6 +101,7 @@ fun MemberEditDialogContent( .fillMaxWidth() .clickable { isActivated = !isActivated } .padding(vertical = 8.dp) + .testTag("member_activated_row") ) { Icon( imageVector = Icons.Default.Block, diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt index 5dba687..6653d65 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt @@ -22,7 +22,7 @@ import net.helcel.cowspent.theme.ThemeUtils class MemberManagementActivity : AppCompatActivity() { - private val viewModel: MemberManagementViewModel by viewModels() + internal val viewModel: MemberManagementViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt index a8f2a57..df21f9b 100644 --- a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt @@ -134,7 +134,7 @@ fun SettingsScreen( title = { Text(stringResource(R.string.action_settings)) }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } }, backgroundColor = MaterialTheme.colors.primary, @@ -401,13 +401,7 @@ fun SettingsListPreference( ), verticalAlignment = Alignment.CenterVertically ) { - RadioButton( - selected = (key == value), - onClick = { - onValueChange(key) - showDialog = false - } - ) + RadioButton(selected = (key == value), onClick = null) Spacer(Modifier.width(8.dp)) Text(text = label) } diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt index 5677935..eaf090c 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt @@ -47,7 +47,7 @@ class ProjectStatisticsActivity : AppCompatActivity() { } companion object { - private const val EXTRA_PROJECT_ID = "extra_project_id" + internal const val EXTRA_PROJECT_ID = "extra_project_id" fun createIntent(context: Context, projectId: Long): Intent { return Intent(context, ProjectStatisticsActivity::class.java).apply { diff --git a/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt b/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt index 5d0e244..59e0f4d 100644 --- a/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt +++ b/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt @@ -86,7 +86,7 @@ class NextcloudClient( } @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) - private fun requestServerWithSSO( + internal fun requestServerWithSSO( nextcloudAPI: NextcloudAPI, target: String, method: String, @@ -141,7 +141,7 @@ class NextcloudClient( } @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) - private fun imageRequestServerWithSSO( + internal fun imageRequestServerWithSSO( nextcloudAPI: NextcloudAPI, target: String, method: String, @@ -182,7 +182,7 @@ class NextcloudClient( } @Throws(IOException::class, NextcloudHttpRequestFailedException::class) - private fun requestServer( + internal fun requestServer( target: String, method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean ): VersatileProjectSyncClient.ResponseData { @@ -248,7 +248,7 @@ class NextcloudClient( } @Throws(IOException::class, NextcloudHttpRequestFailedException::class) - private fun imageRequestServer( + internal fun imageRequestServer( target: String, method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean ): VersatileProjectSyncClient.ResponseData { diff --git a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt index 829c801..20004df 100644 --- a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt +++ b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt @@ -158,10 +158,18 @@ class VersatileProjectSyncClient( paramKeys.add("password") paramValues.add(newPassword) } + if (newEmail != null) { + paramKeys.add("contact_email") + paramValues.add(newEmail) + } if (newMainCurrencyName != null) { paramKeys.add("currencyName") paramValues.add(newMainCurrencyName) } + if (newArchivedTs != null) { + paramKeys.add("archivedTs") + paramValues.add(newArchivedTs.toString()) + } } if (canAccessProjectWithNCLogin(project)) { username = this.username diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95bafae..8019552 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,9 @@ Edit Share Search + Open menu + Close search + Clear search Delete Archive Unarchive diff --git a/gradle.properties b/gradle.properties index 6fe660e..4bc4c94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ org.gradle.dependency.verification.console=verbose # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +org.gradle.parallel=true android.useAndroidX=true android.nonTransitiveRClass=false org.gradle.warning.mode=all