Accessibility

This commit is contained in:
2026-09-06 02:55:11 +02:00
parent e59680e710
commit 7904aabbc3
22 changed files with 73 additions and 33 deletions
+22
View File
@@ -17,6 +17,8 @@ android {
targetSdk = 37 targetSdk = 37
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4" versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
signingConfigs { signingConfigs {
@@ -74,6 +76,18 @@ android {
testOptions { testOptions {
unitTests { unitTests {
includeAndroidResources = true 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 'org.robolectric:robolectric:4.16.1'
testImplementation 'androidx.test:core:1.7.0' testImplementation 'androidx.test:core:1.7.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.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) { tasks.register('jacocoTestReport', JacocoReport) {
@@ -48,7 +48,7 @@ fun AboutScreen(
title = { Text(stringResource(R.string.title_about)) }, title = { Text(stringResource(R.string.title_about)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
backgroundColor = MaterialTheme.colors.primary, backgroundColor = MaterialTheme.colors.primary,
@@ -48,7 +48,7 @@ import java.util.Locale
class AccountActivity : AppCompatActivity() { class AccountActivity : AppCompatActivity() {
private val viewModel: AccountViewModel by viewModels() internal val viewModel: AccountViewModel by viewModels()
companion object { companion object {
private val TAG = AccountActivity::class.java.simpleName private val TAG = AccountActivity::class.java.simpleName
@@ -34,7 +34,7 @@ import java.util.Calendar
class EditBillActivity : AppCompatActivity() { class EditBillActivity : AppCompatActivity() {
private val viewModel: EditBillViewModel by viewModels() internal val viewModel: EditBillViewModel by viewModels()
private lateinit var db: CowspentSQLiteOpenHelper private lateinit var db: CowspentSQLiteOpenHelper
private lateinit var bill: DBBill private lateinit var bill: DBBill
private var projectType: ProjectType = ProjectType.LOCAL private var projectType: ProjectType = ProjectType.LOCAL
@@ -17,7 +17,7 @@ import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.model.ProjectType
class LabelBillsActivity : AppCompatActivity() { class LabelBillsActivity : AppCompatActivity() {
private val viewModel: LabelBillsViewModel by viewModels() internal val viewModel: LabelBillsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -223,7 +224,10 @@ fun BillSummaryCard(bill: DBBill, members: List<DBMember>, remainingCount: Int)
fun CategoryButton(icon: String, name: String, onClick: () -> Unit) { fun CategoryButton(icon: String, name: String, onClick: () -> Unit) {
OutlinedButton( OutlinedButton(
onClick = onClick, onClick = onClick,
modifier = Modifier.fillMaxWidth().height(60.dp), modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.testTag("CategoryButton_$name"),
contentPadding = PaddingValues(2.dp) contentPadding = PaddingValues(2.dp)
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
@@ -23,7 +23,7 @@ import net.helcel.cowspent.util.ICallback
class ManageCurrenciesActivity : AppCompatActivity() { class ManageCurrenciesActivity : AppCompatActivity() {
private val viewModel: ManageCurrenciesViewModel by viewModels() internal val viewModel: ManageCurrenciesViewModel by viewModels()
private var db: CowspentSQLiteOpenHelper? = null private var db: CowspentSQLiteOpenHelper? = null
private var selectedProjectID: Long = -1 private var selectedProjectID: Long = -1
@@ -41,6 +41,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -216,7 +218,7 @@ fun ManageCurrenciesScreen(
) { ) {
Icon( Icon(
imageVector = if (isEditing) Icons.Default.Done else Icons.Default.Add, imageVector = if (isEditing) Icons.Default.Done else Icons.Default.Add,
contentDescription = null, contentDescription = if (isEditing) "Done" else "Add",
tint = if (viewModel.isAddEnabled()) { tint = if (viewModel.isAddEnabled()) {
if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
} else MaterialTheme.colors.onSurface.copy(alpha = 0.2f) } else MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
@@ -274,7 +276,9 @@ fun CurrencyRow(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
elevation = if (isEditing) 4.dp else 1.dp, elevation = if (isEditing) 4.dp else 1.dp,
border = if (isEditing) BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.5f)) else null, 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( Row(
modifier = Modifier modifier = Modifier
@@ -320,7 +324,7 @@ fun CurrencyRow(
IconButton(onClick = onDelete) { IconButton(onClick = onDelete) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = null, contentDescription = "Delete",
tint = MaterialTheme.colors.error.copy(alpha = 0.7f), tint = MaterialTheme.colors.error.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@@ -10,7 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
class LabelManagementActivity : AppCompatActivity() { class LabelManagementActivity : AppCompatActivity() {
private val viewModel: LabelManagementViewModel by viewModels() internal val viewModel: LabelManagementViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@@ -107,7 +107,7 @@ fun LabelManagementScreenContent(
showEditDialog = true showEditDialog = true
} }
}) { }) {
Icon(Icons.Default.Add, contentDescription = null) Icon(Icons.Default.Add, contentDescription = "Add Label")
} }
} }
) { padding -> ) { padding ->
@@ -293,7 +293,7 @@ fun BillsListScreen(
isSearchExpanded = false isSearchExpanded = false
viewModel.searchQuery = "" 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 isSearchExpanded = false
viewModel.searchQuery = "" viewModel.searchQuery = ""
}) { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_close_search))
} }
} else { } else {
IconButton(onClick = { IconButton(onClick = {
scope.launch { scaffoldState.drawerState.open() } 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 }) { IconButton(onClick = { isSearchExpanded = true }) {
Icon(Icons.Default.Search, contentDescription = null) Icon(
Icons.Default.Search,
contentDescription = stringResource(R.string.action_search)
)
} }
} }
}, },
@@ -63,7 +63,7 @@ class BillsListViewActivity :
AppCompatActivity(), AppCompatActivity(),
IRefreshBillsListCallback { IRefreshBillsListCallback {
private val viewModel: BillsListViewModel by viewModels() internal val viewModel: BillsListViewModel by viewModels()
companion object { companion object {
var DEBUG = false var DEBUG = false
@@ -31,7 +31,7 @@ import net.helcel.cowspent.android.project.ProjectImportHelper
class NewProjectActivity : AppCompatActivity() { class NewProjectActivity : AppCompatActivity() {
private val viewModel: NewProjectViewModel by viewModels() internal val viewModel: NewProjectViewModel by viewModels()
private lateinit var db: CowspentSQLiteOpenHelper private lateinit var db: CowspentSQLiteOpenHelper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -23,7 +23,7 @@ import net.helcel.cowspent.android.main.MainConstants
class EditProjectActivity : AppCompatActivity() { class EditProjectActivity : AppCompatActivity() {
private val viewModel: EditProjectViewModel by viewModels() internal val viewModel: EditProjectViewModel by viewModels()
private lateinit var db: CowspentSQLiteOpenHelper private lateinit var db: CowspentSQLiteOpenHelper
private lateinit var project: DBProject private lateinit var project: DBProject
@@ -12,6 +12,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -100,6 +101,7 @@ fun MemberEditDialogContent(
.fillMaxWidth() .fillMaxWidth()
.clickable { isActivated = !isActivated } .clickable { isActivated = !isActivated }
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.testTag("member_activated_row")
) { ) {
Icon( Icon(
imageVector = Icons.Default.Block, imageVector = Icons.Default.Block,
@@ -22,7 +22,7 @@ import net.helcel.cowspent.theme.ThemeUtils
class MemberManagementActivity : AppCompatActivity() { class MemberManagementActivity : AppCompatActivity() {
private val viewModel: MemberManagementViewModel by viewModels() internal val viewModel: MemberManagementViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@@ -134,7 +134,7 @@ fun SettingsScreen(
title = { Text(stringResource(R.string.action_settings)) }, title = { Text(stringResource(R.string.action_settings)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
backgroundColor = MaterialTheme.colors.primary, backgroundColor = MaterialTheme.colors.primary,
@@ -401,13 +401,7 @@ fun SettingsListPreference(
), ),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(selected = (key == value), onClick = null)
selected = (key == value),
onClick = {
onValueChange(key)
showDialog = false
}
)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text(text = label) Text(text = label)
} }
@@ -47,7 +47,7 @@ class ProjectStatisticsActivity : AppCompatActivity() {
} }
companion object { 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 { fun createIntent(context: Context, projectId: Long): Intent {
return Intent(context, ProjectStatisticsActivity::class.java).apply { return Intent(context, ProjectStatisticsActivity::class.java).apply {
@@ -86,7 +86,7 @@ class NextcloudClient(
} }
@Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
private fun requestServerWithSSO( internal fun requestServerWithSSO(
nextcloudAPI: NextcloudAPI, nextcloudAPI: NextcloudAPI,
target: String, target: String,
method: String, method: String,
@@ -141,7 +141,7 @@ class NextcloudClient(
} }
@Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
private fun imageRequestServerWithSSO( internal fun imageRequestServerWithSSO(
nextcloudAPI: NextcloudAPI, nextcloudAPI: NextcloudAPI,
target: String, target: String,
method: String, method: String,
@@ -182,7 +182,7 @@ class NextcloudClient(
} }
@Throws(IOException::class, NextcloudHttpRequestFailedException::class) @Throws(IOException::class, NextcloudHttpRequestFailedException::class)
private fun requestServer( internal fun requestServer(
target: String, target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): VersatileProjectSyncClient.ResponseData {
@@ -248,7 +248,7 @@ class NextcloudClient(
} }
@Throws(IOException::class, NextcloudHttpRequestFailedException::class) @Throws(IOException::class, NextcloudHttpRequestFailedException::class)
private fun imageRequestServer( internal fun imageRequestServer(
target: String, target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): VersatileProjectSyncClient.ResponseData {
@@ -158,10 +158,18 @@ class VersatileProjectSyncClient(
paramKeys.add("password") paramKeys.add("password")
paramValues.add(newPassword) paramValues.add(newPassword)
} }
if (newEmail != null) {
paramKeys.add("contact_email")
paramValues.add(newEmail)
}
if (newMainCurrencyName != null) { if (newMainCurrencyName != null) {
paramKeys.add("currencyName") paramKeys.add("currencyName")
paramValues.add(newMainCurrencyName) paramValues.add(newMainCurrencyName)
} }
if (newArchivedTs != null) {
paramKeys.add("archivedTs")
paramValues.add(newArchivedTs.toString())
}
} }
if (canAccessProjectWithNCLogin(project)) { if (canAccessProjectWithNCLogin(project)) {
username = this.username username = this.username
+3
View File
@@ -10,6 +10,9 @@
<string name="action_edit">Edit</string> <string name="action_edit">Edit</string>
<string name="action_share">Share</string> <string name="action_share">Share</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_open_menu">Open menu</string>
<string name="action_close_search">Close search</string>
<string name="action_clear_search">Clear search</string>
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="action_archive">Archive</string> <string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string> <string name="action_unarchive">Unarchive</string>
+1 -1
View File
@@ -19,7 +19,7 @@ org.gradle.dependency.verification.console=verbose
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # 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.useAndroidX=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
org.gradle.warning.mode=all org.gradle.warning.mode=all