commit 308e56942794255aa8832bc60789b09ae6dee2e9
parent fdf492bdd079c37419a36b07abd37fdaa83ac7d3
Author: Segun Famisa <sfamisa@mozilla.com>
Date: Mon, 24 Nov 2025 13:17:17 +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:
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) {
@@ -436,20 +439,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() {
@@ -460,89 +473,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")
}
@@ -551,46 +587,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 {
@@ -622,7 +670,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()
@@ -690,24 +738,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,195 +4,84 @@
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 mozilla.components.lib.state.helpers.StoreProvider.Companion.storeProvider
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.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()
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ val store: CreditCardEditorStore = storeProvider.get { state ->
+ CreditCardEditorStore(
+ initialState = state
+ ?: CreditCardEditorState.Default.copy(inEditMode = args.creditCard != null),
+ middleware = listOf(
+ CreditCardEditorMiddleware(
+ CreditCardEditorEnvironment(
+ navigateBack = { findNavController().popBackStack() },
+ ),
+ storage = requireComponents.core.autofillStorage,
+ calendarDataProvider = DefaultCalendarDataProvider(),
+ coroutineScope = lifecycleScope,
+ ),
+ ),
+ )
+ }
+ return content {
+ LaunchedEffect(Unit) {
+ store.dispatch(CreditCardEditorAction.Initialization.InitStarted(args.creditCard))
}
- creditCardEditorView = CreditCardEditorView(binding, interactor)
- creditCardEditorView.bind(creditCardEditorState)
- 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
- }
+ FirefoxTheme {
+ CreditCardEditorScreen(store)
}
}
}
- 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))
- }
- }
-
/**
* Close the keyboard, any open dialogs or menus and then reauthenticate if the
* 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
- }
}