chromium/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_component/AccessorySheetRenderTest.java

// Copyright 2021 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.keyboard_accessory.sheet_component;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;

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

import static org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetProperties.ACTIVE_TAB_INDEX;
import static org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetProperties.HEIGHT;
import static org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetProperties.NO_ACTIVE_TAB;
import static org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetProperties.TABS;
import static org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetProperties.TOP_SHADOW_VISIBLE;
import static org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetProperties.VISIBLE;
import static org.chromium.ui.base.LocalizationUtils.setRtlForTesting;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import androidx.test.filters.MediumTest;

import org.junit.After;
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.chromium.base.Callback;
import org.chromium.base.FeatureList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.params.ParameterAnnotations;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.ApplicationTestUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManagerFactory;
import org.chromium.chrome.browser.autofill.helpers.FaviconHelper;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.keyboard_accessory.AccessoryTabType;
import org.chromium.chrome.browser.keyboard_accessory.R;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
import org.chromium.chrome.browser.keyboard_accessory.data.PropertyProvider;
import org.chromium.chrome.browser.keyboard_accessory.data.Provider;
import org.chromium.chrome.browser.keyboard_accessory.data.UserInfoField;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.AccessorySheetTabCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.AddressAccessorySheetCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.CreditCardAccessorySheetCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.PasswordAccessorySheetCoordinator;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import org.chromium.ui.AsyncViewProvider;
import org.chromium.ui.AsyncViewStub;
import org.chromium.ui.modelutil.LazyConstructionPropertyMcp;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.BlankUiTestActivity;
import org.chromium.ui.test.util.NightModeTestUtils;
import org.chromium.ui.test.util.ViewUtils;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * These tests render screenshots of various accessory sheets and compare them to a gold standard.
 */
@RunWith(ParameterizedRunner.class)
@ParameterAnnotations.UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@DisableFeatures(ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class AccessorySheetRenderTest {
    @ParameterAnnotations.ClassParameter
    private static List<ParameterSet> sClassParams =
            Arrays.asList(
                    new ParameterSet().value(false, false).name("Default"),
                    new ParameterSet().value(false, true).name("RTL"),
                    new ParameterSet().value(true, false).name("NightMode"));

    private FrameLayout mContentView;
    private PropertyModel mSheetModel;

    // No @Rule since we only need the launching helpers. Adding the rule to the chain breaks with
    // any ParameterizedRunnerDelegate.
    private BaseActivityTestRule<BlankUiTestActivity> mActivityTestRule =
            new BaseActivityTestRule<>(BlankUiTestActivity.class);

    @Rule
    public final ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setRevision(1)
                    .setBugComponent(ChromeRenderTestRule.Component.UI_BROWSER_AUTOFILL)
                    .build();

    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock private Profile mProfile;
    @Mock private PersonalDataManager mPersonalDataManager;

    public AccessorySheetRenderTest(boolean nightModeEnabled, boolean useRtlLayout) {
        Map<String, Boolean> featureMap = new HashMap<>();
        featureMap.put(ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES, false);
        FeatureList.setTestFeatures(featureMap);

        setRtlForTesting(useRtlLayout);
        NightModeTestUtils.setUpNightModeForBlankUiTestActivity(nightModeEnabled);
        mRenderTestRule.setNightModeEnabled(nightModeEnabled);
        mRenderTestRule.setVariantPrefix(useRtlLayout ? "RTL" : "LTR");
    }

    private static class TestFaviconHelper extends FaviconHelper {
        public TestFaviconHelper(Context context) {
            super(context, null);
        }

        @Override
        public void fetchFavicon(String origin, Callback<Drawable> setIconCallback) {
            setIconCallback.onResult(getDefaultIcon(origin));
        }
    }

    @Before
    public void setUp() throws InterruptedException {
        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
        FaviconHelper.setCreationStrategy((context, profile) -> new TestFaviconHelper(context));

        ProfileManager.setLastUsedProfileForTesting(mProfile);
        PersonalDataManagerFactory.setInstanceForTesting(mPersonalDataManager);

        mActivityTestRule.launchActivity(null);
        // Calling #setTheme() explicitly because the test rule doesn't have the @Rule annotation
        // and won't apply the theme.
        mActivityTestRule.getActivity().setTheme(R.style.Theme_BrowserUI_DayNight);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    AsyncViewStub sheetStub = initializeContentViewWithSheetStub();

                    mSheetModel =
                            createSheetModel(
                                    mActivityTestRule
                                            .getActivity()
                                            .getResources()
                                            .getDimensionPixelSize(
                                                    R.dimen.keyboard_accessory_sheet_height));

                    LazyConstructionPropertyMcp.create(
                            mSheetModel,
                            VISIBLE,
                            AsyncViewProvider.of(
                                    sheetStub, R.id.keyboard_accessory_sheet_container),
                            AccessorySheetViewBinder::bind);
                });
    }

    @After
    public void tearDown() {
        NightModeTestUtils.tearDownNightModeForBlankUiTestActivity();
        setRtlForTesting(false);
        try {
            ApplicationTestUtils.finishActivity(mActivityTestRule.getActivity());
        } catch (Exception e) {
            // Activity was already closed (e.g. due to last test tearing down the suite).
        }
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testAddingPasswordTabToModelRendersTabsView() throws Exception {
        final KeyboardAccessoryData.AccessorySheetData sheet =
                new KeyboardAccessoryData.AccessorySheetData(
                        AccessoryTabType.PASSWORDS,
                        /* userInfoTitle= */ "",
                        /* plusAddressTitle= */ "",
                        /* warning= */ "");
        sheet.getUserInfoList()
                .add(new KeyboardAccessoryData.UserInfo("http://psl.origin.com/", true));
        sheet.getUserInfoList()
                .get(0)
                .addField(new UserInfoField("No username", "No username", "", false, null));
        sheet.getUserInfoList()
                .get(0)
                .addField(
                        new UserInfoField(
                                "Password", "Password for No username", "", true, cb -> {}));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Suggest strong password", cb -> {}));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Manage Passwords", cb -> {}));

        PasswordAccessorySheetCoordinator coordinator =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new PasswordAccessorySheetCoordinator(
                                        mActivityTestRule.getActivity(), mProfile, null));
        showSheetTab(coordinator, sheet);

        mRenderTestRule.render(mContentView, "Passwords");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testAddingPlusAddressesToPasswordTabRendersTabsView() throws Exception {
        final KeyboardAccessoryData.AccessorySheetData sheet =
                new KeyboardAccessoryData.AccessorySheetData(
                        AccessoryTabType.PASSWORDS,
                        /* userInfoTitle= */ "No saved passwords for google.com",
                        /* plusAddressTitle= */ "",
                        /* warning= */ "");
        sheet.getPlusAddressInfoList()
                .add(
                        new KeyboardAccessoryData.PlusAddressInfo(
                                /* origin= */ "google.com",
                                new UserInfoField(
                                        "[email protected]",
                                        "[email protected]",
                                        "",
                                        false,
                                        unused -> {})));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Suggest strong password", cb -> {}));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Manage Passwords", cb -> {}));

        PasswordAccessorySheetCoordinator coordinator =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new PasswordAccessorySheetCoordinator(
                                        mActivityTestRule.getActivity(), mProfile, null));
        showSheetTab(coordinator, sheet);

        mRenderTestRule.render(mContentView, "Passwords with plus address");
    }

    // Tests rendering of Payments tab with both credit cards and promo code offers.
    // Promo code offers should appear above the credit cards.
    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testAddingCreditCardAndPromoCodeToModelRendersTabsView() throws Exception {
        final KeyboardAccessoryData.AccessorySheetData sheet =
                new KeyboardAccessoryData.AccessorySheetData(
                        AccessoryTabType.CREDIT_CARDS,
                        /* userInfoTitle= */ "",
                        /* plusAddressTitle= */ "",
                        /* warning= */ "");
        sheet.getUserInfoList().add(new KeyboardAccessoryData.UserInfo("", true));
        sheet.getUserInfoList()
                .get(0)
                .addField(
                        new UserInfoField(
                                "**** 9219", "Card for Todd Tester", "1", false, result -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(new UserInfoField("10", "10", "-1", false, result -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(new UserInfoField("2021", "2021", "-1", false, result -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(
                        new UserInfoField("Todd Tester", "Todd Tester", "0", false, result -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(new UserInfoField("123", "123", "-1", false, result -> {}));
        sheet.getPromoCodeInfoList().add(new KeyboardAccessoryData.PromoCodeInfo());
        sheet.getPromoCodeInfoList()
                .get(0)
                .setPromoCode(
                        new UserInfoField(
                                "50$OFF", "Promo Code for Todd Tester", "1", false, result -> {}));
        sheet.getPromoCodeInfoList()
                .get(0)
                .setDetailsText("Get $50 off when you use this code at checkout.");
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Manage payment methods", cb -> {}));

        CreditCardAccessorySheetCoordinator coordinator =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new CreditCardAccessorySheetCoordinator(
                                        mActivityTestRule.getActivity(), mProfile, null));
        showSheetTab(coordinator, sheet);

        mRenderTestRule.render(mContentView, "credit_cards_and_promo_codes");
    }

    // Tests rendering of Payments tab with IBANs.
    // IBANs should appear in Payment Methods section.
    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testAddingIbansToModelRendersTabsView() throws Exception {
        final KeyboardAccessoryData.AccessorySheetData sheet =
                new KeyboardAccessoryData.AccessorySheetData(
                        AccessoryTabType.CREDIT_CARDS,
                        /* userInfoTitle= */ "",
                        /* plusAddressTitle= */ "",
                        /* warning= */ "");
        sheet.getIbanInfoList().add(new KeyboardAccessoryData.IbanInfo());
        sheet.getIbanInfoList()
                .get(0)
                .setValue(
                        new UserInfoField(
                                /* displayText= */ "CH56 •••• •••• •••• •800 9",
                                /* a11yDescription= */ "",
                                /* id= */ "123456",
                                /* isObfuscated= */ false,
                                result -> {}));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Manage payment methods", cb -> {}));

        CreditCardAccessorySheetCoordinator coordinator =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new CreditCardAccessorySheetCoordinator(
                                        mActivityTestRule.getActivity(), mProfile, null));
        showSheetTab(coordinator, sheet);

        mRenderTestRule.render(mContentView, "ibans");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testAddingAddressToModelRendersTabsView() throws Exception {
        // Construct a sheet with a few data fields. Leave gaps so that the footer is visible in the
        // screenshot (but supply the fields itself since the field count should be fixed).
        final KeyboardAccessoryData.AccessorySheetData sheet =
                new KeyboardAccessoryData.AccessorySheetData(
                        AccessoryTabType.ADDRESSES,
                        /* userInfoTitle= */ "",
                        /* plusAddressTitle= */ "",
                        /* warning= */ "");
        sheet.getUserInfoList().add(new KeyboardAccessoryData.UserInfo("", true));
        sheet.getUserInfoList()
                .get(0)
                .addField(new UserInfoField("Todd Tester", "Todd Tester", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField( // Unused company name field.
                        new UserInfoField("", "", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(
                        new UserInfoField(
                                "112 Second Str", "112 Second Str", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField( // Unused address line 2 field.
                        new UserInfoField("", "", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField( // Unused ZIP code field.
                        new UserInfoField("", "", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(new UserInfoField("Budatest", "Budatest", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField( // Unused state field.
                        new UserInfoField("", "", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField( // Unused country field.
                        new UserInfoField("", "", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(
                        new UserInfoField("+088343188321", "+088343188321", "", false, item -> {}));
        sheet.getUserInfoList()
                .get(0)
                .addField(
                        new UserInfoField(
                                "[email protected]",
                                "[email protected]",
                                "",
                                false,
                                item -> {}));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Manage addresses", cb -> {}));

        AddressAccessorySheetCoordinator coordinator =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new AddressAccessorySheetCoordinator(
                                        mActivityTestRule.getActivity(), mProfile, null));
        showSheetTab(coordinator, sheet);

        mRenderTestRule.render(mContentView, "Addresses");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testAddingPlusAddressToModelRendersTabsView() throws Exception {
        final KeyboardAccessoryData.AccessorySheetData sheet =
                new KeyboardAccessoryData.AccessorySheetData(
                        AccessoryTabType.ADDRESSES,
                        /* userInfoTitle= */ "No saved addresses",
                        /* plusAddressTitle= */ "",
                        /* warning= */ "");
        sheet.getPlusAddressInfoList()
                .add(
                        new KeyboardAccessoryData.PlusAddressInfo(
                                /* origin= */ "google.com",
                                new UserInfoField(
                                        "[email protected]",
                                        "[email protected]",
                                        "",
                                        false,
                                        unused -> {})));
        sheet.getFooterCommands()
                .add(new KeyboardAccessoryData.FooterCommand("Manage addresses", cb -> {}));

        AddressAccessorySheetCoordinator coordinator =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                new AddressAccessorySheetCoordinator(
                                        mActivityTestRule.getActivity(), mProfile, null));
        showSheetTab(coordinator, sheet);

        mRenderTestRule.render(mContentView, "Addresses with plus address");
    }

    private AsyncViewStub initializeContentViewWithSheetStub() {
        mContentView =
                (FrameLayout)
                        LayoutInflater.from(mActivityTestRule.getActivity())
                                .inflate(R.layout.test_main, null);
        AsyncViewStub sheetStub = mContentView.findViewById(R.id.keyboard_accessory_sheet_stub);
        sheetStub.setLayoutResource(R.layout.keyboard_accessory_sheet);
        sheetStub.setShouldInflateOnBackgroundThread(true);
        LinearLayout.LayoutParams layoutParams =
                new LinearLayout.LayoutParams(
                        MATCH_PARENT,
                        mActivityTestRule
                                .getActivity()
                                .getResources()
                                .getDimensionPixelSize(R.dimen.keyboard_accessory_sheet_height));
        layoutParams.gravity = Gravity.START | Gravity.BOTTOM;
        sheetStub.setLayoutParams(layoutParams);

        mActivityTestRule.getActivity().setContentView(mContentView);
        return sheetStub;
    }

    private static PropertyModel createSheetModel(int height) {
        return new PropertyModel.Builder(
                        TABS, ACTIVE_TAB_INDEX, VISIBLE, HEIGHT, TOP_SHADOW_VISIBLE)
                .with(HEIGHT, height)
                .with(TABS, new ListModel<>())
                .with(ACTIVE_TAB_INDEX, NO_ACTIVE_TAB)
                .with(VISIBLE, false)
                .with(TOP_SHADOW_VISIBLE, false)
                .build();
    }

    private void showSheetTab(
            AccessorySheetTabCoordinator sheetComponent,
            KeyboardAccessoryData.AccessorySheetData sheetData) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mSheetModel.get(TABS).add(sheetComponent.getTab());
                    Provider<KeyboardAccessoryData.AccessorySheetData> provider =
                            new PropertyProvider<>();
                    sheetComponent.registerDataProvider(provider);
                    provider.notifyObservers(sheetData);
                    mSheetModel.set(ACTIVE_TAB_INDEX, 0);
                    mSheetModel.set(VISIBLE, true);
                });
        ViewUtils.waitForView(mContentView, withId(R.id.keyboard_accessory_sheet_frame));
    }
}