tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/AddLoginScreen.kt | 40+++++++++++++++++++++++++++++++++-------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/EditLoginScreen.kt | 38++++++++++++++++++++++++++++++++++----
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginDetailsScreen.kt | 23++++++++++++++++-------
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginsTestingTags.kt | 35+++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt | 22+++++++++++++++++++---
Mmobile/android/fenix/app/src/main/res/values/strings.xml | 5++++-
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 -->