For #24849 - Add an address editor screen

pull/543/head
Gabriel Luong 2 years ago committed by mergify[bot]
parent 808ce5a564
commit 25ffce77c0

@ -0,0 +1,52 @@
/* 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.address
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import org.mozilla.fenix.R
import org.mozilla.fenix.SecureFragment
import org.mozilla.fenix.databinding.FragmentAddressEditorBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.address.controller.DefaultAddressEditorController
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
import org.mozilla.fenix.settings.address.interactor.DefaultAddressEditorInteractor
import org.mozilla.fenix.settings.address.view.AddressEditorView
/**
* Displays an address editor for adding and editing an address.
*/
class AddressEditorFragment : SecureFragment(R.layout.fragment_address_editor) {
private lateinit var addressEditorView: AddressEditorView
private lateinit var interactor: AddressEditorInteractor
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val storage = requireContext().components.core.autofillStorage
interactor = DefaultAddressEditorInteractor(
controller = DefaultAddressEditorController(
storage = storage,
lifecycleScope = lifecycleScope,
navController = findNavController()
)
)
val binding = FragmentAddressEditorBinding.bind(view)
addressEditorView = AddressEditorView(binding, interactor)
addressEditorView.bind()
}
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.addresses_add_address))
}
}

@ -0,0 +1,60 @@
/* 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.address.controller
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.storage.UpdatableAddressFields
import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
import org.mozilla.fenix.settings.address.AddressEditorFragment
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
/**
* [AddressEditorFragment] controller. An interface that handles the view manipulation of the
* credit card editor.
*/
interface AddressEditorController {
/**
* @see [AddressEditorInteractor.onCancelButtonClicked]
*/
fun handleCancelButtonClicked()
/**
* @see [AddressEditorInteractor.onSaveAddress]
*/
fun handleSaveAddress(addressFields: UpdatableAddressFields)
}
/**
* The default implementation of [AddressEditorController].
*
* @param storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving
* addresses.
* @param lifecycleScope [CoroutineScope] scope to launch coroutines.
* @param navController [NavController] used for navigation.
*/
class DefaultAddressEditorController(
private val storage: AutofillCreditCardsAddressesStorage,
private val lifecycleScope: CoroutineScope,
private val navController: NavController,
) : AddressEditorController {
override fun handleCancelButtonClicked() {
navController.popBackStack()
}
override fun handleSaveAddress(addressFields: UpdatableAddressFields) {
lifecycleScope.launch {
storage.addAddress(addressFields)
lifecycleScope.launch(Dispatchers.Main) {
navController.popBackStack()
}
}
}
}

@ -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.settings.address.interactor
import mozilla.components.concept.storage.UpdatableAddressFields
import org.mozilla.fenix.settings.address.controller.AddressEditorController
/**
* Interface for the address editor interactor.
*/
interface AddressEditorInteractor {
/**
* Navigates back to the autofill preference settings. Called when a user taps on the
* "Cancel" button.
*/
fun onCancelButtonClicked()
/**
* Saves the provided address field into the autofill storage. Called when a user
* taps on the save menu item or "Save" button.
*
* @param addressFields A [UpdatableAddressFields] record to add.
*/
fun onSaveAddress(addressFields: UpdatableAddressFields)
}
/**
* The default implementation of [AddressEditorInteractor].
*
* @param controller An instance of [AddressEditorController] which will be delegated for all
* user interactions.
*/
class DefaultAddressEditorInteractor(
private val controller: AddressEditorController
) : AddressEditorInteractor {
override fun onCancelButtonClicked() {
controller.handleCancelButtonClicked()
}
override fun onSaveAddress(addressFields: UpdatableAddressFields) {
controller.handleSaveAddress(addressFields)
}
}

@ -0,0 +1,61 @@
/* 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.address.view
import mozilla.components.concept.storage.UpdatableAddressFields
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.databinding.FragmentAddressEditorBinding
import org.mozilla.fenix.ext.placeCursorAtEnd
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
/**
* Shows an address editor for adding or updating an address.
*/
class AddressEditorView(
private val binding: FragmentAddressEditorBinding,
private val interactor: AddressEditorInteractor
) {
/**
* Binds the view.
*/
fun bind() {
binding.fullNameInput.apply {
requestFocus()
placeCursorAtEnd()
showKeyboard()
}
binding.cancelButton.setOnClickListener {
interactor.onCancelButtonClicked()
}
binding.saveButton.setOnClickListener {
saveAddress()
}
}
internal fun saveAddress() {
binding.root.hideKeyboard()
interactor.onSaveAddress(
UpdatableAddressFields(
givenName = binding.fullNameInput.text.toString(),
additionalName = "",
familyName = "",
organization = "",
streetAddress = binding.streetAddressInput.text.toString(),
addressLevel3 = "",
addressLevel2 = "",
addressLevel1 = "",
postalCode = binding.zipInput.text.toString(),
country = "",
tel = binding.phoneInput.toString(),
email = binding.emailInput.toString()
)
)
}
}

@ -110,7 +110,7 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() {
consumeFrom(store) { state ->
if (requireComponents.settings.addressFeature) {
updateAddressPreference(state.addresses.isNotEmpty())
updateAddressPreference(state.addresses.isNotEmpty(), findNavController())
}
updateCardManagementPreference(state.creditCards.isNotEmpty(), findNavController())
}
@ -160,7 +160,10 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() {
* Updates preferences visibility depending on addresses being already saved or not.
*/
@VisibleForTesting
internal fun updateAddressPreference(hasAddresses: Boolean) {
internal fun updateAddressPreference(
hasAddresses: Boolean,
navController: NavController,
) {
val manageAddressesPreference =
requirePreference<Preference>(R.string.pref_key_addresses_manage_addresses)
@ -173,6 +176,14 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() {
manageAddressesPreference.title =
getString(R.string.preferences_addresses_add_address)
}
manageAddressesPreference.setOnPreferenceClickListener {
navController.navigate(
AutofillSettingFragmentDirections
.actionAutofillSettingFragmentToAddressEditorFragment()
)
super.onPreferenceTreeClick(it)
}
}
/**

@ -0,0 +1,371 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<!-- Full Name -->
<TextView
android:id="@+id/full_name_title"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_full_name"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/card_number_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/full_name_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
app:errorEnabled="true"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/full_name_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/full_name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Street Address -->
<TextView
android:id="@+id/street_address_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_street_address"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/street_address_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/full_name_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/street_address_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/street_address_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/street_address_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- City -->
<TextView
android:id="@+id/city_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_city"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/city_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/street_address_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/city_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/city_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/city_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- State -->
<TextView
android:id="@+id/state_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_state"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/state_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/city_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/state_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
app:hintEnabled="false"
app:layout_constraintHorizontal_bias="0.8"
app:layout_constraintEnd_toStartOf="@+id/zip_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/state_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/state_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Zip -->
<TextView
android:id="@+id/zip_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_zip"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/zip_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="@+id/zip_layout"
app:layout_constraintTop_toBottomOf="@+id/city_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/zip_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:textColor="?attr/textPrimary"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/state_layout"
app:layout_constraintTop_toBottomOf="@+id/zip_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/zip_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Phone -->
<TextView
android:id="@+id/phone_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_phone"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/phone_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/state_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/phone_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phone_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/phone_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Email -->
<TextView
android:id="@+id/email_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:letterSpacing="0.05"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:text="@string/addresses_email"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/city_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phone_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/email_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/email_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/email_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:imeOptions="flagNoExtractUi"
android:letterSpacing="0.01"
android:lineSpacingExtra="8sp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?attr/textPrimary"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:letterSpacing="0"
android:padding="10dp"
android:text="@string/addressess_delete_address_button"
android:textAllCaps="false"
android:textColor="@color/fx_mobile_text_color_warning"
android:visibility="gone"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintTop_toBottomOf="@+id/email_layout"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:letterSpacing="0"
android:padding="10dp"
android:text="@string/addresses_cancel_button"
android:textAllCaps="false"
android:textColor="?attr/textPrimary"
android:textStyle="bold"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintTop_toBottomOf="@+id/email_layout"
app:layout_constraintEnd_toStartOf="@id/save_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/save_button"
style="@style/NeutralButton"
android:layout_width="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/addresses_save_button"
app:layout_constraintTop_toTopOf="@+id/cancel_button"
app:layout_constraintBottom_toBottomOf="@+id/cancel_button"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

@ -1206,6 +1206,13 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_autofillSettingFragment_to_addressEditorFragment"
app:destination="@id/addressEditorFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/creditCardEditorFragment"
@ -1229,5 +1236,9 @@
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/addressEditorFragment"
android:name="org.mozilla.fenix.settings.address.AddressEditorFragment"
android:label="@string/addresses_add_address" />
</navigation>
</navigation>

@ -1551,6 +1551,28 @@
<string name="credit_cards_biometric_prompt_message_pin">Unlock your device</string>
<!-- Message displayed in biometric prompt for authentication, before allowing users to use their stored credit card information -->
<string name="credit_cards_biometric_prompt_unlock_message">Unlock to use stored credit card information</string>
<!-- Title of the "Add address" screen -->
<string name="addresses_add_address">Add address</string>
<!-- The header for the full name of an address -->
<string name="addresses_full_name">Full Name</string>
<!-- The header for the street address of an address -->
<string name="addresses_street_address">Street Address</string>
<!-- The header for the city of an address -->
<string name="addresses_city">City</string>
<!-- The header for the state of an address -->
<string name="addresses_state">State</string>
<!-- The header for the zip code of an address -->
<string name="addresses_zip">Zip</string>
<!-- The header for the phone number of an address -->
<string name="addresses_phone">Phone</string>
<!-- The header for the email of an address -->
<string name="addresses_email">Email</string>
<!-- The text for the "Save" button for saving an address -->
<string name="addresses_save_button">Save</string>
<!-- The text for the "Cancel" button for cancelling adding, updating or deleting an address -->
<string name="addresses_cancel_button">Cancel</string>
<!-- The text for the "Delete address" button for deleting an address -->
<string name="addressess_delete_address_button">Delete address</string>
<!-- Title of the Add search engine screen -->
<string name="search_engine_add_custom_search_engine_title">Add search engine</string>

@ -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.settings.address
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddressEditorBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
import org.mozilla.fenix.settings.address.view.AddressEditorView
@RunWith(FenixRobolectricTestRunner::class)
class AddressEditorViewTest {
private lateinit var view: View
private lateinit var interactor: AddressEditorInteractor
private lateinit var addressEditorView: AddressEditorView
private lateinit var binding: FragmentAddressEditorBinding
@Before
fun setup() {
view = LayoutInflater.from(testContext).inflate(R.layout.fragment_address_editor, null)
binding = FragmentAddressEditorBinding.bind(view)
interactor = mockk(relaxed = true)
addressEditorView = spyk(AddressEditorView(binding, interactor))
}
@Test
fun `WHEN the cancel button is clicked THEN interactor is called`() {
addressEditorView.bind()
binding.cancelButton.performClick()
verify { interactor.onCancelButtonClicked() }
}
}

@ -0,0 +1,76 @@
/* 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.address.controller
import androidx.navigation.NavController
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.concept.storage.UpdatableAddressFields
import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class DefaultAddressEditorControllerTest {
private val storage: AutofillCreditCardsAddressesStorage = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val testCoroutineScope = TestCoroutineScope()
private lateinit var controller: DefaultAddressEditorController
@Before
fun setup() {
controller = spyk(
DefaultAddressEditorController(
storage = storage,
lifecycleScope = testCoroutineScope,
navController = navController,
)
)
}
@Test
fun `WHEN cancel button is clicked THEN pop the NavController back stack`() {
controller.handleCancelButtonClicked()
verify {
navController.popBackStack()
}
}
@Test
fun `GIVEN a new address record WHEN save address is called THEN save the new address record to storage`() = testCoroutineScope.runBlockingTest {
val addressFields = UpdatableAddressFields(
givenName = "John",
additionalName = "",
familyName = "Smith",
organization = "Mozilla",
streetAddress = "123 Sesame Street",
addressLevel3 = "",
addressLevel2 = "",
addressLevel1 = "",
postalCode = "90210",
country = "US",
tel = "+1 519 555-5555",
email = "foo@bar.com"
)
controller.handleSaveAddress(addressFields)
coVerify {
storage.addAddress(addressFields)
navController.popBackStack()
}
}
}

@ -0,0 +1,52 @@
/* 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.address.interactor
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.concept.storage.UpdatableAddressFields
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.settings.address.controller.AddressEditorController
class DefaultAddressEditorInteractorTest {
private val controller: AddressEditorController = mockk(relaxed = true)
private lateinit var interactor: AddressEditorInteractor
@Before
fun setup() {
interactor = DefaultAddressEditorInteractor(controller)
}
@Test
fun `WHEN cancel button is clicked THEN forward to controller handler`() {
interactor.onCancelButtonClicked()
verify { controller.handleCancelButtonClicked() }
}
@Test
fun `WHEN save button is clicked THEN forward to controller handler`() {
val addressFields = UpdatableAddressFields(
givenName = "John",
additionalName = "",
familyName = "Smith",
organization = "Mozilla",
streetAddress = "123 Sesame Street",
addressLevel3 = "",
addressLevel2 = "",
addressLevel1 = "",
postalCode = "90210",
country = "US",
tel = "+1 519 555-5555",
email = "foo@bar.com"
)
interactor.onSaveAddress(addressFields)
verify { controller.handleSaveAddress(addressFields) }
}
}

@ -103,7 +103,7 @@ class AutofillSettingFragmentTest {
fun `GIVEN the list of addresses is not empty WHEN fragment is displayed THEN the manage addresses preference label is 'Manage addresses'`() {
val preferenceTitle =
testContext.getString(R.string.preferences_addresses_manage_addresses)
val manageCardsPreference = autofillSettingFragment.findPreference<Preference>(
val manageAddressesPreference = autofillSettingFragment.findPreference<Preference>(
autofillSettingFragment.getPreferenceKey(R.string.pref_key_addresses_manage_addresses)
)
@ -113,18 +113,28 @@ class AutofillSettingFragmentTest {
val store = AutofillFragmentStore(state)
autofillSettingFragment.updateAddressPreference(
store.state.addresses.isNotEmpty()
store.state.addresses.isNotEmpty(),
navController
)
assertNull(manageCardsPreference?.icon)
assertEquals(preferenceTitle, manageCardsPreference?.title)
assertNull(manageAddressesPreference?.icon)
assertEquals(preferenceTitle, manageAddressesPreference?.title)
manageAddressesPreference?.performClick()
verify {
navController.navigate(
AutofillSettingFragmentDirections
.actionAutofillSettingFragmentToAddressEditorFragment()
)
}
}
@Test
fun `GIVEN the list of addresses is empty WHEN fragment is displayed THEN the manage addresses preference label is 'Add address'`() {
val preferenceTitle =
testContext.getString(R.string.preferences_addresses_add_address)
val manageCardsPreference = autofillSettingFragment.findPreference<Preference>(
val manageAddressesPreference = autofillSettingFragment.findPreference<Preference>(
autofillSettingFragment.getPreferenceKey(R.string.pref_key_addresses_manage_addresses)
)
@ -132,10 +142,20 @@ class AutofillSettingFragmentTest {
val store = AutofillFragmentStore(state)
autofillSettingFragment.updateAddressPreference(
store.state.addresses.isNotEmpty()
store.state.addresses.isNotEmpty(),
navController
)
assertNotNull(manageCardsPreference?.icon)
assertEquals(preferenceTitle, manageCardsPreference?.title)
assertNotNull(manageAddressesPreference?.icon)
assertEquals(preferenceTitle, manageAddressesPreference?.title)
manageAddressesPreference?.performClick()
verify {
navController.navigate(
AutofillSettingFragmentDirections
.actionAutofillSettingFragmentToAddressEditorFragment()
)
}
}
}

Loading…
Cancel
Save