chromium/chrome/browser/ui/android/fast_checkout/internal/junit/src/org/chromium/chrome/browser/ui/fast_checkout/FastCheckoutMediatorTest.java

// 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.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.CREDIT_CARD_MODEL_LIST;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.CURRENT_SCREEN;
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.DETAIL_SCREEN_SETTINGS_MENU_TITLE;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DETAIL_SCREEN_TITLE;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.HOME_SCREEN_DELEGATE;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.PROFILE_MODEL_LIST;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.SELECTED_CREDIT_CARD;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.SELECTED_PROFILE;
import static org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.VISIBLE;

import androidx.recyclerview.widget.RecyclerView;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.UserActionTester;
import org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DetailItemType;
import org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.ScreenType;
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.FooterItemProperties;
import org.chromium.chrome.browser.ui.fast_checkout.home_screen.HomeScreenCoordinator;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.List;

/**
 * Controller tests verify that the Fast Checkout controller modifies the model if the API is used
 * properly.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class FastCheckoutMediatorTest {
    private static final List<FastCheckoutAutofillProfile> DUMMY_PROFILES =
            List.of(
                    FastCheckoutTestUtils.createDetailedProfile(
                            /* guid= */ "123",
                            /* name= */ "John Moe",
                            /* streetAddress= */ "Park Avenue 234",
                            /* city= */ "New York",
                            /* postalCode= */ "12345",
                            /* email= */ "[email protected]",
                            /* phoneNumber= */ "+1-345-543-645"),
                    FastCheckoutTestUtils.createDetailedProfile(
                            /* guid= */ "234",
                            /* name= */ "Jane Doe",
                            /* streetAddress= */ "Sunset Blvd. 456",
                            /* city= */ "Los Angeles",
                            /* postalCode= */ "99999",
                            /* email= */ "[email protected]",
                            /* phoneNumber= */ "+1-345-333-319"),
                    FastCheckoutTestUtils.createDetailedProfile(
                            /* guid= */ "345",
                            /* name= */ "Foo Boo",
                            /* streetAddress= */ "Centennial Park",
                            /* city= */ "San Francisco",
                            /* postalCode= */ "23441",
                            /* email= */ "[email protected]",
                            /* phoneNumber= */ "+1-205-333-009"));
    private static final List<FastCheckoutCreditCard> DUMMY_CARDS =
            List.of(
                    FastCheckoutTestUtils.createDummyCreditCard(
                            "xyz", "https://example.com", "4111111111111111"),
                    FastCheckoutTestUtils.createDummyCreditCard(
                            "hfg", "https://example.co.uk", "4111111145454111"),
                    FastCheckoutTestUtils.createDummyCreditCard(
                            "iyul", "https://neverseenbefore.com", "411167568911"),
                    FastCheckoutTestUtils.createDummyCreditCard(
                            "iyul", "https://www.example.com", "4118102027996045"));

    @Mock RecyclerView mMockParentView;
    @Mock private FastCheckoutComponent.Delegate mMockDelegate;
    @Mock private BottomSheetContent mMockBottomSheetContent;
    @Mock private BottomSheetController mMockBottomSheetController;

    private FastCheckoutMediator mMediator = new FastCheckoutMediator();
    private UserActionTester mActionTester;

    private final PropertyModel mModel = FastCheckoutProperties.createDefaultModel();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mActionTester = new UserActionTester();
        mMediator.initialize(mMockDelegate, mModel, mMockBottomSheetController);
    }

    @After
    public void tearDown() {
        mActionTester.tearDown();
    }

    @Test
    public void testCreatesValidDefaultModel() {
        assertThat(mModel.get(VISIBLE), is(false));
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.HOME_SCREEN));
        assertThat(mModel.get(PROFILE_MODEL_LIST), instanceOf(ModelList.class));
        assertThat(mModel.get(PROFILE_MODEL_LIST).size(), is(0));
        assertThat(mModel.get(CREDIT_CARD_MODEL_LIST), instanceOf(ModelList.class));
        assertThat(mModel.get(CREDIT_CARD_MODEL_LIST).size(), is(0));

        // On top of that, the initialize method of the Mediator sets up delegates.
        assertNotNull(mModel.get(HOME_SCREEN_DELEGATE));
        assertThat(
                mModel.get(HOME_SCREEN_DELEGATE), instanceOf(HomeScreenCoordinator.Delegate.class));

        assertNotNull(mModel.get(DETAIL_SCREEN_BACK_CLICK_HANDLER));
        assertThat(mModel.get(DETAIL_SCREEN_BACK_CLICK_HANDLER), instanceOf(Runnable.class));
        assertActionRecorded(FastCheckoutUserActions.INITIALIZED);
    }

    @Test
    public void testSetCurrentScreenUpdatesModel() {
        mMediator.setCurrentScreen(ScreenType.AUTOFILL_PROFILE_SCREEN);
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.AUTOFILL_PROFILE_SCREEN));
        assertThat(
                mModel.get(DETAIL_SCREEN_TITLE),
                is(R.string.fast_checkout_autofill_profile_sheet_title));
        assertThat(
                mModel.get(DETAIL_SCREEN_SETTINGS_MENU_TITLE),
                is(R.string.fast_checkout_autofill_profile_settings_button_description));
        assertThat(mModel.get(DETAIL_SCREEN_MODEL_LIST), is(mModel.get(PROFILE_MODEL_LIST)));

        assertNotNull(mModel.get(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER));
        assertThat(mModel.get(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER), instanceOf(Runnable.class));

        mMediator.setCurrentScreen(ScreenType.CREDIT_CARD_SCREEN);
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.CREDIT_CARD_SCREEN));
        assertThat(
                mModel.get(DETAIL_SCREEN_TITLE),
                is(R.string.fast_checkout_credit_card_sheet_title));
        assertThat(
                mModel.get(DETAIL_SCREEN_SETTINGS_MENU_TITLE),
                is(R.string.fast_checkout_credit_card_settings_button_description));
        assertThat(mModel.get(DETAIL_SCREEN_MODEL_LIST), is(mModel.get(CREDIT_CARD_MODEL_LIST)));

        assertNotNull(mModel.get(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER));
        assertThat(mModel.get(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER), instanceOf(Runnable.class));
    }

    @Test
    public void testNavigateBackWorksFromAutofillProfileScreen() {
        mMediator.setCurrentScreen(ScreenType.AUTOFILL_PROFILE_SCREEN);
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.AUTOFILL_PROFILE_SCREEN));

        mModel.get(DETAIL_SCREEN_BACK_CLICK_HANDLER).run();
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.HOME_SCREEN));
        assertActionRecorded(FastCheckoutUserActions.NAVIGATED_BACK_HOME);
    }

    @Test
    public void testOpenSettingsWorksFromAutofillProfileScreen() {
        mMediator.setCurrentScreen(ScreenType.AUTOFILL_PROFILE_SCREEN);
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.AUTOFILL_PROFILE_SCREEN));

        mModel.get(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER).run();

        verify(mMockDelegate).openAutofillProfileSettings();
        assertActionRecorded(FastCheckoutUserActions.NAVIGATED_TO_ADDRESSES_SETTINGS_VIA_ICON);
    }

    @Test
    public void testOpenSettingsWorksFromCreditCardScreen() {
        mMediator.setCurrentScreen(ScreenType.CREDIT_CARD_SCREEN);
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.CREDIT_CARD_SCREEN));

        mModel.get(DETAIL_SCREEN_SETTINGS_CLICK_HANDLER).run();

        verify(mMockDelegate).openCreditCardSettings();
        assertActionRecorded(FastCheckoutUserActions.NAVIGATED_TO_CREDIT_CARDS_SETTINGS_VIA_ICON);
    }

    @Test
    public void testShowOptionsSetsVisible() {
        mMediator.showOptions(DUMMY_PROFILES, DUMMY_CARDS);

        verify(mMockBottomSheetController, never()).hideContent(any(), anyBoolean());
        assertThat(mModel.get(VISIBLE), is(true));
    }

    @Test
    public void testSetAutofillProfilesCreatesModels() {
        mMediator.setAutofillProfileItems(DUMMY_PROFILES);

        ModelList models = mModel.get(PROFILE_MODEL_LIST);
        // There is one extra item due to the footer.
        assertThat(models.size(), is(DUMMY_PROFILES.size() + 1));
        for (int index = 0; index < DUMMY_PROFILES.size(); ++index) {
            assertThat(models.get(index).type, is(DetailItemType.PROFILE));
            PropertyModel model = models.get(index).model;
            assertThat(
                    model.get(AutofillProfileItemProperties.AUTOFILL_PROFILE),
                    is(DUMMY_PROFILES.get(index)));
        }
        assertThat(models.get(DUMMY_PROFILES.size()).type, is(DetailItemType.FOOTER));
    }

    @Test
    public void testSetCreditCardsCreatesModels() {
        mMediator.setCreditCardItems(DUMMY_CARDS);

        ModelList models = mModel.get(CREDIT_CARD_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_CARDS.size() + 1));
        for (int index = 0; index < DUMMY_CARDS.size(); ++index) {
            assertThat(models.get(index).type, is(DetailItemType.CREDIT_CARD));
            PropertyModel model = models.get(index).model;
            assertThat(model.get(CreditCardItemProperties.CREDIT_CARD), is(DUMMY_CARDS.get(index)));
        }
    }

    @Test
    public void testSetAutofillProfileItemsUpdatesPreservesPriorSelection() {
        mMediator.setAutofillProfileItems(DUMMY_PROFILES);
        ModelList models = mModel.get(PROFILE_MODEL_LIST);

        // There is one extra item due to the footer.
        assertThat(models.size(), is(DUMMY_PROFILES.size() + 1));
        assertThat(mModel.get(SELECTED_PROFILE), is(DUMMY_PROFILES.get(0)));

        // If the old profile no longer exists, the new first one is selected.
        mMediator.setAutofillProfileItems(DUMMY_PROFILES.subList(1, 3));
        assertThat(mModel.get(PROFILE_MODEL_LIST).size(), is(3));
        assertThat(mModel.get(SELECTED_PROFILE), is(DUMMY_PROFILES.get(1)));

        // If it can be found, it remains selected.
        mMediator.setAutofillProfileItems(DUMMY_PROFILES);
        assertThat(mModel.get(PROFILE_MODEL_LIST).size(), is(DUMMY_PROFILES.size() + 1));
        assertThat(mModel.get(SELECTED_PROFILE), is(DUMMY_PROFILES.get(1)));

        // That is true even if only the GUID remains the same.
        FastCheckoutAutofillProfile sameGUIDProfile =
                FastCheckoutTestUtils.createDetailedProfile(
                        DUMMY_PROFILES.get(1).getGUID(),
                        /* name= */ "Frank Tank",
                        /* streetAddress= */ "Somewhere 123",
                        /* city= */ "Des Moines",
                        /* postalCode= */ "93439",
                        /* email= */ "[email protected]",
                        /* phoneNumber= */ "+1-111-333-222");
        mMediator.setAutofillProfileItems(
                List.of(DUMMY_PROFILES.get(0), DUMMY_PROFILES.get(1), sameGUIDProfile));
        assertThat(mModel.get(PROFILE_MODEL_LIST).size(), is(4));
        assertThat(mModel.get(SELECTED_PROFILE), is(sameGUIDProfile));
    }

    @Test
    public void testSetCreditCardItemsUpdatesPreservesPriorSelection() {
        mMediator.setCreditCardItems(DUMMY_CARDS);

        // There is one extra item due to the footer.
        assertThat(mModel.get(CREDIT_CARD_MODEL_LIST).size(), is(DUMMY_CARDS.size() + 1));
        assertThat(mModel.get(SELECTED_CREDIT_CARD), is(DUMMY_CARDS.get(0)));

        // If the old profile no longer exists, the new first one is selected.
        mMediator.setCreditCardItems(DUMMY_CARDS.subList(1, 3));
        assertThat(mModel.get(CREDIT_CARD_MODEL_LIST).size(), is(3));
        assertThat(mModel.get(SELECTED_CREDIT_CARD), is(DUMMY_CARDS.get(1)));

        // If it can be found, it remains selected.
        mMediator.setCreditCardItems(DUMMY_CARDS);
        assertThat(mModel.get(CREDIT_CARD_MODEL_LIST).size(), is(DUMMY_CARDS.size() + 1));
        assertThat(mModel.get(SELECTED_CREDIT_CARD), is(DUMMY_CARDS.get(1)));

        // That is true even if only the GUID remains the same.
        FastCheckoutCreditCard sameGUIDCard =
                FastCheckoutTestUtils.createDummyCreditCard(
                        DUMMY_CARDS.get(1).getGUID(), "https://example.fr", "56456551111");
        mMediator.setCreditCardItems(List.of(DUMMY_CARDS.get(0), DUMMY_CARDS.get(1), sameGUIDCard));
        assertThat(mModel.get(CREDIT_CARD_MODEL_LIST).size(), is(4));
        assertThat(mModel.get(SELECTED_CREDIT_CARD), is(sameGUIDCard));
    }

    @Test
    public void testSetSelectedAutofillProfileUpdatesModels() {
        mMediator.setAutofillProfileItems(DUMMY_PROFILES);

        ModelList models = mModel.get(PROFILE_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_PROFILES.size() + 1));

        mMediator.setSelectedAutofillProfile(DUMMY_PROFILES.get(0));
        checkThatAutofillProfileIsSelected(0);

        mMediator.setSelectedAutofillProfile(DUMMY_PROFILES.get(2));
        checkThatAutofillProfileIsSelected(2);
    }

    @Test
    public void testSetSelectedCreditCardUpdatesModels() {
        mMediator.setCreditCardItems(DUMMY_CARDS);

        ModelList models = mModel.get(CREDIT_CARD_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_CARDS.size() + 1));

        mMediator.setSelectedCreditCard(DUMMY_CARDS.get(0));
        checkThatCreditCardIsSelected(0);

        mMediator.setSelectedCreditCard(DUMMY_CARDS.get(1));
        checkThatCreditCardIsSelected(1);
    }

    private void checkThatAutofillProfileIsSelected(int selectedIndex) {
        ModelList models = mModel.get(PROFILE_MODEL_LIST);
        for (int index = 0; index < DUMMY_PROFILES.size(); ++index) {
            PropertyModel model = models.get(index).model;
            assertThat(
                    model.get(AutofillProfileItemProperties.IS_SELECTED),
                    is(index == selectedIndex));
        }
    }

    private void checkThatCreditCardIsSelected(int selectedIndex) {
        ModelList models = mModel.get(CREDIT_CARD_MODEL_LIST);
        for (int index = 0; index < DUMMY_CARDS.size(); ++index) {
            PropertyModel model = models.get(index).model;
            assertThat(model.get(CreditCardItemProperties.IS_SELECTED), is(index == selectedIndex));
        }
    }

    @Test
    public void testAutofillProfileItemOnClickListenerUpdatesSelectedProfile() {
        mMediator.setAutofillProfileItems(DUMMY_PROFILES);
        mMediator.setSelectedAutofillProfile(DUMMY_PROFILES.get(0));
        mMediator.setCurrentScreen(ScreenType.AUTOFILL_PROFILE_SCREEN);

        ModelList models = mModel.get(PROFILE_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_PROFILES.size() + 1));

        PropertyModel model = models.get(2).model;
        model.get(AutofillProfileItemProperties.ON_CLICK_LISTENER).run();

        assertThat(
                mModel.get(SELECTED_PROFILE),
                is(model.get(AutofillProfileItemProperties.AUTOFILL_PROFILE)));
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.HOME_SCREEN));
        assertActionRecorded(FastCheckoutUserActions.SELECTED_DIFFERENT_ADDRESS);
    }

    @Test
    public void testCreditCardItemOnClickListenerUpdatesSelectedCreditCard() {
        mMediator.setCreditCardItems(DUMMY_CARDS);
        mMediator.setSelectedCreditCard(DUMMY_CARDS.get(0));
        mMediator.setCurrentScreen(ScreenType.CREDIT_CARD_SCREEN);

        ModelList models = mModel.get(CREDIT_CARD_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_CARDS.size() + 1));

        PropertyModel model = models.get(1).model;
        model.get(CreditCardItemProperties.ON_CLICK_LISTENER).run();

        assertThat(
                mModel.get(SELECTED_CREDIT_CARD),
                is(model.get(CreditCardItemProperties.CREDIT_CARD)));
        assertThat(mModel.get(CURRENT_SCREEN), is(ScreenType.HOME_SCREEN));
        assertActionRecorded(FastCheckoutUserActions.SELECTED_DIFFERENT_CREDIT_CARD);
    }

    @Test
    public void testFooterClickOnAutofillProfileSelectionOpensSettings() {
        mMediator.setAutofillProfileItems(DUMMY_PROFILES);
        mMediator.setSelectedAutofillProfile(DUMMY_PROFILES.get(0));
        mMediator.setCurrentScreen(ScreenType.AUTOFILL_PROFILE_SCREEN);

        ModelList models = mModel.get(DETAIL_SCREEN_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_PROFILES.size() + 1));

        // Get the last item.
        PropertyModel model = models.get(DUMMY_PROFILES.size()).model;
        model.get(FooterItemProperties.ON_CLICK_HANDLER).run();
        verify(mMockDelegate).openAutofillProfileSettings();
        assertActionRecorded(FastCheckoutUserActions.NAVIGATED_TO_ADDRESSES_SETTINGS_VIA_FOOTER);
    }

    @Test
    public void testFooterClickOnCreditCardSelectionOpensSettings() {
        mMediator.setCreditCardItems(DUMMY_CARDS);
        mMediator.setSelectedCreditCard(DUMMY_CARDS.get(0));
        mMediator.setCurrentScreen(ScreenType.CREDIT_CARD_SCREEN);

        ModelList models = mModel.get(DETAIL_SCREEN_MODEL_LIST);
        assertThat(models.size(), is(DUMMY_CARDS.size() + 1));

        // Get the last item.
        PropertyModel model = models.get(DUMMY_CARDS.size()).model;
        model.get(FooterItemProperties.ON_CLICK_HANDLER).run();
        verify(mMockDelegate).openCreditCardSettings();
        assertActionRecorded(FastCheckoutUserActions.NAVIGATED_TO_CREDIT_CARDS_SETTINGS_VIA_FOOTER);
    }

    @Test
    public void testHidesTheBottomSheetOnDestroy() {
        mMediator.showOptions(DUMMY_PROFILES, DUMMY_CARDS);

        verify(mMockBottomSheetController, never()).hideContent(any(), anyBoolean());
        assertThat(mModel.get(VISIBLE), is(true));

        mMediator.destroy();
        assertThat(mModel.get(VISIBLE), is(false));
        assertActionRecorded(FastCheckoutUserActions.DESTROYED);
    }

    private void assertActionRecorded(FastCheckoutUserActions action) {
        assertTrue(mActionTester.getActions().contains(action.getAction()));
    }
}