From b7021c8e0be3fd9b063f93a1a19e5d153e8c1bb0 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Mon, 28 Aug 2023 20:36:09 +0000 Subject: [PATCH] Added UI tests for FingerprintEnrollIntro Test: m -j40 RunSettingsRoboTests ROBOTEST_FILTER=FingerprintEnrollmentIntroFragmentTest Bug: 295206367 Change-Id: I70f6b50dd2604e01805df04ffb1c07a9134ba065 --- .../fingerprint_v2_enroll_introduction.xml | 317 +++++++++--------- .../FingerprintEnrollmentV2Activity.kt | 7 +- .../FingerprintEnrollIntroV2Fragment.kt | 119 ++++--- .../FingerprintEnrolllNavigationViewModel.kt | 11 - tests/robotests/Android.bp | 2 + .../FingerprintEnrollIntroFragmentTest.kt | 173 ++++++++++ 6 files changed, 414 insertions(+), 215 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt diff --git a/res/layout/fingerprint_v2_enroll_introduction.xml b/res/layout/fingerprint_v2_enroll_introduction.xml index e9dd08ad7ca..2fd1f9c84ee 100644 --- a/res/layout/fingerprint_v2_enroll_introduction.xml +++ b/res/layout/fingerprint_v2_enroll_introduction.xml @@ -16,199 +16,210 @@ --> - - + android:layout_height="match_parent"> - - - - - - - - - - - + android:layout_height="wrap_content" /> + + + + + + + + android:orientation="vertical"> - - + - + android:text="@string/security_settings_fingerprint_v2_enroll_introduction_footer_title_2" /> - - - - - - + android:orientation="horizontal"> - - + - + - - - + + + - + android:orientation="horizontal"> - - + - + - - + + + + + + - + android:orientation="horizontal"> - + - - + + + + + + - + android:text="@string/security_settings_face_enroll_introduction_info_title" /> - - - - - + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + + + - - \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt index 31afcb7e05a..2565ab815aa 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt @@ -42,6 +42,7 @@ import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED import com.android.settings.biometrics.GatekeeperPasswordProvider import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment @@ -82,6 +83,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { private lateinit var accessibilityViewModel: AccessibilityViewModel private lateinit var foldStateViewModel: FoldStateViewModel private lateinit var orientationStateViewModel: OrientationStateViewModel + private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel private val coroutineDispatcher = Dispatchers.Default /** Result listener for ChooseLock activity flow. */ @@ -210,8 +212,9 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { )[FingerprintEnrollViewModel::class.java] // Initialize scroll view model - ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[ - FingerprintScrollViewModel::class.java] + fingerprintScrollViewModel = + ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[ + FingerprintScrollViewModel::class.java] // Initialize AccessibilityViewModel accessibilityViewModel = diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt index dbf6d128c0d..898b158aa25 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt @@ -25,10 +25,13 @@ import android.os.Bundle import android.text.Html import android.text.method.LinkMovementMethod import android.util.Log +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.ScrollView import android.widget.TextView +import androidx.annotation.VisibleForTesting import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -72,48 +75,69 @@ private data class TextModel( * 2. How the data will be stored * 3. How the user can access and remove their data */ -class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_introduction) { - private lateinit var footerBarMixin: FooterBarMixin - private lateinit var textModel: TextModel - private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel - private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel - private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel - private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel +class FingerprintEnrollIntroV2Fragment() : Fragment(R.layout.fingerprint_v2_enroll_introduction) { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - navigationViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java] - fingerprintEnrollViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java] - fingerprintScrollViewModel = - ViewModelProvider(requireActivity())[FingerprintScrollViewModel::class.java] - gateKeeperViewModel = - ViewModelProvider(requireActivity())[FingerprintGatekeeperViewModel::class.java] + /** Used for testing purposes */ + private var factory: ViewModelProvider.Factory? = null + + @VisibleForTesting + constructor(theFactory: ViewModelProvider.Factory) : this() { + factory = theFactory } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + private val viewModelProvider: ViewModelProvider by lazy { + if (factory != null) { + ViewModelProvider(requireActivity(), factory!!) + } else { + ViewModelProvider(requireActivity()) + } + } - lifecycleScope.launch { - combine( - navigationViewModel.enrollType, - fingerprintEnrollViewModel.sensorType, - ) { enrollType, sensorType -> - Pair(enrollType, sensorType) - } - .collect { (enrollType, sensorType) -> - textModel = - when (enrollType) { - Unicorn -> getUnicornTextModel() - else -> getNormalTextModel() - } + private lateinit var footerBarMixin: FooterBarMixin + private lateinit var textModel: TextModel - setupFooterBarAndScrollView(view) + // Note that the ViewModels cannot be requested before the onCreate call + private val navigationViewModel: FingerprintEnrollNavigationViewModel by lazy { + viewModelProvider[FingerprintEnrollNavigationViewModel::class.java] + } + private val fingerprintViewModel: FingerprintEnrollViewModel by lazy { + viewModelProvider[FingerprintEnrollViewModel::class.java] + } + private val fingerprintScrollViewModel: FingerprintScrollViewModel by lazy { + viewModelProvider[FingerprintScrollViewModel::class.java] + } + private val gateKeeperViewModel: FingerprintGatekeeperViewModel by lazy { + viewModelProvider[FingerprintGatekeeperViewModel::class.java] + } - if (savedInstanceState == null) { - getLayout()?.setHeaderText(textModel.headerText) - getLayout()?.setDescriptionText(textModel.descriptionText) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = + super.onCreateView(inflater, container, savedInstanceState).also { theView -> + val view = theView!! + + viewLifecycleOwner.lifecycleScope.launch { + combine( + navigationViewModel.enrollType, + fingerprintViewModel.sensorType, + ) { enrollType, sensorType -> + Pair(enrollType, sensorType) + } + .collect { (enrollType, sensorType) -> + textModel = + when (enrollType) { + Unicorn -> getUnicornTextModel() + else -> getNormalTextModel() + } + + setupFooterBarAndScrollView(view) + + val layout = view as GlifLayout + + layout.setHeaderText(textModel.headerText) + layout.setDescriptionText(textModel.descriptionText) // Set color filter for the following icons. val colorFilter = getIconColorFilter() @@ -158,9 +182,9 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll view.requireViewById(R.id.footer_title_1).setText(textModel.footerTitleOne) view.requireViewById(R.id.footer_title_2).setText(textModel.footerTitleOne) } - } + } + return view } - } private fun setFooterLink(view: View) { val footerLink: TextView = view.requireViewById(R.id.footer_learn_more) @@ -185,17 +209,18 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll navigationViewModel.nextStep() } - val layout: GlifLayout = requireActivity().requireViewById(R.id.setup_wizard_layout) + val layout: GlifLayout = view.findViewById(R.id.setup_wizard_layout)!! footerBarMixin = layout.getMixin(FooterBarMixin::class.java) footerBarMixin.primaryButton = - FooterButton.Builder(requireActivity()) + FooterButton.Builder(requireContext()) .setText(R.string.security_settings_face_enroll_introduction_more) .setListener(onNextButtonClick) .setButtonType(FooterButton.ButtonType.OPT_IN) .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) .build() + footerBarMixin.setSecondaryButton( - FooterButton.Builder(requireActivity()) + FooterButton.Builder(requireContext()) .setText(textModel.negativeButton) .setListener({ Log.d(TAG, "prevClicked") }) .setButtonType(FooterButton.ButtonType.NEXT) @@ -211,8 +236,8 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll val requireScrollMixin = layout.getMixin(RequireScrollMixin::class.java) requireScrollMixin.requireScrollWithButton( - requireActivity(), - footerBarMixin.primaryButton, + requireContext(), + primaryButton, R.string.security_settings_face_enroll_introduction_more, onNextButtonClick ) @@ -224,7 +249,7 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll } } - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { fingerprintScrollViewModel.hasReadConsentScreen.collect { consented -> if (consented) { primaryButton.setText( @@ -244,7 +269,7 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll // the flow. For instance if someone launches the activity with an invalid challenge, it // either 1) Fails or 2) Launched confirmDeviceCredential primaryButton.isEnabled = false - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { gateKeeperViewModel.hasValidGatekeeperInfo.collect { primaryButton.isEnabled = it } } } @@ -284,8 +309,4 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll PorterDuff.Mode.SRC_IN ) } - - private fun getLayout(): GlifLayout? { - return requireView().findViewById(R.id.setup_wizard_layout) as GlifLayout? - } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt index d2bb3212122..97c8271a73d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt @@ -31,11 +31,6 @@ import kotlinx.coroutines.launch private const val TAG = "FingerprintEnrollNavigationViewModel" -/** Interface to validate a gatekeeper hat */ -interface Validator { - fun validateGateKeeper(challenge: Long?): Boolean -} - /** * The [EnrollType] for fingerprint enrollment indicates information on how the flow should behave. */ @@ -56,7 +51,6 @@ object Unicorn : EnrollType() */ class FingerprintEnrollNavigationViewModel( private val dispatcher: CoroutineDispatcher, - private val validator: Validator, private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val gatekeeperViewModel: FingerprintGatekeeperViewModel, private val canSkipConfirm: Boolean @@ -145,11 +139,6 @@ class FingerprintEnrollNavigationViewModel( return FingerprintEnrollNavigationViewModel( backgroundDispatcher, - object : Validator { - override fun validateGateKeeper(challenge: Long?): Boolean { - return challenge != null - } - }, fingerprintManagerInteractor, fingerprintGatekeeperViewModel, canSkipConfirm, diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp index fbfd888ad51..1fd4f252f2e 100644 --- a/tests/robotests/Android.bp +++ b/tests/robotests/Android.bp @@ -25,6 +25,8 @@ android_app { "androidx.fragment_fragment-testing", "frameworks-base-testutils", "androidx.fragment_fragment", + "androidx.lifecycle_lifecycle-runtime-testing", + "kotlinx_coroutines_test", ], aaptflags: ["--extra-packages com.android.settings"], diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt new file mode 100644 index 00000000000..cea6676bc20 --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint2.fragment + +import android.content.Context +import android.os.Bundle +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.runner.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo +import com.android.settings.testutils2.FakeFingerprintManagerInteractor +import com.google.android.setupdesign.GlifLayout +import com.google.android.setupdesign.template.RequireScrollMixin +import kotlinx.coroutines.test.StandardTestDispatcher +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FingerprintEnrollIntroFragmentTest { + private var context: Context = ApplicationProvider.getApplicationContext() + private var interactor = FakeFingerprintManagerInteractor() + + private val gatekeeperViewModel = + FingerprintGatekeeperViewModel( + GatekeeperInfo.GatekeeperPasswordInfo(byteArrayOf(1, 2, 3), 100L), + interactor + ) + private val backgroundDispatcher = StandardTestDispatcher() + private lateinit var fragmentScenario: FragmentScenario + + private val navigationViewModel = + FingerprintEnrollNavigationViewModel( + backgroundDispatcher, + interactor, + gatekeeperViewModel, + canSkipConfirm = true, + ) + private var fingerprintViewModel = FingerprintEnrollViewModel(interactor, backgroundDispatcher) + private var fingerprintScrollViewModel = FingerprintScrollViewModel() + + @Before + fun setup() { + val factory = + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return when (modelClass) { + FingerprintEnrollViewModel::class.java -> fingerprintViewModel + FingerprintScrollViewModel::class.java -> fingerprintScrollViewModel + FingerprintEnrollNavigationViewModel::class.java -> navigationViewModel + FingerprintGatekeeperViewModel::class.java -> gatekeeperViewModel + else -> null + } + as T + } + } + + fragmentScenario = + launchFragmentInContainer(Bundle(), R.style.SudThemeGlif) { + FingerprintEnrollIntroV2Fragment(factory) + } + } + + @Test + fun testScrollToBottomButtonChangesText() { + fragmentScenario.onFragment { fragment -> + onView(withText("I agree")).check(doesNotExist()) + val someView = (fragment.requireView().findViewById(R.id.setup_wizard_layout))!! + val scrollMixin = someView.getMixin(RequireScrollMixin::class.java)!! + val listener = scrollMixin.onRequireScrollStateChangedListener + // This actually changes the button text + listener.onRequireScrollStateChanged(false) + + onView(withText("I agree")).check(matches(isDisplayed())) + } + } + + @Test + fun testBasicTitle() { + onView(withText(R.string.security_settings_fingerprint_enroll_introduction_title)) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } + + @Test + fun testFooterMessageTwo() { + onView(withId(R.id.footer_message_2)) + .check( + matches( + withText( + context.getString( + (R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_2) + ) + ) + ) + ) + } + + @Test + fun testFooterMessageThree() { + onView(withId(R.id.footer_message_3)) + .check( + matches( + withText( + context.getString( + (R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_3) + ) + ) + ) + ) + } + + @Test + fun testFooterMessageFour() { + onView(withId(R.id.footer_message_4)) + .check( + matches( + withText( + context.getString( + (R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_4) + ) + ) + ) + ) + } + + @Test + fun testFooterMessageFive() { + onView(withId(R.id.footer_message_5)) + .check( + matches( + withText( + context.getString( + (R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5) + ) + ) + ) + ) + } +}