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
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) {
@@ -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,
@@ -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
@@ -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
@@ -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()
@@ -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<DBMember>, 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) {
@@ -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
@@ -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)
)
@@ -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()
@@ -107,7 +107,7 @@ fun LabelManagementScreenContent(
showEditDialog = true
}
}) {
Icon(Icons.Default.Add, contentDescription = null)
Icon(Icons.Default.Add, contentDescription = "Add Label")
}
}
) { padding ->
@@ -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)
)
}
}
},
@@ -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
@@ -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?) {
@@ -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
@@ -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,
@@ -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()
@@ -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)
}
@@ -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 {
@@ -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 {
@@ -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
+3
View File
@@ -10,6 +10,9 @@
<string name="action_edit">Edit</string>
<string name="action_share">Share</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_archive">Archive</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.
# 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