// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.ui.fast_checkout;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.CREDIT_CARD_MODEL_LIST;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DETAIL_SCREEN_BACK_CLICK_HANDLER;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DETAIL_SCREEN_MODEL_LIST;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DETAIL_SCREEN_SETTINGS_CLICK_HANDLER;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.PROFILE_MODEL_LIST;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.FeatureList;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DetailItemType;
import org.chromium.chrome.browser.ui.fast_checkout.data.FastCheckoutAutofillProfile;
import org.chromium.chrome.browser.ui.fast_checkout.data.FastCheckoutCreditCard;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.AutofillProfileItemProperties;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.CreditCardItemProperties;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.DetailScreenCoordinator;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.FooterItemProperties;
import org.chromium.chrome.browser.ui.suggestion.Icon;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
/** Simple unit tests for the detail screen view. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@DisableFeatures(ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES)
@CommandLineFlags.Add({
ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
ChromeSwitches.DISABLE_NATIVE_INITIALIZATION
})
public class FastCheckoutDetailScreenViewTest {
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
new ActivityScenarioRule<>(TestActivity.class);
@Mock private Runnable mBackClickHandler;
@Mock private Runnable mSettingsClickHandler;
@Mock private BottomSheetController mBottomSheetController;
private View mView;
private PropertyModel mModel;
private static final FastCheckoutAutofillProfile sSampleProfile1 =
FastCheckoutTestUtils.createDetailedProfile(
/* guid= */ "111",
/* name= */ "John Moe",
/* streetAddress= */ "Park Avenue 234",
/* city= */ "New York",
/* postalCode= */ "12345",
/* email= */ "[email protected]",
/* phoneNumber= */ "+1-345-543-645");
private static final FastCheckoutAutofillProfile sSampleProfile2 =
FastCheckoutTestUtils.createDetailedProfile(
/* guid= */ "555",
/* name= */ "Jane Doe",
/* streetAddress= */ "Sunset Blvd. 456",
/* city= */ "Los Angeles",
/* postalCode= */ "99999",
/* email= */ "[email protected]",
/* phoneNumber= */ "+1-345-333-319");
private static final FastCheckoutCreditCard sSampleCard1 =
FastCheckoutTestUtils.createDetailedLocalCreditCard(
/* guid= */ "123",
/* origin= */ "https://example.com",
/* name= */ "John Moe",
/* number= */ "75675675656",
/* obfuscatedNumber= */ "5656",
/* month= */ "05",
/* year= */ "2031",
/* issuerIcon= */ Icon.CARD_VISA);
private static final FastCheckoutCreditCard sSampleCard2 =
FastCheckoutTestUtils.createDetailedLocalCreditCard(
/* guid= */ "154",
/* origin= */ "https://example.fr",
/* name= */ "Jane Doe",
/* number= */ "4564565541234",
/* obfuscatedNumber= */ "1234",
/* month= */ "10",
/* year= */ "2025",
/* issuerIcon= */ Icon.CARD_DINERS);
@Before
public void setUp() {
FeatureList.TestValues featureTestValues = new FeatureList.TestValues();
featureTestValues.addFeatureFlagOverride(
ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES, false);
FeatureList.setTestValues(featureTestValues);
mActivityScenarioRule
.getScenario()
.onActivity(
activity -> {
mModel = FastCheckoutProperties.createDefaultModel();
mModel.set(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER, mSettingsClickHandler);
mModel.set(DETAIL_SCREEN_BACK_CLICK_HANDLER, mBackClickHandler);
// Create the view.
mView =
LayoutInflater.from(activity)
.inflate(
R.layout.fast_checkout_detail_screen_sheet,
null);
// Let the coordinator connect model and view.
new DetailScreenCoordinator(
activity, mView, mModel, mBottomSheetController);
activity.setContentView(mView);
assertNotNull(mView);
});
}
@Test
@SmallTest
public void testBackArrowClickCallsHandler() {
assertNotNull(mView);
ImageButton backImageButton =
mView.findViewById(R.id.fast_checkout_toolbar_back_image_button);
assertNotNull(backImageButton);
backImageButton.performClick();
ShadowLooper.shadowMainLooper().idle();
verify(mBackClickHandler).run();
}
@Test
@SmallTest
public void testOpenSettingsClickCallsHandler() {
// Click on the settings element.
View settingsImageButton =
mView.findViewById(R.id.fast_checkout_toolbar_settings_image_button);
assertNotNull(settingsImageButton);
settingsImageButton.performClick();
ShadowLooper.shadowMainLooper().idle();
verify(mSettingsClickHandler).run();
}
@Test
@SmallTest
public void testRecyclerViewPopulatesProfileItemEntriesAndReactsToClicks() {
Runnable callback1 = mock(Runnable.class);
Runnable callback2 = mock(Runnable.class);
ModelList models = mModel.get(PROFILE_MODEL_LIST);
models.add(
new ListItem(
DetailItemType.PROFILE,
AutofillProfileItemProperties.create(
sSampleProfile1,
/* isSelected= */ false,
/* onClickListener= */ callback1)));
models.add(
new ListItem(
DetailItemType.PROFILE,
AutofillProfileItemProperties.create(
sSampleProfile2,
/* isSelected= */ true,
/* onClickListener= */ callback2)));
mModel.set(DETAIL_SCREEN_MODEL_LIST, models);
// Check that the sheet is populated properly.
ShadowLooper.shadowMainLooper().idle();
assertThat(getListItems().getChildCount(), is(2));
// Check that clicks are handled properly.
getListItemAt(0).performClick();
ShadowLooper.shadowMainLooper().idle();
verify(callback1, times(1)).run();
verify(callback2, never()).run();
}
@Test
@SmallTest
public void testRecyclerViewBindsProfileDataToItemView() {
FastCheckoutAutofillProfile emptyFieldsProfile =
FastCheckoutTestUtils.createDetailedProfile(
/* guid= */ "222",
/* name= */ "",
/* streetAddress= */ "",
/* city= */ "",
/* postalCode= */ "",
/* email= */ "",
/* phoneNumber= */ "");
ModelList models = mModel.get(PROFILE_MODEL_LIST);
models.add(
new ListItem(
DetailItemType.PROFILE,
AutofillProfileItemProperties.create(
sSampleProfile1,
/* isSelected= */ true,
/* onClickListener= */ () -> {})));
models.add(
new ListItem(
DetailItemType.PROFILE,
AutofillProfileItemProperties.create(
emptyFieldsProfile,
/* isSelected= */ false,
/* onClickListener= */ () -> {})));
mModel.set(DETAIL_SCREEN_MODEL_LIST, models);
// Check that the sheet is populated properly.
ShadowLooper.shadowMainLooper().idle();
assertThat(getListItems().getAdapter().getItemCount(), is(2));
assertThatProfileItemLayoutIsCorrectAt(0, sSampleProfile1, /* isSelected= */ true);
assertThatProfileItemLayoutIsCorrectAt(1, emptyFieldsProfile, /* isSelected= */ false);
// Update the selection.
models.get(0).model.set(AutofillProfileItemProperties.IS_SELECTED, false);
models.get(1).model.set(AutofillProfileItemProperties.IS_SELECTED, true);
ShadowLooper.shadowMainLooper().idle();
assertThatProfileItemLayoutIsCorrectAt(0, sSampleProfile1, /* isSelected= */ false);
assertThatProfileItemLayoutIsCorrectAt(1, emptyFieldsProfile, /* isSelected= */ true);
}
@Test
@SmallTest
public void testRecyclerViewBindsCreditCardDataToItemView() {
ModelList models = mModel.get(CREDIT_CARD_MODEL_LIST);
FastCheckoutCreditCard sampleCardNoName =
FastCheckoutTestUtils.createDetailedLocalCreditCard(
/* guid= */ "123",
/* origin= */ "https://example.at",
/* name= */ "",
/* number= */ "23423423432",
/* obfuscatedNumber= */ "34326",
/* month= */ "05",
/* year= */ "2035",
/* issuerIcon= */ Icon.CARD_VISA);
FastCheckoutCreditCard sampleCardEmptyFields =
FastCheckoutTestUtils.createDetailedLocalCreditCard(
/* guid= */ "7534",
/* origin= */ "",
/* name= */ "",
/* number= */ "",
/* obfuscatedNumber= */ "",
/* month= */ "05",
/* year= */ "2035",
/* issuerIcon= */ Icon.CARD_VISA);
models.add(
new ListItem(
DetailItemType.CREDIT_CARD,
CreditCardItemProperties.create(
sSampleCard1,
/* isSelected= */ true,
/* onClickListener= */ () -> {})));
models.add(
new ListItem(
DetailItemType.CREDIT_CARD,
CreditCardItemProperties.create(
sampleCardNoName,
/* isSelected= */ false,
/* onClickListener= */ () -> {})));
models.add(
new ListItem(
DetailItemType.CREDIT_CARD,
CreditCardItemProperties.create(
sampleCardEmptyFields,
/* isSelected= */ false,
/* onClickListener= */ () -> {})));
mModel.set(DETAIL_SCREEN_MODEL_LIST, models);
// Check that the sheet is populated properly.
ShadowLooper.shadowMainLooper().idle();
assertThat(getListItems().getAdapter().getItemCount(), is(3));
assertThatCreditCardItemLayoutIsCorrectAt(0, sSampleCard1, /* isSelected= */ true);
assertThatCreditCardItemLayoutIsCorrectAt(1, sampleCardNoName, /* isSelected= */ false);
assertThatCreditCardItemLayoutIsCorrectAt(
2, sampleCardEmptyFields, /* isSelected= */ false);
// Update the selection.
models.get(0).model.set(CreditCardItemProperties.IS_SELECTED, false);
models.get(1).model.set(CreditCardItemProperties.IS_SELECTED, true);
models.get(2).model.set(CreditCardItemProperties.IS_SELECTED, false);
ShadowLooper.shadowMainLooper().idle();
assertThatCreditCardItemLayoutIsCorrectAt(0, sSampleCard1, /* isSelected= */ false);
assertThatCreditCardItemLayoutIsCorrectAt(1, sampleCardNoName, /* isSelected= */ true);
assertThatCreditCardItemLayoutIsCorrectAt(
2, sampleCardEmptyFields, /* isSelected= */ false);
}
@Test
@SmallTest
public void testRecyclerViewHandlesFooterItemCorrectly() {
Runnable callback = mock(Runnable.class);
ModelList models = mModel.get(PROFILE_MODEL_LIST);
models.add(
new ListItem(
DetailItemType.PROFILE,
AutofillProfileItemProperties.create(
sSampleProfile1,
/* isSelected= */ false,
/* onClickListener= */ () -> {
Assert.fail();
})));
models.add(
new ListItem(
DetailItemType.FOOTER,
FooterItemProperties.create(
/* label= */ R.string
.fast_checkout_detail_screen_add_autofill_profile_text,
/* onClickHandler= */ callback)));
mModel.set(DETAIL_SCREEN_MODEL_LIST, models);
// Check that the sheet is populated properly.
ShadowLooper.shadowMainLooper().idle();
assertThat(getListItems().getChildCount(), is(2));
// Check that the correct text is set for the footer item.
assertThat(
getTextFromListItemWithId(1, R.id.fast_checkout_add_new_item_label),
equalTo(
mView.getContext()
.getResources()
.getString(
R.string
.fast_checkout_detail_screen_add_autofill_profile_text)));
// Check that clicks are handled properly.
getListItemAt(1).performClick();
ShadowLooper.shadowMainLooper().idle();
verify(callback, times(1)).run();
}
private RecyclerView getListItems() {
return mView.findViewById(R.id.fast_checkout_detail_screen_recycler_view);
}
private View getListItemAt(int index) {
return getListItems().getChildAt(index);
}
/** Returns the text contained in the item with resId inside the item at this index. */
private String getTextFromListItemWithId(int index, int resId) {
TextView textView = getListItemAt(index).findViewById(resId);
return textView.getText().toString();
}
/** Returns the TextView with resId inside the item at this index. */
private TextView getTextViewFromListItemWithId(int index, int resId) {
return getListItemAt(index).findViewById(resId);
}
/** Asserts that the layout of the profile item at the given index is correct. */
private void assertThatProfileItemLayoutIsCorrectAt(
int index, FastCheckoutAutofillProfile profile, boolean isSelected) {
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(index, R.id.fast_checkout_autofill_profile_item_name),
profile.getFullName());
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(
index, R.id.fast_checkout_autofill_profile_item_street_address),
profile.getStreetAddress());
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(
index, R.id.fast_checkout_autofill_profile_item_city_and_postal_code),
getLocalityAndPostalCode(profile));
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(
index, R.id.fast_checkout_autofill_profile_item_country),
profile.getCountryName());
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(
index, R.id.fast_checkout_autofill_profile_item_email),
profile.getEmailAddress());
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(
index, R.id.fast_checkout_autofill_profile_item_phone_number),
profile.getPhoneNumber());
View icon =
getListItemAt(index)
.findViewById(R.id.fast_checkout_autofill_profile_item_selected_icon);
assertThat(icon.getVisibility(), is(isSelected ? View.VISIBLE : View.GONE));
}
/** Asserts that the layout of the credit card item at the given index is correct. */
private void assertThatCreditCardItemLayoutIsCorrectAt(
int index, FastCheckoutCreditCard card, boolean isSelected) {
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(index, R.id.fast_checkout_credit_card_item_number),
card.getObfuscatedNumber());
// The name row should get hidden if the name is empty.
assertViewShowsTextOrInvisible(
getListItemAt(index).findViewById(R.id.fast_checkout_credit_card_item_name),
card.getName());
assertViewShowsTextOrInvisible(
getTextViewFromListItemWithId(
index, R.id.fast_checkout_credit_card_item_expiration_date),
card.getMonth() + "/" + card.getYear());
View icon =
getListItemAt(index)
.findViewById(R.id.fast_checkout_credit_card_item_selected_icon);
assertThat(icon.getVisibility(), is(isSelected ? View.VISIBLE : View.GONE));
// Check that the icon is the correct one.
ImageView paymentIcon =
getListItemAt(index).findViewById(R.id.fast_checkout_credit_card_icon);
assertThat(
shadowOf(paymentIcon.getDrawable()).getCreatedFromResId(),
is(card.getIssuerIconDrawableId()));
}
private void assertViewShowsTextOrInvisible(TextView view, String text) {
if (text.isEmpty()) {
assertThat(view.getVisibility(), is(View.GONE));
} else {
assertThat(view.getVisibility(), is(View.VISIBLE));
assertThat(view.getText().toString(), equalTo(text));
}
}
/**
* Returns the properly formatted combination of city and postal code. For now, that means
* adhering to US formatting.
*/
private static String getLocalityAndPostalCode(FastCheckoutAutofillProfile profile) {
StringBuilder builder = new StringBuilder();
builder.append(profile.getLocality());
// Add divider only if both elements exist.
if (!profile.getLocality().isEmpty() && !profile.getPostalCode().isEmpty()) {
builder.append(", ");
}
builder.append(profile.getPostalCode());
return builder.toString();
}
}