commit 6c938c67c4fce17a1a7f291790d3cac5bc76f59a
parent 290aae21ed299c8d9c294270294f0a6902fa6ac6
Author: avirvara <avirvara@mozilla.com>
Date: Mon, 8 Dec 2025 11:15:43 +0000
Bug 2002588: add support for automation testing for compose logins r=android-reviewers,sfamisa,android-l10n-reviewers,flod
try link;; https://treeherder.mozilla.org/jobs?repo=try&revision=a250acdab0eb7a86d269a6b9e1104c7abf3f801f
Differential Revision: https://phabricator.services.mozilla.com/D274244
Diffstat:
6 files changed, 141 insertions(+), 22 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/AddLoginScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/AddLoginScreen.kt
@@ -28,12 +28,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
import mozilla.components.compose.base.button.IconButton
+import mozilla.components.compose.base.text.Text
import mozilla.components.compose.base.textfield.TextField
import mozilla.components.lib.state.ext.observeAsState
import mozilla.components.support.ktx.util.URLStringUtils.isHttpOrHttps
@@ -49,6 +53,9 @@ internal fun AddLoginScreen(store: LoginsStore) {
topBar = {
AddLoginTopBar(store)
},
+ modifier = Modifier.semantics {
+ testTagsAsResourceId = true
+ },
) { paddingValues ->
Column(
modifier = Modifier
@@ -140,11 +147,16 @@ private fun AddLoginHost(store: LoginsStore) {
horizontal = FirefoxTheme.layout.space.static200,
vertical = FirefoxTheme.layout.space.static100,
)
- .width(FirefoxTheme.layout.size.containerMaxWidth),
+ .width(FirefoxTheme.layout.size.containerMaxWidth)
+ .semantics {
+ testTag = LoginsTestingTags.ADD_LOGIN_HOST_NAME_TEXT_FIELD
+ },
label = stringResource(R.string.preferences_passwords_saved_logins_site),
trailingIcon = {
if (isFocused && isValidHost(host)) {
- CrossTextFieldButton { store.dispatch(AddLoginAction.HostChanged("")) }
+ CrossTextFieldButton(
+ contentDescription = Text.Resource(R.string.saved_login_clear_hostname),
+ ) { store.dispatch(AddLoginAction.HostChanged("")) }
}
},
)
@@ -182,11 +194,18 @@ private fun AddLoginUsername(store: LoginsStore) {
horizontal = FirefoxTheme.layout.space.static200,
vertical = FirefoxTheme.layout.space.static100,
)
- .width(FirefoxTheme.layout.size.containerMaxWidth),
+ .width(FirefoxTheme.layout.size.containerMaxWidth)
+ .semantics {
+ testTag = LoginsTestingTags.ADD_LOGIN_USER_NAME_TEXT_FIELD
+ },
label = stringResource(R.string.preferences_passwords_saved_logins_username),
trailingIcon = {
if (isFocused && addLoginState?.username?.isNotEmpty() == true) {
- CrossTextFieldButton { store.dispatch(AddLoginAction.UsernameChanged("")) }
+ CrossTextFieldButton(contentDescription = Text.Resource(R.string.saved_login_clear_username)) {
+ store.dispatch(
+ AddLoginAction.UsernameChanged(""),
+ )
+ }
}
},
)
@@ -212,11 +231,18 @@ private fun AddLoginPassword(store: LoginsStore) {
horizontal = FirefoxTheme.layout.space.static200,
vertical = FirefoxTheme.layout.space.static100,
)
- .width(FirefoxTheme.layout.size.containerMaxWidth),
- label = stringResource(R.string.preferences_passwords_saved_logins_password),
+ .width(FirefoxTheme.layout.size.containerMaxWidth)
+ .semantics {
+ testTag = LoginsTestingTags.ADD_LOGIN_PASSWORD_TEXT_FIELD
+ },
+ label = stringResource(R.string.saved_logins_clear_password),
trailingIcon = {
if (isFocused && state?.password?.isNotEmpty() == true) {
- CrossTextFieldButton { store.dispatch(AddLoginAction.PasswordChanged("")) }
+ CrossTextFieldButton(contentDescription = Text.Resource(R.string.saved_logins_clear_password)) {
+ store.dispatch(
+ AddLoginAction.PasswordChanged(""),
+ )
+ }
}
},
visualTransformation = PasswordVisualTransformation(),
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/EditLoginScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/EditLoginScreen.kt
@@ -20,17 +20,25 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
import mozilla.components.compose.base.button.IconButton
+import mozilla.components.compose.base.text.Text
import mozilla.components.compose.base.textfield.TextField
import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.lib.state.ext.observeAsState
@@ -51,6 +59,7 @@ internal fun EditLoginScreen(store: LoginsStore) {
loginItem = editState.login,
)
},
+ modifier = Modifier.semantics { testTagsAsResourceId = true },
) { paddingValues ->
Column(
modifier = Modifier
@@ -166,11 +175,16 @@ private fun EditLoginUsername(store: LoginsStore, user: String) {
horizontal = FirefoxTheme.layout.space.static200,
vertical = FirefoxTheme.layout.space.static100,
)
- .width(FirefoxTheme.layout.size.containerMaxWidth),
+ .width(FirefoxTheme.layout.size.containerMaxWidth)
+ .semantics {
+ testTag = LoginsTestingTags.EDIT_LOGIN_USERNAME_TEXT_FIELD
+ },
label = stringResource(R.string.preferences_passwords_saved_logins_username),
trailingIcon = {
if (editState?.newUsername?.isNotEmpty() == true) {
- CrossTextFieldButton {
+ CrossTextFieldButton(
+ contentDescription = Text.Resource(R.string.saved_login_clear_username),
+ ) {
store.dispatch(EditLoginAction.UsernameChanged(""))
}
}
@@ -184,6 +198,11 @@ private fun EditLoginPassword(store: LoginsStore, pass: String) {
val isPasswordVisible = editState?.isPasswordVisible ?: true
val password = editState?.newPassword ?: pass
+ val focusRequester = remember { FocusRequester() }
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
Row(verticalAlignment = Alignment.CenterVertically) {
TextField(
value = password,
@@ -198,10 +217,19 @@ private fun EditLoginPassword(store: LoginsStore, pass: String) {
horizontal = FirefoxTheme.layout.space.static200,
vertical = FirefoxTheme.layout.space.static100,
)
- .width(FirefoxTheme.layout.size.containerMaxWidth),
+ .width(FirefoxTheme.layout.size.containerMaxWidth)
+ .semantics {
+ testTag = LoginsTestingTags.EDIT_LOGIN_PASSWORD_TEXT_FIELD
+ }
+ .focusRequester(focusRequester),
label = stringResource(R.string.preferences_passwords_saved_logins_password),
trailingIcon = {
EyePasswordIconButton(
+ contentDescription = if (isPasswordVisible) {
+ Text.Resource(R.string.saved_login_hide_password)
+ } else {
+ Text.Resource(R.string.saved_login_reveal_password)
+ },
isPasswordVisible = isPasswordVisible,
onTrailingIconClick = {
store.dispatch(
@@ -212,7 +240,9 @@ private fun EditLoginPassword(store: LoginsStore, pass: String) {
},
)
if (editState?.newPassword?.isNotEmpty() == true) {
- CrossTextFieldButton {
+ CrossTextFieldButton(
+ contentDescription = Text.Resource(R.string.saved_logins_clear_password),
+ ) {
store.dispatch(EditLoginAction.PasswordChanged(""))
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginDetailsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginDetailsScreen.kt
@@ -33,6 +33,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
@@ -47,6 +50,7 @@ import mozilla.components.compose.base.menu.DropdownMenu
import mozilla.components.compose.base.menu.MenuItem
import mozilla.components.compose.base.snackbar.Snackbar
import mozilla.components.compose.base.snackbar.displaySnackbar
+import mozilla.components.compose.base.text.Text
import mozilla.components.compose.base.textfield.TextField
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.R
@@ -177,15 +181,11 @@ private fun LoginDetailMenu(
DropdownMenu(
menuItems = listOf(
MenuItem.TextItem(
- text = mozilla.components.compose.base.text.Text.Resource(
- R.string.login_detail_menu_edit_button,
- ),
+ text = Text.Resource(R.string.login_detail_menu_edit_button),
onClick = { store.dispatch(DetailLoginMenuAction.EditLoginMenuItemClicked(loginItem)) },
),
MenuItem.TextItem(
- text = mozilla.components.compose.base.text.Text.Resource(
- R.string.login_detail_menu_delete_button,
- ),
+ text = Text.Resource(R.string.login_detail_menu_delete_button),
onClick = {
store.dispatch(
DetailLoginMenuAction.DeleteLoginMenuItemClicked(
@@ -298,9 +298,18 @@ private fun LoginDetailsPassword(
modifier = Modifier
.padding(horizontal = FirefoxTheme.layout.space.static200)
.wrapContentHeight()
- .width(FirefoxTheme.layout.size.containerMaxWidth),
+ .width(FirefoxTheme.layout.size.containerMaxWidth)
+ .semantics {
+ testTagsAsResourceId = true
+ testTag = LoginsTestingTags.LOGIN_DETAILS_PASSWORD_TEXT_FIELD
+ },
trailingIcon = {
EyePasswordIconButton(
+ contentDescription = if (isPasswordVisible) {
+ Text.Resource(R.string.saved_login_hide_password)
+ } else {
+ Text.Resource(R.string.saved_login_reveal_password)
+ },
isPasswordVisible = isPasswordVisible,
onTrailingIconClick = { isPasswordVisible = !isPasswordVisible },
)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsTestingTags.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsTestingTags.kt
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.settings.logins.ui
+
+internal object LoginsTestingTags {
+
+ // Saved logins list
+ const val SAVED_LOGINS_LIST = "saved.logins.list"
+
+ // Saved login item
+ const val SAVED_LOGINS_LIST_ITEM = "saved.logins.list.item"
+
+ // Saved logins search
+ const val SAVED_LOGINS_PASSWORD_SEARCH_FIELD = "saved.logins.password.search.field"
+
+ // Add login host name
+ const val ADD_LOGIN_HOST_NAME_TEXT_FIELD = "logins.add.host.name.text.field"
+
+ // Add login user name
+ const val ADD_LOGIN_USER_NAME_TEXT_FIELD = "logins.add.user.name.text.field"
+
+ // Add login password
+ const val ADD_LOGIN_PASSWORD_TEXT_FIELD = "logins.add.password.text.field"
+
+ // Edit login password
+ const val EDIT_LOGIN_PASSWORD_TEXT_FIELD = "logins.edit.password.text.field"
+
+ // Edit login username
+ const val EDIT_LOGIN_USERNAME_TEXT_FIELD = "logins.edit.username.text.field"
+
+ // Login details password
+ const val LOGIN_DETAILS_PASSWORD_TEXT_FIELD = "login.details.password.text.field"
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt
@@ -40,6 +40,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.collectionInfo
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -133,6 +135,7 @@ private fun LoginsList(store: LoginsStore) {
)
},
contentWindowInsets = WindowInsets(0.dp),
+ modifier = Modifier.semantics { testTagsAsResourceId = true },
) { paddingValues ->
if (state.searchText.isNullOrEmpty() && state.loginItems.isEmpty()) {
EmptyList(dispatcher = store::dispatch, paddingValues = paddingValues)
@@ -149,6 +152,7 @@ private fun LoginsList(store: LoginsStore) {
.width(FirefoxTheme.layout.size.containerMaxWidth)
.weight(1f, false)
.semantics {
+ testTag = LoginsTestingTags.SAVED_LOGINS_LIST
collectionInfo =
CollectionInfo(rowCount = state.loginItems.size, columnCount = 1)
},
@@ -164,6 +168,9 @@ private fun LoginsList(store: LoginsStore) {
isSelected = false,
onClick = { store.dispatch(LoginClicked(item)) },
description = item.username.trimmed(),
+ modifier = Modifier.semantics {
+ testTag = LoginsTestingTags.SAVED_LOGINS_LIST_ITEM + ".${item.url.trimmed()}"
+ },
)
}
}
@@ -185,6 +192,7 @@ private fun AddPasswordItem(
label = stringResource(R.string.preferences_logins_add_login_2),
modifier = modifier,
beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_plus_24),
+ description = stringResource(R.string.saved_logins_add_new_login_button_content_description),
onClick = { onAddPasswordClicked() },
)
}
@@ -306,10 +314,13 @@ private fun LoginsListTopBar(
)
}
- IconButton(onClick = { searchActive = true }, contentDescription = null) {
+ IconButton(
+ onClick = { searchActive = true },
+ contentDescription = stringResource(R.string.preferences_passwords_saved_logins_search_2),
+ ) {
Icon(
painter = painterResource(iconsR.drawable.mozac_ic_search_24),
- contentDescription = stringResource(R.string.preferences_passwords_saved_logins_search_2),
+ contentDescription = null,
)
}
},
@@ -336,6 +347,9 @@ private fun SearchBar(
},
errorText = "",
modifier = Modifier
+ .semantics {
+ testTag = LoginsTestingTags.SAVED_LOGINS_PASSWORD_SEARCH_FIELD
+ }
.fillMaxWidth()
.focusRequester(focusRequester),
trailingIcon = {
@@ -349,7 +363,9 @@ private fun SearchBar(
),
)
},
- contentDescription = null,
+ contentDescription = stringResource(
+ R.string.saved_logins_clear_search_text_button_content_description,
+ ),
) {
Icon(
painter = painterResource(iconsR.drawable.mozac_ic_cross_24),
diff --git a/mobile/android/fenix/app/src/main/res/values/strings.xml b/mobile/android/fenix/app/src/main/res/values/strings.xml
@@ -2475,7 +2475,10 @@
<string name="edit_login_navigate_back_button_content_description">Navigate back</string>
<!-- Content description, used by tools like screenreaders, to press on the edit login button. -->
<string name="edit_login_button_content_description">Edit login</string>
-
+ <!-- Content description, used by tools like screenreaders, to press on the add new login button. -->
+ <string name="saved_logins_add_new_login_button_content_description">Add new login</string>
+ <!-- Content description, used by tools like screenreaders, to press on the clear search text button. -->
+ <string name="saved_logins_clear_search_text_button_content_description">Clear search text</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
<string name="saved_login_open_site">Open site in browser</string>
<!-- Content Description (for screenreaders etc) read for the button to reveal a password in logins -->