diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginServerListItem.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginServerListItem.kt index 9385753d6c..575ab0d1a4 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginServerListItem.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginServerListItem.kt @@ -75,6 +75,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription @@ -195,7 +196,8 @@ fun LoginServerListItem( modifier = Modifier .padding(end = PADDING_SIZE.dp) .size(ICON_SIZE.dp) - .offset { offset }, + .offset { offset } + .testTag(LoginViewTestTags.SERVER_DELETE_BUTTON), ) { Icon( Icons.TwoTone.Delete, @@ -225,6 +227,7 @@ fun LoginServerListItem( .background(colorScheme.error) .width(DELETE_BUTTON_SIZE.dp) .height(rowHeightDp.value) + .testTag(LoginViewTestTags.SERVER_CONFIRM_DELETE_BUTTON) .clickable { removeServer(server) }, ) { val deleteContentDescription = stringResource(sf__server_delete_content_description) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginView.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginView.kt index 7ed627f54e..960105a4cd 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginView.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginView.kt @@ -90,6 +90,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.core.net.toUri import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -98,9 +99,11 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -234,6 +237,7 @@ fun LoginView() { ) } +@OptIn(ExperimentalComposeUiApi::class) @Composable internal fun LoginView( dynamicBackgroundColor: MutableState, @@ -254,6 +258,9 @@ internal fun LoginView( ) Scaffold( + // Expose Compose testTags as Android resource-ids so UI automation (UIAutomator2/UTAM) + // can anchor on the stable, locale-invariant tags rather than localized contentDescriptions. + modifier = Modifier.semantics { testTagsAsResourceId = true }, bottomBar = bottomAppBar, contentWindowInsets = WindowInsets.safeDrawing, topBar = topAppBar, @@ -285,7 +292,7 @@ internal fun LoginView( } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable internal fun DefaultTopAppBar( backgroundColor: Color, @@ -324,6 +331,7 @@ internal fun DefaultTopAppBar( disabledContainerColor = Color.Transparent, disabledContentColor = Color.Transparent, ), + modifier = Modifier.testTag(LoginViewTestTags.MORE_OPTIONS_BUTTON), ) { Icon(Icons.Default.MoreVert, contentDescription = moreOptionsDescription) } @@ -335,33 +343,36 @@ internal fun DefaultTopAppBar( DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false }, + // The menu renders in its own popup window, so opt in here as well to expose + // the menu items' testTags as Android resource-ids for UI automation. + modifier = Modifier.semantics { testTagsAsResourceId = true }, ) { - MenuItem(stringResource(sf__pick_server)) { + MenuItem(stringResource(sf__pick_server), testTag = LoginViewTestTags.MENU_ITEM_PICK_SERVER) { showServerPicker.value = true showMenu = false } - MenuItem(stringResource(sf__clear_cookies)) { + MenuItem(stringResource(sf__clear_cookies), testTag = LoginViewTestTags.MENU_ITEM_CLEAR_COOKIES) { clearCookies() reloadWebView() showMenu = false } - MenuItem(stringResource(sf__clear_cache)) { + MenuItem(stringResource(sf__clear_cache), testTag = LoginViewTestTags.MENU_ITEM_CLEAR_CACHE) { clearWebViewCache() reloadWebView() showMenu = false } - MenuItem(stringResource(sf__reload)) { + MenuItem(stringResource(sf__reload), testTag = LoginViewTestTags.MENU_ITEM_RELOAD) { reloadWebView() showMenu = false } onLoginForAdmins?.let { - MenuItem(stringResource(sf__login_for_admins)) { + MenuItem(stringResource(sf__login_for_admins), testTag = LoginViewTestTags.MENU_ITEM_LOGIN_FOR_ADMINS) { it.invoke() showMenu = false } } showDevSupport?.let { - MenuItem(stringResource(sf__dev_support_title_menu_item)) { + MenuItem(stringResource(sf__dev_support_title_menu_item), testTag = LoginViewTestTags.MENU_ITEM_DEV_SUPPORT) { it.invoke() showMenu = false } @@ -380,6 +391,7 @@ internal fun DefaultTopAppBar( disabledContainerColor = Color.Transparent, disabledContentColor = Color.Transparent, ), + modifier = Modifier.testTag(LoginViewTestTags.BACK_BUTTON), ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, @@ -403,6 +415,7 @@ internal fun DefaultLoadingIndicator() { modifier = Modifier .size(LOADING_INDICATOR_SIZE.dp) .fillMaxSize() + .testTag(LoginViewTestTags.LOADING_INDICATOR) .semantics { contentDescription = description }, ) } @@ -411,6 +424,7 @@ internal fun DefaultLoadingIndicator() { @Composable internal fun MenuItem( text: String, + testTag: String? = null, onClick: () -> Unit, ) { DropdownMenuItem( @@ -422,7 +436,9 @@ internal fun MenuItem( ) }, onClick = onClick, - modifier = Modifier.semantics { contentDescription = text } + modifier = Modifier + .then(if (testTag != null) Modifier.testTag(testTag) else Modifier) + .semantics { contentDescription = text } ) } @@ -469,6 +485,7 @@ internal fun DefaultBottomAppBar( .padding(PADDING_SIZE.dp) .height(BUTTON_HEIGHT.dp) .fillMaxWidth() + .testTag(LoginViewTestTags.LOGIN_BUTTON) .shadow(LEVEL_3_ELEVATION.dp, buttonShape), shape = buttonShape, contentPadding = PaddingValues(PADDING_SIZE.dp), diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginViewTestTags.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginViewTestTags.kt new file mode 100644 index 0000000000..0900f507af --- /dev/null +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginViewTestTags.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.androidsdk.ui.components + +/** + * Stable, locale-invariant test tags for the Mobile SDK login screen and the + * server / account picker bottom sheet. + * + * Each value is applied to its Compose node via [androidx.compose.ui.platform.testTag]. Because + * the composition roots that host these nodes opt in with + * `Modifier.semantics { testTagsAsResourceId = true }`, the tags are surfaced to UI automation + * (UIAutomator2 / UTAM) as Android `resource-id`s. Unlike the localized `contentDescription` + * strings the UI also exposes, these values never change across device languages, so test and + * page-object code can anchor on them in any locale — including right-to-left layouts. + * + * This object is the single source of truth for those values. It is intentionally public so that + * SDK instrumented tests, sample-app page objects (e.g. AuthFlowTester), and external UTAM page + * objects can all reference the same constants instead of duplicating the literal strings. + * + * IMPORTANT: These are test anchors only. Do NOT localize them and do NOT reuse them as + * user-facing strings. + */ +object LoginViewTestTags { + + // region Login screen top bar + /** Three-dot "More Options" overflow button in the login top app bar. */ + const val MORE_OPTIONS_BUTTON = "sf__more_options_button" + + /** Navigation "back" button in the login top app bar (shown conditionally). */ + const val BACK_BUTTON = "sf__back_button" + // endregion + + // region Login screen overflow menu items + /** "Change Server" overflow menu item. */ + const val MENU_ITEM_PICK_SERVER = "sf__menu_item_pick_server" + + /** "Clear Cookies" overflow menu item. */ + const val MENU_ITEM_CLEAR_COOKIES = "sf__menu_item_clear_cookies" + + /** "Clear Cache" overflow menu item. */ + const val MENU_ITEM_CLEAR_CACHE = "sf__menu_item_clear_cache" + + /** "Reload" overflow menu item. */ + const val MENU_ITEM_RELOAD = "sf__menu_item_reload" + + /** "Login for Admins" overflow menu item (shown conditionally). */ + const val MENU_ITEM_LOGIN_FOR_ADMINS = "sf__menu_item_login_for_admins" + + /** "Developer Support" overflow menu item (debug builds only). */ + const val MENU_ITEM_DEV_SUPPORT = "sf__menu_item_dev_support" + // endregion + + // region Login screen body + /** Bottom-bar login action button (biometric / IDP / custom). */ + const val LOGIN_BUTTON = "sf__login_button" + + /** Indeterminate loading spinner shown over the login WebView. */ + const val LOADING_INDICATOR = "sf__loading_indicator" + // endregion + + // region Picker bottom sheet + /** Root container of the login-server picker bottom sheet. */ + const val SERVER_PICKER = "sf__server_picker" + + /** Root container of the user-account picker bottom sheet. */ + const val ACCOUNT_PICKER = "sf__account_picker" + + /** Back arrow shown in the picker header while adding a new connection. */ + const val PICKER_BACK_BUTTON = "sf__picker_back_button" + + /** Close (X) button in the picker header. */ + const val PICKER_CLOSE_BUTTON = "sf__picker_close_button" + + /** "Add New Connection" button in the login-server picker. */ + const val CUSTOM_URL_BUTTON = "sf__custom_url_button" + + /** "Add New Account" button in the user-account picker. */ + const val ADD_NEW_ACCOUNT_BUTTON = "sf__add_new_account_button" + + /** Name field in the picker's "Add Connection" form. */ + const val PICKER_CUSTOM_LABEL = "sf__picker_custom_label" + + /** URL field in the picker's "Add Connection" form. */ + const val PICKER_CUSTOM_URL = "sf__picker_custom_url" + + /** Save / apply button in the picker's "Add Connection" form. */ + const val APPLY_BUTTON = "sf__apply_button" + // endregion + + // region Server list item + /** + * Trash icon on a custom server row that arms the slide-to-delete affordance. + * + * Every custom server row carries this same tag by design, so a bare match resolves to + * multiple nodes. To target a specific row, combine it with a content matcher that + * identifies the row: + * ``` + * onNode(hasTestTag(SERVER_DELETE_BUTTON) and hasAncestor(hasText("MyOrg"))) + * ``` + */ + const val SERVER_DELETE_BUTTON = "sf__server_delete_button" + + /** + * Revealed confirm-delete target on a custom server row. + * + * Shared across all custom rows just like [SERVER_DELETE_BUTTON]; combine it with a content + * matcher (e.g. `and hasAncestor(hasText("MyOrg"))`) to target a specific row. + */ + const val SERVER_CONFIRM_DELETE_BUTTON = "sf__server_confirm_delete_button" + // endregion +} diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/PickerBottomSheet.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/PickerBottomSheet.kt index c07daa142e..68cb9741dd 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/PickerBottomSheet.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/PickerBottomSheet.kt @@ -90,6 +90,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -101,6 +102,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview @@ -250,7 +252,7 @@ internal fun TestablePickerBottomSheet( } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable @VisibleForTesting internal fun PickerBottomSheet( @@ -270,6 +272,15 @@ internal fun PickerBottomSheet( PickerStyle.LoginServerPicker -> stringResource(sf__server_picker_content_description) PickerStyle.UserAccountPicker -> stringResource(sf__account_picker_content_description) } + // Stable, locale-invariant test anchor for the picker container and its add button. + val containerTestTag = when (pickerStyle) { + PickerStyle.LoginServerPicker -> LoginViewTestTags.SERVER_PICKER + PickerStyle.UserAccountPicker -> LoginViewTestTags.ACCOUNT_PICKER + } + val addButtonTestTag = when (pickerStyle) { + PickerStyle.LoginServerPicker -> LoginViewTestTags.CUSTOM_URL_BUTTON + PickerStyle.UserAccountPicker -> LoginViewTestTags.ADD_NEW_ACCOUNT_BUTTON + } val addButtonContentDescription = when (pickerStyle) { PickerStyle.LoginServerPicker -> stringResource(sf__custom_url_button_content_description) PickerStyle.UserAccountPicker -> stringResource(sf__add_new_account_content_description) @@ -300,7 +311,13 @@ internal fun PickerBottomSheet( Column( modifier = Modifier .animateContentSize() - .semantics { contentDescription = containerContentDescription } + // The bottom sheet is hosted in its own window/composition, so opt in here too + // to expose Compose testTags as Android resource-ids for UI automation. + .semantics { + testTagsAsResourceId = true + contentDescription = containerContentDescription + } + .testTag(containerTestTag) .focusRequester(pickerFocus) .focusable(), ) { @@ -326,7 +343,9 @@ internal fun PickerBottomSheet( disabledContainerColor = Color.Transparent, disabledContentColor = Color.Transparent, ), - modifier = Modifier.size(ICON_SIZE.dp), + modifier = Modifier + .size(ICON_SIZE.dp) + .testTag(LoginViewTestTags.PICKER_BACK_BUTTON), ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, @@ -362,7 +381,9 @@ internal fun PickerBottomSheet( disabledContainerColor = Color.Transparent, disabledContentColor = Color.Transparent, ), - modifier = Modifier.size(ICON_SIZE.dp), + modifier = Modifier + .size(ICON_SIZE.dp) + .testTag(LoginViewTestTags.PICKER_CLOSE_BUTTON), ) { Icon( Icons.Default.Close, @@ -475,6 +496,7 @@ internal fun PickerBottomSheet( modifier = Modifier .padding(PADDING_SIZE.dp) .fillMaxWidth() + .testTag(addButtonTestTag) .semantics { contentDescription = addButtonContentDescription }, shape = RoundedCornerShape(CORNER_RADIUS.dp), contentPadding = PaddingValues(PADDING_SIZE.dp), @@ -538,7 +560,7 @@ internal fun AddConnection( .fillMaxWidth() .padding(PADDING_SIZE.dp) .focusRequester(focusRequester) - .testTag("sf__picker_custom_label") + .testTag(LoginViewTestTags.PICKER_CUSTOM_LABEL) .semantics { contentDescription = nameFieldDesc }, colors = TextFieldDefaults.colors( focusedIndicatorColor = colorScheme.tertiary, @@ -562,7 +584,7 @@ internal fun AddConnection( modifier = Modifier .fillMaxWidth() .padding(start = PADDING_SIZE.dp, end = PADDING_SIZE.dp) - .testTag("sf__picker_custom_url") + .testTag(LoginViewTestTags.PICKER_CUSTOM_URL) .semantics { contentDescription = urlFieldDesc }, colors = TextFieldDefaults.colors( focusedIndicatorColor = colorScheme.tertiary, @@ -586,7 +608,7 @@ internal fun AddConnection( modifier = Modifier .padding(PADDING_SIZE.dp) .fillMaxWidth() - .testTag("sf__apply_button") + .testTag(LoginViewTestTags.APPLY_BUTTON) .semantics { contentDescription = applyButtonDesc }, shape = RoundedCornerShape(CORNER_RADIUS.dp), contentPadding = PaddingValues(PADDING_SIZE.dp), diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt index 9a7093c988..582734e6b1 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt @@ -45,18 +45,15 @@ import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onChildAt -import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule -import com.salesforce.androidsdk.R.string.sf__account_selector_text -import com.salesforce.androidsdk.R.string.sf__custom_url_button -import com.salesforce.androidsdk.R.string.sf__pick_server import com.salesforce.androidsdk.accounts.UserAccountManager import com.salesforce.androidsdk.config.LoginServerManager.LoginServer import com.salesforce.androidsdk.ui.components.AddConnection +import com.salesforce.androidsdk.ui.components.LoginViewTestTags import com.salesforce.androidsdk.ui.components.PickerBottomSheet import com.salesforce.androidsdk.ui.components.PickerStyle import com.salesforce.androidsdk.ui.components.TestablePickerBottomSheet @@ -66,9 +63,6 @@ import org.junit.Assert import org.junit.Rule import org.junit.Test -private const val NAME_FIELD_IDENTIFIER = "Name" -private const val URL_FIELD_IDENTIFIER = "URL" -private const val SAVE_BUTTON_IDENTIFIER = "Save" private const val TEST_NAME = "Production" private const val VALID_URL = "https://login.salesforce.com" private const val INVALID_URL = "invalid" @@ -128,10 +122,8 @@ class PickerBottomSheetTest { ) } - val context = getInstrumentation().targetContext - val button = composeTestRule.onNode(hasText(context.getString(sf__account_selector_text))) - - button.assertIsDisplayed() + // Anchor on the locale-invariant container tag rather than the localized title text. + composeTestRule.onNodeWithTag(LoginViewTestTags.ACCOUNT_PICKER).assertIsDisplayed() } @OptIn(ExperimentalMaterial3Api::class) @@ -145,10 +137,8 @@ class PickerBottomSheetTest { ) } - val context = getInstrumentation().targetContext - val button = composeTestRule.onNode(hasText(context.getString(sf__pick_server))) - - button.assertIsDisplayed() + // Anchor on the locale-invariant container tag rather than the localized title text. + composeTestRule.onNodeWithTag(LoginViewTestTags.SERVER_PICKER).assertIsDisplayed() } // endregion @@ -168,9 +158,9 @@ class PickerBottomSheetTest { AddConnection(getValidServer = serverValidator) } - val nameField = composeTestRule.onNodeWithText(NAME_FIELD_IDENTIFIER) - val urlField = composeTestRule.onNodeWithText(URL_FIELD_IDENTIFIER) - val saveButton = composeTestRule.onNodeWithText(SAVE_BUTTON_IDENTIFIER) + val nameField = composeTestRule.onNodeWithTag(LoginViewTestTags.PICKER_CUSTOM_LABEL) + val urlField = composeTestRule.onNodeWithTag(LoginViewTestTags.PICKER_CUSTOM_URL) + val saveButton = composeTestRule.onNodeWithTag(LoginViewTestTags.APPLY_BUTTON) saveButton.assertIsDisplayed() saveButton.assertIsNotEnabled() @@ -201,9 +191,9 @@ class PickerBottomSheetTest { ) } - val nameField = composeTestRule.onNodeWithText(NAME_FIELD_IDENTIFIER) - val urlField = composeTestRule.onNodeWithText(URL_FIELD_IDENTIFIER) - val saveButton = composeTestRule.onNodeWithText(SAVE_BUTTON_IDENTIFIER) + val nameField = composeTestRule.onNodeWithTag(LoginViewTestTags.PICKER_CUSTOM_LABEL) + val urlField = composeTestRule.onNodeWithTag(LoginViewTestTags.PICKER_CUSTOM_URL) + val saveButton = composeTestRule.onNodeWithTag(LoginViewTestTags.APPLY_BUTTON) nameField.performTextInput(TEST_NAME) urlField.performTextInput(VALID_URL) @@ -304,10 +294,7 @@ class PickerBottomSheetTest { ) } - val context = getInstrumentation().targetContext - val button = composeTestRule.onNode(hasText(context.getString(sf__custom_url_button))) - - button.assertIsDisplayed() + composeTestRule.onNodeWithTag(LoginViewTestTags.CUSTOM_URL_BUTTON).assertIsDisplayed() } @OptIn(ExperimentalMaterial3Api::class) @@ -320,10 +307,7 @@ class PickerBottomSheetTest { ) } - val context = getInstrumentation().targetContext - val button = composeTestRule.onNode(hasText(context.getString(sf__custom_url_button))) - - button.assertDoesNotExist() + composeTestRule.onNodeWithTag(LoginViewTestTags.CUSTOM_URL_BUTTON).assertDoesNotExist() } @OptIn(ExperimentalMaterial3Api::class) diff --git a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/pageObjects/LoginPageObject.kt b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/pageObjects/LoginPageObject.kt index 7e92b91101..9ff60ea7fd 100644 --- a/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/pageObjects/LoginPageObject.kt +++ b/native/NativeSampleApps/AuthFlowTester/src/androidTest/java/com/salesforce/samples/authflowtester/pageObjects/LoginPageObject.kt @@ -31,9 +31,9 @@ import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click @@ -48,6 +48,7 @@ import androidx.test.espresso.web.webdriver.Locator import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.salesforce.androidsdk.R +import com.salesforce.androidsdk.ui.components.LoginViewTestTags import com.salesforce.samples.authflowtester.testUtility.KnownLoginHostConfig import com.salesforce.samples.authflowtester.testUtility.KnownUserConfig import com.salesforce.samples.authflowtester.testUtility.testConfig @@ -76,13 +77,13 @@ open class LoginPageObject(composeTestRule: ComposeTestRule): BasePageObject(com /** * Returns true when the LoginActivity top bar is currently in front - * (detected via the SDK's "More Options" content description). Used by + * (detected via the SDK's locale-invariant "More Options" test tag). Used by * negative tests to assert the user did not advance past login. */ fun isLoginScreenVisible(): Boolean = try { composeTestRule - .onAllNodesWithContentDescription(getString(R.string.sf__more_options)) + .onAllNodesWithTag(LoginViewTestTags.MORE_OPTIONS_BUTTON) .fetchSemanticsNodes() .isNotEmpty() } catch (_: Throwable) { @@ -104,12 +105,12 @@ open class LoginPageObject(composeTestRule: ComposeTestRule): BasePageObject(com fun openLoginOptions() { // Tap "More Options" three-dot menu (Compose IconButton) - composeTestRule.onNodeWithContentDescription(getString(R.string.sf__more_options)) + composeTestRule.onNodeWithTag(LoginViewTestTags.MORE_OPTIONS_BUTTON) .performClick() composeTestRule.waitForIdle() // Tap "Developer Support" dropdown menu item - composeTestRule.onNodeWithText(getString(R.string.sf__dev_support_title_menu_item)) + composeTestRule.onNodeWithTag(LoginViewTestTags.MENU_ITEM_DEV_SUPPORT) .performClick() composeTestRule.waitForIdle() @@ -150,12 +151,12 @@ open class LoginPageObject(composeTestRule: ComposeTestRule): BasePageObject(com */ fun tapLoginForAdminsMenuItem() { // Tap "More Options" three-dot menu (Compose IconButton) - composeTestRule.onNodeWithContentDescription(getString(R.string.sf__more_options)) + composeTestRule.onNodeWithTag(LoginViewTestTags.MORE_OPTIONS_BUTTON) .performClick() composeTestRule.waitForIdle() // Tap "Login for Admins" dropdown menu item - composeTestRule.onNodeWithText(getString(R.string.sf__login_for_admins)) + composeTestRule.onNodeWithTag(LoginViewTestTags.MENU_ITEM_LOGIN_FOR_ADMINS) .performClick() composeTestRule.waitForIdle() } @@ -171,20 +172,19 @@ open class LoginPageObject(composeTestRule: ComposeTestRule): BasePageObject(com */ fun changeServerByUrl(url: String) { // Tap "More Options" three-dot menu (Compose IconButton) - composeTestRule.onNodeWithContentDescription(getString(R.string.sf__more_options)) + composeTestRule.onNodeWithTag(LoginViewTestTags.MORE_OPTIONS_BUTTON) .performClick() composeTestRule.waitForIdle() // Tap "Change Server" dropdown menu item - composeTestRule.onNodeWithText(getString(R.string.sf__pick_server)) + composeTestRule.onNodeWithTag(LoginViewTestTags.MENU_ITEM_PICK_SERVER) .performClick() // Wait for server picker bottom sheet to appear try { composeTestRule.waitUntil(timeoutMillis = TIMEOUT_MS) { - composeTestRule.onAllNodesWithContentDescription( - getString(R.string.sf__server_picker_content_description) - ).fetchSemanticsNodes().isNotEmpty() + composeTestRule.onAllNodesWithTag(LoginViewTestTags.SERVER_PICKER) + .fetchSemanticsNodes().isNotEmpty() } } catch (e: ComposeTimeoutException) { throw AssertionError("Timed out after ${TIMEOUT_MS}ms waiting for server picker bottom sheet to appear", e)