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

// Copyright 2012 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.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
import static org.chromium.base.test.util.CriteriaHelper.pollUiThread;

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

import androidx.annotation.Nullable;
import androidx.test.espresso.Espresso;
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.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.ScalableTimeout;
import org.chromium.chrome.browser.flags.ChromeSwitches;
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.suggestion.Icon;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetTestSupport;

import java.util.List;

/**
 * Integration tests for the FastCheckout component that confirm that interactions between different
 * screens on the bottom sheet work correctly and the bridge is invoked as expected.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class FastCheckoutIntegrationTest {
    private static final List<FastCheckoutAutofillProfile> DUMMY_PROFILES =
            List.of(
                    FastCheckoutTestUtils.createDetailedProfile(
                            /* guid= */ "123",
                            /* name= */ "Frederic Profiletest",
                            /* streetAddress= */ "Park Avenue 234",
                            /* city= */ "New York",
                            /* postalCode= */ "12345",
                            /* email= */ "[email protected]",
                            /* phoneNumber= */ "+1-345-543-645"),
                    FastCheckoutTestUtils.createDetailedProfile(
                            /* guid= */ "234",
                            /* name= */ "Rufus Dufus",
                            /* 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.createDetailedLocalCreditCard(
                            /* guid= */ "154",
                            /* origin= */ "https://example.fr",
                            /* name= */ "Frederic Profiletest",
                            /* number= */ "4111111111111",
                            /* obfuscatedNumber= */ "1111",
                            /* month= */ "11",
                            /* year= */ "2023",
                            /* issuerIcon= */ Icon.CARD_DINERS),
                    FastCheckoutTestUtils.createDetailedLocalCreditCard(
                            /* guid= */ "431",
                            /* origin= */ "https://example.fr",
                            /* name= */ "Jane Doe",
                            /* number= */ "4564565541234",
                            /* obfuscatedNumber= */ "1234",
                            /* month= */ "10",
                            /* year= */ "2025",
                            /* issuerIcon= */ Icon.CARD_VISA));

    private FastCheckoutComponent mFastCheckout;

    @Mock private FastCheckoutComponent.Delegate mMockBridge;

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private BottomSheetController mBottomSheetController;
    private BottomSheetTestSupport mTestSupport;

    public FastCheckoutIntegrationTest() {
        MockitoAnnotations.initMocks(this);
    }

    @Before
    public void setUp() throws InterruptedException {
        mActivityTestRule.startMainActivityOnBlankPage();
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout = new FastCheckoutCoordinator();
                    mBottomSheetController =
                            BottomSheetControllerProvider.from(
                                    mActivityTestRule.getActivity().getWindowAndroid());
                    mTestSupport = new BottomSheetTestSupport(mBottomSheetController);
                    mFastCheckout.initialize(
                            mActivityTestRule.getActivity(), mBottomSheetController, mMockBridge);
                });
    }

    @Test
    @MediumTest
    public void testOpenBottomSheetSelectOtherProfileAndAccept() {
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // The first Autofill profile should be displayed.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).check(matches(isDisplayed()));

        // Clicking on it opens the Autofill profile selection sheet.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).perform(click());
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(
                                                R.string
                                                        .fast_checkout_autofill_profile_sheet_title)))
                .check(matches(isDisplayed()));

        // Clicking on another profile opens the home sheet again.
        onView(withText(DUMMY_PROFILES.get(1).getFullName())).check(matches(isDisplayed()));
        onView(withText(DUMMY_PROFILES.get(1).getFullName())).perform(click());
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(R.string.fast_checkout_home_sheet_title)))
                .check(matches(isDisplayed()));

        // Accept the bottom sheet.
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(R.string.fast_checkout_home_sheet_accept)))
                .perform(click());

        waitForEvent(mMockBridge).onOptionsSelected(DUMMY_PROFILES.get(1), DUMMY_CARDS.get(0));
        // `onDismissed` is only called when the selection screen. was not accepted.
        verify(mMockBridge, never()).onDismissed();
    }

    @Test
    @MediumTest
    public void testOpenCardsListAndSelect() {
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // The first Autofill profile and credit card should be displayed.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).check(matches(isDisplayed()));
        onView(withText(DUMMY_CARDS.get(0).getObfuscatedNumber())).check(matches(isDisplayed()));

        // Clicking on it opens the credit card selection sheet.
        onView(withText(DUMMY_CARDS.get(0).getObfuscatedNumber())).perform(click());
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(R.string.fast_checkout_credit_card_sheet_title)))
                .check(matches(isDisplayed()));

        // Clicking on another card opens the home sheet again.
        onView(withText(DUMMY_CARDS.get(1).getObfuscatedNumber())).check(matches(isDisplayed()));
        onView(withText(DUMMY_CARDS.get(1).getObfuscatedNumber())).perform(click());
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(R.string.fast_checkout_home_sheet_title)))
                .check(matches(isDisplayed()));

        // Accept the bottom sheet.
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(R.string.fast_checkout_home_sheet_accept)))
                .perform(click());

        waitForEvent(mMockBridge).onOptionsSelected(DUMMY_PROFILES.get(0), DUMMY_CARDS.get(1));
        verify(mMockBridge, never()).onDismissed();
    }

    @Test
    @MediumTest
    public void testBackDismissesAndCallsCallback() {
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        Espresso.pressBack();

        waitForEvent(mMockBridge).onDismissed();
        verify(mMockBridge, never()).onOptionsSelected(any(), any());
    }

    @Test
    @MediumTest
    public void testUserDismissBottomSheetCallsCallback() {
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // Hide the bottom sheet.
        runOnUiThreadBlocking(
                () -> mTestSupport.setSheetState(BottomSheetController.SheetState.HIDDEN, false));

        waitForEvent(mMockBridge).onDismissed();
        verify(mMockBridge, never()).onOptionsSelected(any(), any());
    }

    @Test
    @MediumTest
    public void testOpenProfilesAndDismissBottomSheetCallsCallback() {
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // The first Autofill profile should be displayed.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).check(matches(isDisplayed()));

        // Clicking on it opens the Autofill profile selection sheet.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).perform(click());
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(
                                                R.string
                                                        .fast_checkout_autofill_profile_sheet_title)))
                .check(matches(isDisplayed()));

        // Hide the bottom sheet.
        runOnUiThreadBlocking(
                () -> mTestSupport.setSheetState(BottomSheetController.SheetState.HIDDEN, false));

        waitForEvent(mMockBridge).onDismissed();
        verify(mMockBridge, never()).onOptionsSelected(any(), any());
    }

    @Test
    @MediumTest
    public void testOpenProfilesAndUpdateProfilesList() {
        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // The first Autofill profile should be displayed.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).check(matches(isDisplayed()));

        // Clicking on it opens the Autofill profile selection sheet.
        onView(withText(DUMMY_PROFILES.get(0).getFullName())).perform(click());
        onView(
                        withText(
                                mActivityTestRule
                                        .getActivity()
                                        .getString(
                                                R.string
                                                        .fast_checkout_autofill_profile_sheet_title)))
                .check(matches(isDisplayed()));

        List<FastCheckoutAutofillProfile> updatedProfiles =
                List.of(
                        FastCheckoutTestUtils.createDetailedProfile(
                                /* guid= */ "3978",
                                /* name= */ "Palolo Lacau",
                                /* streetAddress= */ "Park Avenue 234",
                                /* city= */ "Munich",
                                /* postalCode= */ "12345",
                                /* email= */ "[email protected]",
                                /* phoneNumber= */ "+1-111-111-111"),
                        FastCheckoutTestUtils.createDetailedProfile(
                                /* guid= */ "9999",
                                /* name= */ "Bacalau Lee",
                                /* streetAddress= */ "Park Avenue 99",
                                /* city= */ "Berlin",
                                /* postalCode= */ "12345",
                                /* email= */ "[email protected]",
                                /* phoneNumber= */ "+1-456-123-113"));

        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(updatedProfiles, DUMMY_CARDS);
                });
        BottomSheetTestSupport.waitForOpen(mBottomSheetController);

        // The first Autofill profile should be displayed.
        onView(withText(updatedProfiles.get(1).getFullName())).check(matches(isDisplayed()));
        onView(withText(updatedProfiles.get(1).getPhoneNumber())).check(matches(isDisplayed()));
    }

    @DisabledTest(
            message =
                    "Disabled because it's flaky. "
                            + "Investigation will be tracked in crbug.com/1418362.")
    @Test
    @MediumTest
    public void testDismissedIfUnableToShow() throws Exception {
        BottomSheetContent otherBottomSheetContent =
                runOnUiThreadBlocking(
                        () -> {
                            TextView highPriorityBottomSheetContentView =
                                    new TextView(mActivityTestRule.getActivity());
                            highPriorityBottomSheetContentView.setText(
                                    "Another bottom sheet content");
                            BottomSheetContent content =
                                    new BottomSheetContent() {
                                        @Override
                                        public View getContentView() {
                                            return highPriorityBottomSheetContentView;
                                        }

                                        @Nullable
                                        @Override
                                        public View getToolbarView() {
                                            return null;
                                        }

                                        @Override
                                        public int getVerticalScrollOffset() {
                                            return 0;
                                        }

                                        @Override
                                        public void destroy() {}

                                        @Override
                                        public int getPriority() {
                                            return ContentPriority.HIGH;
                                        }

                                        @Override
                                        public boolean swipeToDismissEnabled() {
                                            return false;
                                        }

                                        @Override
                                        public int getSheetContentDescriptionStringId() {
                                            return 0;
                                        }

                                        @Override
                                        public int getSheetHalfHeightAccessibilityStringId() {
                                            return 0;
                                        }

                                        @Override
                                        public int getSheetFullHeightAccessibilityStringId() {
                                            return 0;
                                        }

                                        @Override
                                        public int getSheetClosedAccessibilityStringId() {
                                            return 0;
                                        }
                                    };
                            mBottomSheetController.requestShowContent(
                                    content, /* animate= */ false);
                            return content;
                        });
        pollUiThread(() -> getBottomSheetState() == SheetState.PEEK);
        onView(withText("Another bottom sheet content")).check(matches(isDisplayed()));

        runOnUiThreadBlocking(
                () -> {
                    mFastCheckout.showOptions(DUMMY_PROFILES, DUMMY_CARDS);
                });

        waitForEvent(mMockBridge).onDismissed();
        verify(mMockBridge, never()).onOptionsSelected(any(), any());
        onView(withText("Another bottom sheet content")).check(matches(isDisplayed()));

        runOnUiThreadBlocking(
                () -> {
                    mBottomSheetController.hideContent(
                            otherBottomSheetContent, /* animate= */ false);
                });
        pollUiThread(() -> getBottomSheetState() == SheetState.HIDDEN);
    }

    public static <T> T waitForEvent(T mock) {
        return verify(
                mock,
                timeout(ScalableTimeout.scaleTimeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL)));
    }

    private @SheetState int getBottomSheetState() {
        return mBottomSheetController.getSheetState();
    }
}