chromium/chrome/browser/facilitated_payments/ui/android/internal/java/src/org/chromium/chrome/browser/facilitated_payments/FacilitatedPaymentsPaymentMethodsViewTest.java

// Copyright 2023 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.facilitated_payments;

import static androidx.test.espresso.matcher.ViewMatchers.assertThat;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;

import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.DISMISS_HANDLER;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.FopSelectorProperties.SCREEN_ITEMS;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.ItemType.BANK_ACCOUNT;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.ItemType.CONTINUE_BUTTON;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.SCREEN;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.SCREEN_VIEW_MODEL;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.SequenceScreen.ERROR_SCREEN;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.SequenceScreen.FOP_SELECTOR;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.SequenceScreen.PROGRESS_SCREEN;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.VISIBLE_STATE;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.VisibleState.HIDDEN;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.VisibleState.SHOWN;
import static org.chromium.chrome.browser.facilitated_payments.FacilitatedPaymentsPaymentMethodsProperties.VisibleState.SWAPPING_SCREEN;

import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.MediumTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.autofill.payments.AccountType;
import org.chromium.components.autofill.payments.BankAccount;
import org.chromium.components.autofill.payments.PaymentInstrument;
import org.chromium.components.autofill.payments.PaymentRail;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetTestSupport;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.widget.ButtonCompat;

/** Instrumentation tests for {@link FacilitatedPaymentsPaymentMethodsView}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public final class FacilitatedPaymentsPaymentMethodsViewTest {
    private static final BankAccount BANK_ACCOUNT_1 =
            new BankAccount.Builder()
                    .setPaymentInstrument(
                            new PaymentInstrument.Builder()
                                    .setInstrumentId(100)
                                    .setNickname("nickname1")
                                    .setSupportedPaymentRails(new int[] {PaymentRail.PIX})
                                    .build())
                    .setBankName("bankName1")
                    .setAccountNumberSuffix("1111")
                    .setAccountType(AccountType.CHECKING)
                    .build();
    private static final BankAccount BANK_ACCOUNT_2 =
            new BankAccount.Builder()
                    .setPaymentInstrument(
                            new PaymentInstrument.Builder()
                                    .setInstrumentId(200)
                                    .setNickname("nickname2")
                                    .setSupportedPaymentRails(new int[] {PaymentRail.PIX})
                                    .build())
                    .setBankName("bankName2")
                    .setAccountNumberSuffix("2222")
                    .setAccountType(AccountType.SAVINGS)
                    .build();

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private BottomSheetController mBottomSheetController;
    private BottomSheetTestSupport mSheetTestSupport;
    private FacilitatedPaymentsPaymentMethodsView mView;
    private FacilitatedPaymentsPaymentMethodsMediator mMediator;
    private PropertyModel mModel;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mActivityTestRule.startMainActivityOnBlankPage();
        mBottomSheetController =
                mActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getBottomSheetController();
        mSheetTestSupport = new BottomSheetTestSupport(mBottomSheetController);
        mMediator = new FacilitatedPaymentsPaymentMethodsMediator();
        runOnUiThreadBlocking(
                () -> {
                    mModel =
                            new PropertyModel.Builder(
                                            FacilitatedPaymentsPaymentMethodsProperties.ALL_KEYS)
                                    .with(VISIBLE_STATE, HIDDEN)
                                    .with(DISMISS_HANDLER, (Integer unused) -> {})
                                    .build();
                    mView =
                            new FacilitatedPaymentsPaymentMethodsView(
                                    mActivityTestRule.getActivity(), mBottomSheetController);
                    PropertyModelChangeProcessor.create(
                            mModel,
                            mView,
                            FacilitatedPaymentsPaymentMethodsViewBinder
                                    ::bindFacilitatedPaymentsPaymentMethodsView);
                });
    }

    @Test
    @MediumTest
    public void testViewCanBeShownUsingTheModel() {
        // Confirm that the bottom sheet is not open.
        assertThat(mBottomSheetController.isSheetOpen(), is(false));

        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, FOP_SELECTOR);
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(
                                    new ListItem(
                                            BANK_ACCOUNT, createBankAccountModel(BANK_ACCOUNT_1)));
                    runOnUiThreadBlocking(() -> mModel.set(VISIBLE_STATE, SHOWN));
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Verify that the bottom sheet is opened, and shows the view.
        assertThat(mBottomSheetController.isSheetOpen(), is(true));
        assertThat(mView.getContentView().isShown(), is(true));
    }

    @Test
    @MediumTest
    public void testViewCanBeHiddenUsingTheModel() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, FOP_SELECTOR);
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(
                                    new ListItem(
                                            BANK_ACCOUNT, createBankAccountModel(BANK_ACCOUNT_1)));
                    runOnUiThreadBlocking(() -> mModel.set(VISIBLE_STATE, SHOWN));
                });

        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Confirm that the bottom sheet is opened, and shows the view.
        assertThat(mBottomSheetController.isSheetOpen(), is(true));
        assertThat(mView.getContentView().isShown(), is(true));

        runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE_STATE, HIDDEN);
                });

        BottomSheetTestSupport.waitForState(mBottomSheetController, SheetState.HIDDEN);

        // Verify that the view is hidden, and the bottom sheet is closed.
        assertThat(mView.getContentView().isShown(), is(false));
        assertThat(mBottomSheetController.isSheetOpen(), is(false));
    }

    @Test
    @MediumTest
    public void testBankAccountShown() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, FOP_SELECTOR);
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(
                                    new ListItem(
                                            BANK_ACCOUNT, createBankAccountModel(BANK_ACCOUNT_1)));
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(
                                    new ListItem(
                                            BANK_ACCOUNT, createBankAccountModel(BANK_ACCOUNT_2)));
                    mModel.set(VISIBLE_STATE, SHOWN);
                });

        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        assertThat(getBankAccounts().getChildCount(), is(2));

        String expectedBankAccountSummary1 = String.format("Pix  •  %s ••••%s", "Checking", "1111");
        assertThat(getBankAccountNameAt(0).getText(), is("bankName1"));
        assertThat(getBankAccountSummaryAt(0).getText(), is(expectedBankAccountSummary1));

        String expectedBankAccountSummary2 = String.format("Pix  •  %s ••••%s", "Savings", "2222");
        assertThat(getBankAccountNameAt(1).getText(), is("bankName2"));
        assertThat(getBankAccountSummaryAt(1).getText(), is(expectedBankAccountSummary2));
    }

    @Test
    @MediumTest
    public void testDescriptionLine() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, FOP_SELECTOR);
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(mMediator.buildAdditionalInfo());
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        TextView descriptionLine1 = mView.getContentView().findViewById(R.id.description_line);
        assertThat(
                descriptionLine1.getText(),
                hasToString(
                        containsString(
                                "By continuing, you agree to have your precise device location"
                                    + " collected and shared with your account holder for fraud"
                                    + " prevention purposes. To turn off Pix in Chrome, go to your"
                                    + " payment settings")));
    }

    @Test
    @MediumTest
    public void testContinueButtonText() {
        runOnUiThreadBlocking(
                () -> {
                    PropertyModel bankAccountModel = createBankAccountModel(BANK_ACCOUNT_1);
                    mModel.set(SCREEN, FOP_SELECTOR);
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(new ListItem(BANK_ACCOUNT, bankAccountModel));
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(new ListItem(CONTINUE_BUTTON, bankAccountModel));
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        TextView buttonText =
                mView.getContentView()
                        .findViewById(R.id.facilitated_payments_continue_button_title);
        assertThat(buttonText.getText(), is("Continue"));
    }

    @Test
    @MediumTest
    public void testProgressScreenShown() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, PROGRESS_SCREEN);
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Verify that the {@link ProgressBar} is shown.
        assertThat(
                containsViewOfClass((ViewGroup) mView.getContentView(), ProgressBar.class),
                is(true));
    }

    @Test
    @MediumTest
    public void testErrorScreenShown() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, ERROR_SCREEN);
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Verify that the error screen is shown.
        assertThat(
                containsViewWithId((ViewGroup) mView.getContentView(), R.id.error_screen),
                is(true));
    }

    @Test
    @MediumTest
    public void testErrorScreenContents() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, ERROR_SCREEN);
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        TextView title = mView.getContentView().findViewById(R.id.title);
        assertThat(title.getText(), is("Something went wrong"));
        TextView description = mView.getContentView().findViewById(R.id.description);
        assertThat(
                description.getText(),
                is(
                        "Your transaction didn’t go through. No funds were withdrawn from your"
                                + " account."));
        ButtonCompat primaryButton = mView.getContentView().findViewById(R.id.primary_button);
        assertThat(primaryButton.getText(), is("OK"));
    }

    @Test
    @MediumTest
    public void testFopSelectorToProgressScreenSwapUpdatesView() {
        // Show the FOP selector.
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, FOP_SELECTOR);
                    mModel.get(SCREEN_VIEW_MODEL)
                            .get(SCREEN_ITEMS)
                            .add(
                                    new ListItem(
                                            BANK_ACCOUNT, createBankAccountModel(BANK_ACCOUNT_1)));
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Confirm the FOP selector is shown.
        assertThat(mView.getContentView().isShown(), is(true));
        assertNotNull(mView.getContentView().findViewById(R.id.sheet_item_list));

        // Show the progress screen.
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE_STATE, SWAPPING_SCREEN);
                    mModel.set(SCREEN, PROGRESS_SCREEN);
                    mModel.set(VISIBLE_STATE, SHOWN);
                });

        // Verify that the progress screen is shown.
        assertThat(mView.getContentView().isShown(), is(true));
        assertThat(
                containsViewOfClass((ViewGroup) mView.getContentView(), ProgressBar.class),
                is(true));
    }

    @Test
    @MediumTest
    public void testProgressScreenToErrorScreenSwapUpdatesView() {
        // Show the progress screen.
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(SCREEN, PROGRESS_SCREEN);
                    mModel.set(VISIBLE_STATE, SHOWN);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Confirm the progress screen is shown.
        assertThat(mView.getContentView().isShown(), is(true));
        assertThat(
                containsViewOfClass((ViewGroup) mView.getContentView(), ProgressBar.class),
                is(true));

        // Show the error screen.
        runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE_STATE, SWAPPING_SCREEN);
                    mModel.set(SCREEN, ERROR_SCREEN);
                    mModel.set(VISIBLE_STATE, SHOWN);
                });

        // Verify that the error screen is shown.
        assertThat(mView.getContentView().isShown(), is(true));
        assertThat(
                containsViewWithId((ViewGroup) mView.getContentView(), R.id.error_screen),
                is(true));
    }

    private PropertyModel createBankAccountModel(BankAccount bankAccount) {
        return mMediator.createBankAccountModel(mActivityTestRule.getActivity(), bankAccount);
    }

    private RecyclerView getBankAccounts() {
        return mView.getContentView().findViewById(R.id.sheet_item_list);
    }

    private TextView getBankAccountNameAt(int index) {
        return getBankAccounts().getChildAt(index).findViewById(R.id.bank_name);
    }

    private TextView getBankAccountSummaryAt(int index) {
        return getBankAccounts().getChildAt(index).findViewById(R.id.bank_account_summary);
    }

    private static boolean containsViewOfClass(ViewGroup parent, Class<?> clazz) {
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            if (clazz.isInstance(child)) {
                return true;
            }
            if (child instanceof ViewGroup) {
                if (containsViewOfClass((ViewGroup) child, clazz)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean containsViewWithId(ViewGroup parent, int id) {
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            if (child.getId() == id) {
                return true;
            }
            if (child instanceof ViewGroup) {
                if (containsViewWithId((ViewGroup) child, id)) {
                    return true;
                }
            }
        }
        return false;
    }
}