tor-browser

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

commit 4a5a188cac6a898947c9cd39a47ad21ae1d4b9ce
parent 58f09679dd572a8bca67bdb46cc4e13daf65b6cd
Author: Segun Famisa <sfamisa@mozilla.com>
Date:   Fri, 21 Nov 2025 14:33:28 +0000

Bug 1998200 - Use new compose card editor in place of the old view-based one r=android-reviewers,boek

Differential Revision: https://phabricator.services.mozilla.com/D273210

Diffstat:
Amobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/SemanticsNodeInteraction.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt | 195+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt | 211+++++++++++++++++++------------------------------------------------------------
3 files changed, 223 insertions(+), 230 deletions(-)

diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/SemanticsNodeInteraction.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/SemanticsNodeInteraction.kt @@ -0,0 +1,47 @@ +/* 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.helpers.ext + +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput + +/** + * Asserts that the current semantics node has an error matching the [message]. + * + * Throws [AssertionError] if the node does not have an error component matching [message] + */ +fun SemanticsNodeInteraction.assertHasError( + message: String, + errorMessageOnFail: String? = null, +): SemanticsNodeInteraction { + assert( + SemanticsMatcher.expectValue( + key = SemanticsProperties.Error, + expectedValue = message, + ), + messagePrefixOnError = errorMessageOnFail?.let { message -> + { message } + }, + ) + return this +} + +/** + * Clears the text in the node and then sets the given text. + * + * This is a convenience function that combines [performTextClearance] and [performTextInput]. + * + * @param text The text to be input. + * @return The [SemanticsNodeInteraction] for chaining. + */ +fun SemanticsNodeInteraction.clearAndSetText(text: String): SemanticsNodeInteraction { + performTextClearance() + performTextInput(text) + return this +} diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt @@ -8,6 +8,7 @@ import android.util.Log import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextContains import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isNotDisplayed @@ -18,6 +19,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView @@ -33,21 +35,22 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.endsWith -import org.junit.Assert.assertEquals import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription -import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.hasCousin import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.waitForAppWindowToBeUpdated import org.mozilla.fenix.helpers.click +import org.mozilla.fenix.helpers.ext.assertHasError +import org.mozilla.fenix.helpers.ext.clearAndSetText import org.mozilla.fenix.settings.address.ui.edit.EditAddressTestTag +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorTestTags class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) { @@ -430,20 +433,30 @@ class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) savedCreditCardNumber().clickAndWaitForNewWindow(waitingTime) Log.i(TAG, "clickSavedCreditCard: Clicked the saved credit card and and waited for $waitingTime ms for a new window") } + + @OptIn(ExperimentalTestApi::class) fun clickDeleteCreditCardToolbarButton() { Log.i(TAG, "clickDeleteCreditCardToolbarButton: Waiting for $waitingTime ms for the delete credit card toolbar button to exist") - deleteCreditCardToolbarButton().waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasTestTag(CreditCardEditorTestTags.TOPBAR_DELETE_BUTTON), + waitingTime, + ) Log.i(TAG, "clickDeleteCreditCardToolbarButton: Waited for $waitingTime ms for the delete credit card toolbar button to exist") Log.i(TAG, "clickDeleteCreditCardToolbarButton: Trying to click the delete credit card toolbar button") - deleteCreditCardToolbarButton().click() + composeTestRule.deleteCreditCardToolbarButton().performClick() Log.i(TAG, "clickDeleteCreditCardToolbarButton: Clicked the delete credit card toolbar button") } + + @OptIn(ExperimentalTestApi::class) fun clickDeleteCreditCardMenuButton() { Log.i(TAG, "clickDeleteCreditCardMenuButton: Waiting for $waitingTime ms for the delete credit card menu button to exist") - deleteCreditCardMenuButton().waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasTestTag(CreditCardEditorTestTags.DELETE_BUTTON), + waitingTime, + ) Log.i(TAG, "clickDeleteCreditCardMenuButton: Waited for $waitingTime ms for the delete credit card menu button to exist") Log.i(TAG, "clickDeleteCreditCardMenuButton: Trying to click the delete credit card menu button") - deleteCreditCardMenuButton().click() + composeTestRule.deleteFormButton().performClick() Log.i(TAG, "clickDeleteCreditCardMenuButton: Clicked the delete credit card menu button") } fun clickSaveAndAutofillCreditCardsOption() { @@ -454,89 +467,112 @@ class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) fun clickConfirmDeleteCreditCardButton() { Log.i(TAG, "clickConfirmDeleteCreditCardButton: Trying to click the \"Delete\" credit card dialog button") - confirmDeleteCreditCardButton().click() + composeTestRule.confirmDeleteCreditCardButton().performClick() Log.i(TAG, "clickConfirmDeleteCreditCardButton: Clicked the \"Delete\" credit card dialog button") } fun clickCancelDeleteCreditCardButton() { Log.i(TAG, "clickCancelDeleteCreditCardButton: Trying to click the \"Cancel\" credit card dialog button") - cancelDeleteCreditCardButton().click() + composeTestRule.cancelDeleteCreditCardButton().performClick() Log.i(TAG, "clickCancelDeleteCreditCardButton: Clicked the \"Cancel\" credit card dialog button") } + @OptIn(ExperimentalTestApi::class) fun clickExpiryMonthOption(expiryMonth: String) { Log.i(TAG, "clickExpiryMonthOption: Waiting for $waitingTime ms for the $expiryMonth expiry month option to exist") - expiryMonthOption(expiryMonth).waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasText(expiryMonth, substring = true, ignoreCase = true), + waitingTime, + ) Log.i(TAG, "clickExpiryMonthOption: Waited for $waitingTime ms for the $expiryMonth expiry month option to exist") Log.i(TAG, "clickExpiryMonthOption: Trying to click $expiryMonth expiry month option") - expiryMonthOption(expiryMonth).click() + composeTestRule.expiryMonthOption(expiryMonth).performClick() Log.i(TAG, "clickExpiryMonthOption: Clicked $expiryMonth expiry month option") } + @OptIn(ExperimentalTestApi::class) fun clickExpiryYearOption(expiryYear: String) { Log.i(TAG, "clickExpiryYearOption: Waiting for $waitingTime ms for the $expiryYear expiry year option to exist") - expiryYearOption(expiryYear).waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasText(expiryYear, substring = true, ignoreCase = true), + waitingTime, + ) Log.i(TAG, "clickExpiryYearOption: Waited for $waitingTime ms for the $expiryYear expiry year option to exist") + Log.i(TAG, "clickExpiryYearOption: Trying to click $expiryYear expiry year option") - expiryYearOption(expiryYear).click() + composeTestRule.expiryYearOption(expiryYear).performClick() Log.i(TAG, "clickExpiryYearOption: Clicked $expiryYear expiry year option") } fun verifyAddCreditCardsButton() = assertUIObjectExists(addCreditCardButton()) + @OptIn(ExperimentalTestApi::class) fun fillAndSaveCreditCard(cardNumber: String, cardName: String, expiryMonth: String, expiryYear: String) { Log.i(TAG, "fillAndSaveCreditCard: Waiting for $waitingTime ms for the credit card number text field to exist") - creditCardNumberTextInput().waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasTestTag(CreditCardEditorTestTags.CARD_NUMBER_FIELD), + waitingTime, + ) Log.i(TAG, "fillAndSaveCreditCard: Waited for $waitingTime ms for the credit card number text field to exist") Log.i(TAG, "fillAndSaveCreditCard: Trying to set the credit card number to: $cardNumber") - creditCardNumberTextInput().setText(cardNumber) + composeTestRule.creditCardNumberTextInput().clearAndSetText(cardNumber) Log.i(TAG, "fillAndSaveCreditCard: The credit card number was set to: $cardNumber") Log.i(TAG, "fillAndSaveCreditCard: Trying to set the name on card to: $cardName") - nameOnCreditCardTextInput().setText(cardName) + composeTestRule.nameOnCreditCardTextInput().clearAndSetText(cardName) Log.i(TAG, "fillAndSaveCreditCard: The credit card name was set to: $cardName") Log.i(TAG, "fillAndSaveCreditCard: Trying to click the expiry month dropdown") - expiryMonthDropDown().click() + composeTestRule.expiryMonthDropDown().performClick() + Log.i(TAG, "fillAndSaveCreditCard: Clicked the expiry month dropdown") Log.i(TAG, "fillAndSaveCreditCard: Trying to click $expiryMonth expiry month option") clickExpiryMonthOption(expiryMonth) Log.i(TAG, "fillAndSaveCreditCard: Clicked $expiryMonth expiry month option") Log.i(TAG, "fillAndSaveCreditCard: Trying to click the expiry year dropdown") - expiryYearDropDown().click() + composeTestRule.expiryYearDropDown().performClick() Log.i(TAG, "fillAndSaveCreditCard: Clicked the expiry year dropdown") Log.i(TAG, "fillAndSaveCreditCard: Trying to click $expiryYear expiry year option") clickExpiryYearOption(expiryYear) Log.i(TAG, "fillAndSaveCreditCard: Clicked $expiryYear expiry year option") Log.i(TAG, "fillAndSaveCreditCard: Trying to click the \"Save\" button") - saveButton().click() + composeTestRule.saveFormButton().performClick() Log.i(TAG, "fillAndSaveCreditCard: Clicked the \"Save\" button") Log.i(TAG, "fillAndSaveCreditCard: Waiting for $waitingTime ms for the \"Manage saved cards\" button to exist") manageSavedCreditCardsButton().waitForExists(waitingTime) Log.i(TAG, "fillAndSaveCreditCard: Waited for $waitingTime ms for the \"Manage saved cards\" button to exist") } + @OptIn(ExperimentalTestApi::class) fun clearCreditCardNumber() = - creditCardNumberTextInput().also { + composeTestRule.creditCardNumberTextInput().also { Log.i(TAG, "clearCreditCardNumber: Waiting for $waitingTime ms for the credit card number text field to exist") - it.waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasTestTag(CreditCardEditorTestTags.CARD_NUMBER_FIELD), + waitingTime, + ) Log.i(TAG, "clearCreditCardNumber: Waited for $waitingTime ms for the credit card number text field to exist") + Log.i(TAG, "clearCreditCardNumber: Trying to clear the credit card number text field") - it.clearTextField() + it.performTextClearance() Log.i(TAG, "clearCreditCardNumber: Cleared the credit card number text field") } + @OptIn(ExperimentalTestApi::class) fun clearNameOnCreditCard() = - nameOnCreditCardTextInput().also { + composeTestRule.nameOnCreditCardTextInput().also { Log.i(TAG, "clearNameOnCreditCard: Waiting for $waitingTime ms for name on card text field to exist") - it.waitForExists(waitingTime) + composeTestRule.waitUntilAtLeastOneExists( + hasTestTag(CreditCardEditorTestTags.NAME_ON_CARD_FIELD), + waitingTime, + ) Log.i(TAG, "clearNameOnCreditCard: Waited for $waitingTime ms for name on card text field to exist") Log.i(TAG, "clearNameOnCreditCard: Trying to clear the name on card text field") - it.clearTextField() + it.performTextClearance() Log.i(TAG, "clearNameOnCreditCard: Cleared the name on card text field") } fun clickSaveCreditCardToolbarButton() { Log.i(TAG, "clickSaveCreditCardToolbarButton: Trying to click the save credit card toolbar button") - saveCreditCardToolbarButton().click() + composeTestRule.saveCreditCardToolbarButton().performClick() Log.i(TAG, "clickSaveCreditCardToolbarButton: Clicked the save credit card toolbar button") } @@ -545,46 +581,58 @@ class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) cardName: String, expiryMonth: String, expiryYear: String, - ) { - assertUIObjectExists( - editCreditCardToolbarTitle(), - navigateBackButton(), - deleteCreditCardToolbarButton(), - saveCreditCardToolbarButton(), - ) + ) = with(composeTestRule) { + editCreditCardToolbarTitle() + .assertExists("Unable to assert that the edit credit card toolbar title exists") + deleteCreditCardToolbarButton() + .assertExists("Unable to assert that the delete credit card toolbar button exists") + saveCreditCardToolbarButton() + .assertExists("Unable to assert that the save credit card toolbar button exists") + Log.i(TAG, "verifyEditCreditCardView: Trying to verify that the card number text field is set to: $cardNumber") - assertEquals(cardNumber, creditCardNumberTextInput().text) + creditCardNumberTextInput() + .assertTextContains(cardNumber) Log.i(TAG, "verifyEditCreditCardView: Verified that the card number text field was set to: $cardNumber") Log.i(TAG, "verifyEditCreditCardView: Trying to verify that the card name text field is set to: $cardName") - assertEquals(cardName, nameOnCreditCardTextInput().text) + nameOnCreditCardTextInput().assertTextContains(cardName) Log.i(TAG, "verifyEditCreditCardView: Verified that the card card name text field was set to: $cardName") // Can't get the text from the drop-down items, need to verify them individually - assertUIObjectExists( - expiryYearDropDown(), - expiryMonthDropDown(), - ) + expiryMonthDropDown() + .assertExists("Unable to assert that the expiry month dropdown exists") + expiryYearDropDown() + .assertExists("Unable to assert that the expiry year dropdown exists") - assertUIObjectExists( - itemContainingText(expiryMonth), - itemContainingText(expiryYear), - ) + onNodeWithText(expiryMonth, substring = true) + .assertExists("Unable to assert that the $expiryMonth expiry month is shown") + onNodeWithText(expiryYear, substring = true) + .assertExists("Unable to assert that the $expiryYear expiry year is shown") - assertUIObjectExists( - saveButton(), - cancelButton(), - ) - - assertUIObjectExists(deleteCreditCardMenuButton()) + saveFormButton().assertExists("Unable to assert that the save button exists") + cancelFormButton().assertExists("Unable to assert that the cancel button exists") + deleteFormButton().assertExists("Unable to assert that the delete button exists") } - fun verifyEditCreditCardToolbarTitle() = assertUIObjectExists(editCreditCardToolbarTitle()) + fun verifyEditCreditCardToolbarTitle() = composeTestRule.editCreditCardToolbarTitle() + .assertExists("Unable to assert that the edit credit card toolbar title exists") - fun verifyCreditCardNumberErrorMessage() = - assertUIObjectExists(itemContainingText(getStringResource(R.string.credit_cards_number_validation_error_message_2))) + fun verifyCreditCardNumberErrorMessage() { + val errorMessage = + getStringResource(R.string.credit_cards_number_validation_error_message_2) + + composeTestRule.creditCardNumberTextInput() + .assertHasError(errorMessage) + } - fun verifyNameOnCreditCardErrorMessage() = - assertUIObjectExists(itemContainingText(getStringResource(R.string.credit_cards_name_on_card_validation_error_message_2))) + fun verifyNameOnCreditCardErrorMessage() { + val errorMessage = + getStringResource(R.string.credit_cards_name_on_card_validation_error_message_2) + composeTestRule.nameOnCreditCardTextInput() + .assertHasError( + errorMessage, + "Unable to assert that the name on card validation error message exists", + ) + } class Transition(private val composeTestRule: ComposeTestRule) { fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { @@ -616,7 +664,7 @@ class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) fun goBackToSavedCreditCards(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition { Log.i(TAG, "goBackToSavedCreditCards: Trying to click the navigate up toolbar button") - navigateBackButton().click() + composeTestRule.navigateBackButton().performClick() Log.i(TAG, "goBackToSavedCreditCards: Clicked the navigate up toolbar button") SettingsSubMenuAutofillRobot(composeTestRule).interact() @@ -684,24 +732,33 @@ private fun saveAndAutofillCreditCardsSummary() = itemContainingText(getStringRe private fun syncCreditCardsAcrossDevicesButton() = itemContainingText(getStringResource(R.string.preferences_credit_cards_sync_cards_across_devices)) private fun addCreditCardButton() = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_add_credit_card_2))) private fun savedCreditCardsToolbarTitle() = itemContainingText(getStringResource(R.string.credit_cards_saved_cards)) -private fun editCreditCardToolbarTitle() = itemContainingText(getStringResource(R.string.credit_cards_edit_card)) +private fun ComposeTestRule.editCreditCardToolbarTitle() = onNodeWithText(getStringResource(R.string.credit_cards_edit_card)) private fun manageSavedCreditCardsButton() = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_manage_saved_cards_2))) -private fun creditCardNumberTextInput() = mDevice.findObject(UiSelector().resourceId("$packageName:id/card_number_input")) -private fun nameOnCreditCardTextInput() = mDevice.findObject(UiSelector().resourceId("$packageName:id/name_on_card_input")) -private fun expiryMonthDropDown() = mDevice.findObject(UiSelector().resourceId("$packageName:id/expiry_month_drop_down")) -private fun expiryYearDropDown() = mDevice.findObject(UiSelector().resourceId("$packageName:id/expiry_year_drop_down")) + +private fun ComposeTestRule.creditCardNumberTextInput() = onNodeWithTag(CreditCardEditorTestTags.CARD_NUMBER_FIELD).onChildAt(0) + +private fun ComposeTestRule.nameOnCreditCardTextInput() = onNodeWithTag(CreditCardEditorTestTags.NAME_ON_CARD_FIELD).onChildAt(0) + +private fun ComposeTestRule.expiryMonthDropDown() = + onNodeWithTag(CreditCardEditorTestTags.EXPIRATION_MONTH_FIELD) + +private fun ComposeTestRule.expiryYearDropDown() = + onNodeWithTag(CreditCardEditorTestTags.EXPIRATION_YEAR_FIELD) + private fun savedCreditCardNumber() = mDevice.findObject(UiSelector().resourceId("$packageName:id/credit_card_logo")) -private fun deleteCreditCardToolbarButton() = mDevice.findObject(UiSelector().resourceId("$packageName:id/delete_credit_card_button")) -private fun saveCreditCardToolbarButton() = itemWithResId("$packageName:id/save_credit_card_button") -private fun deleteCreditCardMenuButton() = itemContainingText(getStringResource(R.string.credit_cards_delete_card_button)) -private fun confirmDeleteCreditCardButton() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog()) -private fun cancelDeleteCreditCardButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog()) +private fun ComposeTestRule.deleteCreditCardToolbarButton() = onNodeWithTag(CreditCardEditorTestTags.TOPBAR_DELETE_BUTTON) +private fun ComposeTestRule.saveCreditCardToolbarButton() = onNodeWithTag(CreditCardEditorTestTags.TOPBAR_SAVE_BUTTON) +private fun ComposeTestRule.confirmDeleteCreditCardButton() = onNodeWithTag(CreditCardEditorTestTags.DELETE_DIALOG_DELETE_BUTTON) +private fun ComposeTestRule.cancelDeleteCreditCardButton() = onNodeWithTag(CreditCardEditorTestTags.DELETE_DIALOG_CANCEL_BUTTON) private fun securedCreditCardsLaterButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog()) -private fun saveButton() = itemWithResId("$packageName:id/save_button") -private fun cancelButton() = itemWithResId("$packageName:id/cancel_button") -private fun savedAddress(name: String) = mDevice.findObject(UiSelector().textContains(name)) +private fun ComposeTestRule.saveFormButton() = onNodeWithTag(CreditCardEditorTestTags.SAVE_BUTTON) +private fun ComposeTestRule.cancelFormButton() = onNodeWithTag(CreditCardEditorTestTags.CANCEL_BUTTON) +private fun ComposeTestRule.deleteFormButton() = onNodeWithTag(CreditCardEditorTestTags.DELETE_BUTTON) + private fun ComposeTestRule.subRegionOption(subRegion: String) = onNodeWithTag(EditAddressTestTag.ADDRESS_LEVEL1_FIELD + ".$subRegion") private fun ComposeTestRule.countryOption(country: String) = onNodeWithTag(EditAddressTestTag.COUNTRY_FIELD + ".$country") -private fun expiryMonthOption(expiryMonth: String) = mDevice.findObject(UiSelector().textContains(expiryMonth)) -private fun expiryYearOption(expiryYear: String) = mDevice.findObject(UiSelector().textContains(expiryYear)) +private fun ComposeTestRule.expiryMonthOption(expiryMonth: String) = + onNodeWithText(expiryMonth, substring = true, ignoreCase = true) + +private fun ComposeTestRule.expiryYearOption(expiryYear: String) = onNodeWithText(expiryYear, substring = true, ignoreCase = true) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt @@ -4,138 +4,71 @@ package org.mozilla.fenix.settings.creditcards -import android.content.DialogInterface import android.os.Bundle -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem +import android.view.LayoutInflater import android.view.View -import android.view.autofill.AutofillManager -import android.widget.AdapterView -import androidx.appcompat.app.AlertDialog -import androidx.core.view.MenuProvider -import androidx.lifecycle.Lifecycle +import android.view.ViewGroup +import androidx.compose.runtime.LaunchedEffect +import androidx.fragment.compose.content import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import mozilla.components.support.ktx.android.view.hideKeyboard -import mozilla.components.support.ktx.android.view.showKeyboard -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.SecureFragment -import org.mozilla.fenix.databinding.FragmentCreditCardEditorBinding -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.placeCursorAtEnd +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.redirectToReAuth -import org.mozilla.fenix.ext.showToolbar -import org.mozilla.fenix.settings.creditcards.controller.DefaultCreditCardEditorController -import org.mozilla.fenix.settings.creditcards.interactor.CreditCardEditorInteractor -import org.mozilla.fenix.settings.creditcards.interactor.DefaultCreditCardEditorInteractor -import org.mozilla.fenix.settings.creditcards.view.CreditCardEditorView +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorAction +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorEnvironment +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorMiddleware +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorScreen +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorState +import org.mozilla.fenix.settings.creditcards.ui.CreditCardEditorStore +import org.mozilla.fenix.settings.creditcards.ui.DefaultCalendarDataProvider +import org.mozilla.fenix.theme.FirefoxTheme /** * Display a credit card editor for adding and editing a credit card. */ -class CreditCardEditorFragment : - SecureFragment(R.layout.fragment_credit_card_editor), - MenuProvider { - - private lateinit var creditCardEditorState: CreditCardEditorState - private lateinit var creditCardEditorView: CreditCardEditorView - private lateinit var menu: Menu - - private var deleteDialog: AlertDialog? = null +class CreditCardEditorFragment : SecureFragment() { private val args by navArgs<CreditCardEditorFragmentArgs>() - /** - * Returns true if a credit card is being edited, and false otherwise. - */ - private val isEditing: Boolean - get() = args.creditCard != null - - private lateinit var interactor: CreditCardEditorInteractor - - private var isAutofillSessionActive = false - - private val autofillCallback = object : AutofillManager.AutofillCallback() { - override fun onAutofillEvent(view: View, event: Int) { - super.onAutofillEvent(view, event) - - when (event) { - EVENT_INPUT_SHOWN -> { - isAutofillSessionActive = true - } - EVENT_INPUT_HIDDEN -> { - isAutofillSessionActive = false - } - EVENT_INPUT_UNAVAILABLE -> { - isAutofillSessionActive = false - } - } - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + hideToolbar() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) - - val storage = requireContext().components.core.autofillStorage - interactor = DefaultCreditCardEditorInteractor( - controller = DefaultCreditCardEditorController( - storage = storage, - lifecycleScope = lifecycleScope, - navController = findNavController(), - showDeleteDialog = ::showDeleteDialog, - ), - ) - - val binding = FragmentCreditCardEditorBinding.bind(view) - - lifecycleScope.launch(Dispatchers.Main) { - creditCardEditorState = withContext(Dispatchers.IO) { - args.creditCard?.toCreditCardEditorState(storage) - ?: getInitialCreditCardEditorState() - } - creditCardEditorView = CreditCardEditorView(binding, interactor) - creditCardEditorView.bind(creditCardEditorState) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = content { + val store = StoreProvider.get(this) { + CreditCardEditorStore( + initialState = CreditCardEditorState.Default.copy(inEditMode = args.creditCard != null), + middleware = listOf( + CreditCardEditorMiddleware( + storage = requireComponents.core.autofillStorage, + calendarDataProvider = DefaultCalendarDataProvider(), + coroutineScope = lifecycleScope, + ), + ), + ) + }.also { + val environment = CreditCardEditorEnvironment( + navigateBack = { findNavController().popBackStack() }, + ) + it.dispatch(CreditCardEditorAction.EnvironmentRehydrated(environment)) + } - binding.apply { - cardNumberInput.apply { - requestFocus() - placeCursorAtEnd() - showKeyboard() - } - expiryMonthDropDown.onItemSelectedListener = - object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - expiryMonthDropDown.hideKeyboard() - } - override fun onNothingSelected(parent: AdapterView<*>) = Unit - } - expiryYearDropDown.onItemSelectedListener = - object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - expiryYearDropDown.hideKeyboard() - } - override fun onNothingSelected(parent: AdapterView<*>) = Unit - } - } + LaunchedEffect(Unit) { + store.dispatch(CreditCardEditorAction.Initialization.InitStarted(args.creditCard)) } - } - override fun onResume() { - super.onResume() - activity?.getSystemService(AutofillManager::class.java)?.registerCallback(autofillCallback) - if (!isEditing) { - showToolbar(getString(R.string.credit_cards_add_card)) - } else { - showToolbar(getString(R.string.credit_cards_edit_card)) + FirefoxTheme { + CreditCardEditorScreen(store) } } @@ -144,55 +77,11 @@ class CreditCardEditorFragment : * fragment is paused and the user is not navigating to [CreditCardsManagementFragment]. */ override fun onPause() { - activity?.getSystemService(AutofillManager::class.java)?.unregisterCallback(autofillCallback) - - view?.hideKeyboard() - menu.close() - deleteDialog?.dismiss() - - if (!isAutofillSessionActive) { - redirectToReAuth( - listOf(R.id.creditCardsManagementFragment), - findNavController().currentDestination?.id, - R.id.creditCardEditorFragment, - ) - } + redirectToReAuth( + listOf(R.id.creditCardsManagementFragment), + findNavController().currentDestination?.id, + R.id.creditCardEditorFragment, + ) super.onPause() } - - override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.credit_card_editor, menu) - this.menu = menu - - menu.findItem(R.id.delete_credit_card_button).isVisible = isEditing - } - - @Suppress("MagicNumber") - override fun onMenuItemSelected(item: MenuItem): Boolean = when (item.itemId) { - R.id.delete_credit_card_button -> { - args.creditCard?.let { interactor.onDeleteCardButtonClicked(it.guid) } - true - } - R.id.save_credit_card_button -> { - creditCardEditorView.saveCreditCard(creditCardEditorState) - true - } - else -> false - } - - private fun showDeleteDialog(onPositiveClickListener: DialogInterface.OnClickListener) { - deleteDialog = MaterialAlertDialogBuilder(requireContext()).apply { - setMessage(R.string.credit_cards_delete_dialog_confirmation_2) - setNegativeButton(R.string.credit_cards_cancel_button) { dialog: DialogInterface, _ -> - dialog.cancel() - } - setPositiveButton(R.string.credit_cards_delete_dialog_button, onPositiveClickListener) - create().withCenterAlignedButtons() - }.show() - } - - companion object { - // Number of years to show in the expiry year dropdown. - const val NUMBER_OF_YEARS_TO_SHOW = 10 - } }