chromium/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/bar_component/KeyboardAccessoryViewTest.java

// Copyright 2018 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.bar_component;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
import static androidx.test.espresso.matcher.ViewMatchers.withChild;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.keyboard_accessory.AccessoryAction.AUTOFILL_SUGGESTION;
import static org.chromium.chrome.browser.keyboard_accessory.AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY;
import static org.chromium.chrome.browser.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.BAR_ITEMS;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.DISABLE_ANIMATIONS_FOR_TESTING;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.OBFUSCATED_CHILD_AT_CALLBACK;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.SHEET_OPENER_ITEM;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.SHOW_SWIPING_IPH;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.VISIBLE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_GONE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_INVISIBLE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_NULL;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.filters.MediumTest;

import org.hamcrest.Matcher;
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.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManagerFactory;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.keyboard_accessory.R;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.AutofillBarItem;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.BarItem;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.SheetOpenerBarItem;
import org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupView;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.Action;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.autofill.AutofillSuggestion;
import org.chromium.components.autofill.SuggestionType;
import org.chromium.components.browser_ui.widget.chips.ChipView;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.feature_engagement.TriggerDetails;
import org.chromium.components.feature_engagement.TriggerState;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import org.chromium.ui.AsyncViewProvider;
import org.chromium.ui.AsyncViewStub;
import org.chromium.ui.ViewProvider;
import org.chromium.ui.modelutil.LazyConstructionPropertyMcp;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.ViewUtils;
import org.chromium.ui.widget.ChromeImageView;
import org.chromium.url.GURL;

import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/** View tests for the keyboard accessory component. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@SuppressWarnings("DoNotMock") // Mocks GURL
public class KeyboardAccessoryViewTest {
    private static final String CUSTOM_ICON_URL = "https://www.example.com/image.png";
    private static final Bitmap TEST_CARD_ART_IMAGE =
            Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
    private PropertyModel mModel;
    private BlockingQueue<KeyboardAccessoryView> mKeyboardAccessoryView;

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Mock PersonalDataManager mMockPersonalDataManager;

    private static class TestTracker implements Tracker {
        private boolean mWasDismissed;
        private @Nullable String mEmittedEvent;

        @Override
        public void notifyEvent(String event) {
            mEmittedEvent = event;
        }

        public @Nullable String getLastEmittedEvent() {
            return mEmittedEvent;
        }

        @Override
        public boolean shouldTriggerHelpUI(String feature) {
            return true;
        }

        @Override
        public TriggerDetails shouldTriggerHelpUIWithSnooze(String feature) {
            return null;
        }

        @Override
        public boolean wouldTriggerHelpUI(String feature) {
            return true;
        }

        @Override
        public boolean hasEverTriggered(String feature, boolean fromWindow) {
            return true;
        }

        @Override
        public int getTriggerState(String feature) {
            return TriggerState.HAS_NOT_BEEN_DISPLAYED;
        }

        @Override
        public void dismissed(String feature) {
            mWasDismissed = true;
        }

        @Override
        public void dismissedWithSnooze(String feature, int snoozeAction) {
            mWasDismissed = true;
        }

        public boolean wasDismissed() {
            return mWasDismissed;
        }

        @Nullable
        @Override
        public DisplayLockHandle acquireDisplayLock() {
            return () -> {};
        }

        @Override
        public void setPriorityNotification(String feature) {}

        @Override
        public @Nullable String getPendingPriorityNotification() {
            return null;
        }

        @Override
        public void registerPriorityNotificationHandler(
                String feature, Runnable priorityNotificationHandler) {}

        @Override
        public void unregisterPriorityNotificationHandler(String feature) {}

        @Override
        public boolean isInitialized() {
            return true;
        }

        @Override
        public void addOnInitializedCallback(Callback<Boolean> callback) {
            assert false : "Implement addOnInitializedCallback if you need it.";
        }
    }

    @Before
    public void setUp() throws InterruptedException {
        MockitoAnnotations.initMocks(this);
        mActivityTestRule.startMainActivityOnBlankPage();
        PersonalDataManagerFactory.setInstanceForTesting(mMockPersonalDataManager);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel =
                            KeyboardAccessoryProperties.defaultModelBuilder()
                                    .with(
                                            SHEET_OPENER_ITEM,
                                            new SheetOpenerBarItem(
                                                    new KeyboardAccessoryButtonGroupCoordinator
                                                            .SheetOpenerCallbacks() {
                                                        @Override
                                                        public void onViewBound(View buttons) {}

                                                        @Override
                                                        public void onViewUnbound(View buttons) {}
                                                    }))
                                    .with(DISABLE_ANIMATIONS_FOR_TESTING, true)
                                    .with(OBFUSCATED_CHILD_AT_CALLBACK, unused -> {})
                                    .with(SHOW_SWIPING_IPH, false)
                                    .build();
                    AsyncViewStub viewStub =
                            mActivityTestRule
                                    .getActivity()
                                    .findViewById(R.id.keyboard_accessory_stub);

                    mKeyboardAccessoryView = new ArrayBlockingQueue<>(1);
                    ViewProvider<KeyboardAccessoryView> provider =
                            AsyncViewProvider.of(viewStub, R.id.keyboard_accessory);
                    LazyConstructionPropertyMcp.create(
                            mModel, VISIBLE, provider, KeyboardAccessoryViewBinder::bind);
                    provider.whenLoaded(
                            (view) -> {
                                KeyboardAccessoryViewBinder.UiConfiguration uiConfiguration =
                                        KeyboardAccessoryCoordinator.createUiConfiguration(
                                                mActivityTestRule.getActivity(),
                                                mMockPersonalDataManager);
                                view.setBarItemsAdapter(
                                        KeyboardAccessoryCoordinator.createBarItemsAdapter(
                                                mModel.get(BAR_ITEMS), view, uiConfiguration));
                                mKeyboardAccessoryView.add(view);
                            });
                });
    }

    @Test
    @MediumTest
    public void testAccessoryVisibilityChangedByModel() throws InterruptedException {
        // Initially, there shouldn't be a view yet.
        assertNull(mKeyboardAccessoryView.poll());

        // After setting the visibility to true, the view should exist and be visible.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                });
        KeyboardAccessoryView view = mKeyboardAccessoryView.take();
        assertEquals(view.getVisibility(), View.VISIBLE);

        // After hiding the view, the view should still exist but be invisible.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, false);
                });
        assertNotEquals(view.getVisibility(), View.VISIBLE);
    }

    @Test
    @MediumTest
    public void testClicksWhileViewObscuredNotAllowed() throws InterruptedException {
        // Initially, there shouldn't be a view yet.
        assertNull(mKeyboardAccessoryView.poll());

        // After setting the visibility to true, the view should exist and be visible.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                });
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(false));
    }

    @Test
    @MediumTest
    public void testAddsClickableAutofillSuggestions() {
        AtomicReference<Boolean> clickRecorded = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS)
                            .set(
                                    createAutofillChipAndTab(
                                            "Johnathan", result -> clickRecorded.set(true)));
                });

        onViewWaiting(withText("Johnathan")).perform(click());

        assertTrue(clickRecorded.get());
    }

    @Test
    @MediumTest
    public void testAddsLongClickableAutofillSuggestions() {
        AtomicReference<Boolean> clickRecorded = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS)
                            .set(
                                    new BarItem[] {
                                        new AutofillBarItem(
                                                new AutofillSuggestion.Builder()
                                                        .setLabel("Johnathan")
                                                        .setSubLabel("Smith")
                                                        .setItemTag("")
                                                        .setSuggestionType(
                                                                SuggestionType.ADDRESS_ENTRY)
                                                        .setFeatureForIPH("")
                                                        .setApplyDeactivatedStyle(false)
                                                        .build(),
                                                new Action(
                                                        AUTOFILL_SUGGESTION,
                                                        result -> {},
                                                        result -> clickRecorded.set(true))),
                                        createSheetOpener()
                                    });
                });

        onViewWaiting(withText("Johnathan")).perform(longClick());

        assertTrue(clickRecorded.get());
    }

    @Test
    @MediumTest
    public void testCanAddSingleButtons() {
        BarItem generatePasswordItem =
                new BarItem(
                        BarItem.Type.ACTION_BUTTON,
                        new Action(GENERATE_PASSWORD_AUTOMATIC, unused -> {}),
                        R.string.password_generation_accessory_button);
        BarItem credmanItem =
                new BarItem(
                        BarItem.Type.ACTION_CHIP,
                        new Action(CREDMAN_CONDITIONAL_UI_REENTRY, unused -> {}),
                        R.string.more_passkeys);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS)
                            .set(
                                    new BarItem[] {
                                        generatePasswordItem, credmanItem, createSheetOpener()
                                    });
                });

        onViewWaiting(withText(R.string.password_generation_accessory_button));
        onViewWaiting(withText(R.string.more_passkeys));
        onView(withText(R.string.password_generation_accessory_button))
                .check(matches(isDisplayed()));
        onView(withText(R.string.more_passkeys)).check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    public void testCanRemoveSingleButtons() {
        BarItem generatePasswordsItem =
                new BarItem(
                        BarItem.Type.ACTION_BUTTON,
                        new Action(GENERATE_PASSWORD_AUTOMATIC, unused -> {}),
                        R.string.password_generation_accessory_button);
        BarItem credmanItem =
                new BarItem(
                        BarItem.Type.ACTION_CHIP,
                        new Action(CREDMAN_CONDITIONAL_UI_REENTRY, unused -> {}),
                        R.string.more_passkeys);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS)
                            .set(
                                    new BarItem[] {
                                        generatePasswordsItem, credmanItem, createSheetOpener()
                                    });
                });

        onViewWaiting(withText(R.string.password_generation_accessory_button));
        onView(withText(R.string.password_generation_accessory_button))
                .check(matches(isDisplayed()));
        onView(withText(R.string.more_passkeys)).check(matches(isDisplayed()));

        ThreadUtils.runOnUiThreadBlocking(
                () -> mModel.get(BAR_ITEMS).remove(mModel.get(BAR_ITEMS).get(1)));

        ViewUtils.waitForViewCheckingState(
                withText(R.string.more_passkeys), VIEW_INVISIBLE | VIEW_GONE | VIEW_NULL);
        onView(withText(R.string.password_generation_accessory_button))
                .check(matches(isDisplayed()));
        onView(withText(R.string.more_passkeys)).check(doesNotExist());
    }

    @Test
    @MediumTest
    public void testUpdatesKeyPaddingAfterRotation() throws InterruptedException {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(createAutofillChipAndTab("John", null));
                });
        KeyboardAccessoryView view = mKeyboardAccessoryView.take();
        CriteriaHelper.pollUiThread(
                () -> view.mBarItemsView.isShown() && view.mBarItemsView.getChildAt(1) != null);
        CriteriaHelper.pollUiThread(viewsAreRightAligned(view, view.mBarItemsView.getChildAt(1)));

        rotateActivityToLandscape();

        CriteriaHelper.pollUiThread(view.mBarItemsView::isShown);
        CriteriaHelper.pollUiThread(viewsAreRightAligned(view, view.mBarItemsView.getChildAt(1)));

        // Reset device orientation.
        mActivityTestRule
                .getActivity()
                .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
    }

    @Test
    @MediumTest
    public void testDismissesPlusAddressEducationBubbleOnFilling() throws InterruptedException {
        AutofillBarItem itemWithIPH =
                new AutofillBarItem(
                        new AutofillSuggestion.Builder()
                                .setLabel("Create plus address")
                                .setSubLabel("")
                                .setItemTag("")
                                .setSuggestionType(SuggestionType.CREATE_NEW_PLUS_ADDRESS)
                                .setFeatureForIPH("")
                                .setIPHDescriptionText("IPH description")
                                .setApplyDeactivatedStyle(false)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));
        itemWithIPH.setFeatureForIPH(
                FeatureConstants.KEYBOARD_ACCESSORY_PLUS_ADDRESS_CREATE_SUGGESTION);

        TestTracker tracker = new TestTracker();
        TrackerFactory.setTrackerForTests(tracker);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {itemWithIPH, createSheetOpener()});
                });

        onViewWaiting(withText("Create plus address"));
        waitForHelpBubble(withText("IPH description"));
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(true));
        onView(withChild(withText("Create plus address"))).check(matches(isSelected()));
        onView(withText("Create plus address")).perform(click());

        assertThat(tracker.wasDismissed(), is(true));
        assertThat(
                tracker.getLastEmittedEvent(),
                is(EventConstants.KEYBOARD_ACCESSORY_PLUS_ADDRESS_CREATE_SUGGESTION));
        onView(withChild(withText("Create plus address"))).check(matches(not(isSelected())));
    }

    @Test
    @MediumTest
    public void testDismissesPasswordEducationBubbleOnFilling() throws InterruptedException {
        AutofillBarItem itemWithIPH =
                new AutofillBarItem(
                        new AutofillSuggestion.Builder()
                                .setLabel("Johnathan")
                                .setSubLabel("Smith")
                                .setItemTag("")
                                .setSuggestionType(SuggestionType.PASSWORD_ENTRY)
                                .setFeatureForIPH("")
                                .setApplyDeactivatedStyle(false)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));
        itemWithIPH.setFeatureForIPH(FeatureConstants.KEYBOARD_ACCESSORY_PASSWORD_FILLING_FEATURE);

        TestTracker tracker = new TestTracker();
        TrackerFactory.setTrackerForTests(tracker);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {itemWithIPH, createSheetOpener()});
                });

        onViewWaiting(withText("Johnathan"));
        waitForHelpBubble(withText(R.string.iph_keyboard_accessory_fill_with_chrome));
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(true));
        onView(withChild(withText("Johnathan"))).check(matches(isSelected()));
        onView(withText("Johnathan")).perform(click());

        assertThat(tracker.wasDismissed(), is(true));
        assertThat(
                tracker.getLastEmittedEvent(),
                is(EventConstants.KEYBOARD_ACCESSORY_PASSWORD_AUTOFILLED));
        onView(withChild(withText("Johnathan"))).check(matches(not(isSelected())));
    }

    @Test
    @MediumTest
    public void testDismissesAddressEducationBubbleOnFilling() throws InterruptedException {
        AutofillBarItem itemWithIPH =
                new AutofillBarItem(
                        new AutofillSuggestion.Builder()
                                .setLabel("Johnathan")
                                .setSubLabel("Smith")
                                .setItemTag("")
                                .setSuggestionType(SuggestionType.ADDRESS_ENTRY)
                                .setFeatureForIPH("")
                                .setApplyDeactivatedStyle(false)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));
        itemWithIPH.setFeatureForIPH(FeatureConstants.KEYBOARD_ACCESSORY_ADDRESS_FILL_FEATURE);

        TestTracker tracker = new TestTracker();
        TrackerFactory.setTrackerForTests(tracker);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {itemWithIPH, createSheetOpener()});
                });

        onViewWaiting(withText("Johnathan"));
        waitForHelpBubble(withText(R.string.iph_keyboard_accessory_fill_with_chrome));
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(true));
        onView(withText("Johnathan")).perform(click());

        assertThat(tracker.wasDismissed(), is(true));
        assertThat(
                tracker.getLastEmittedEvent(),
                is(EventConstants.KEYBOARD_ACCESSORY_ADDRESS_AUTOFILLED));
    }

    @Test
    @MediumTest
    public void testDismissesPaymentEducationBubbleOnFilling() throws InterruptedException {
        AutofillBarItem itemWithIPH =
                new AutofillBarItem(
                        new AutofillSuggestion.Builder()
                                .setLabel("Johnathan")
                                .setSubLabel("Smith")
                                .setItemTag("")
                                .setSuggestionType(SuggestionType.CREDIT_CARD_ENTRY)
                                .setFeatureForIPH("")
                                .setApplyDeactivatedStyle(false)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));
        itemWithIPH.setFeatureForIPH(FeatureConstants.KEYBOARD_ACCESSORY_PAYMENT_FILLING_FEATURE);

        TestTracker tracker = new TestTracker();
        TrackerFactory.setTrackerForTests(tracker);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {itemWithIPH, createSheetOpener()});
                });

        onViewWaiting(withText("Johnathan"));
        waitForHelpBubble(withText(R.string.iph_keyboard_accessory_fill_with_chrome));
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(true));
        onView(withText("Johnathan")).perform(click());

        assertThat(tracker.wasDismissed(), is(true));
        assertThat(
                tracker.getLastEmittedEvent(),
                is(EventConstants.KEYBOARD_ACCESSORY_PAYMENT_AUTOFILLED));
    }

    @Test
    @MediumTest
    public void testDismissesSwipingEducationBubbleOnTap() throws InterruptedException {
        TestTracker tracker =
                new TestTracker() {
                    @Override
                    public int getTriggerState(String feature) {
                        // Pretend that an autofill IPH was shown already.
                        return feature.equals(
                                        FeatureConstants
                                                .KEYBOARD_ACCESSORY_PASSWORD_FILLING_FEATURE)
                                ? TriggerState.HAS_BEEN_DISPLAYED
                                : TriggerState.HAS_NOT_BEEN_DISPLAYED;
                    }
                };
        TrackerFactory.setTrackerForTests(tracker);

        // Render a keyboard accessory bar and wait for completion.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(createAutofillChipAndTab("Johnathan", null));
                });
        onViewWaiting(withText("Johnathan"));

        // Pretend an item is offscreen, so swiping is possible and an IPH could be shown.
        ThreadUtils.runOnUiThreadBlocking(() -> mModel.set(SHOW_SWIPING_IPH, true));

        // Wait until the bubble appears, then dismiss is by tapping it.
        waitForHelpBubble(withText(R.string.iph_keyboard_accessory_swipe_for_more));
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(true));
        waitForHelpBubble(withText(R.string.iph_keyboard_accessory_swipe_for_more))
                .perform(click());
        assertThat(tracker.wasDismissed(), is(true));
    }

    @Test
    @MediumTest
    public void testDismissesPaymentOfferEducationBubbleOnFilling() throws InterruptedException {
        String itemTag = "Cashback linked";
        AutofillBarItem itemWithIPH =
                new AutofillBarItem(
                        new AutofillSuggestion.Builder()
                                .setLabel("Johnathan")
                                .setSubLabel("Smith")
                                .setItemTag(itemTag)
                                .setIconId(R.drawable.ic_offer_tag)
                                .setSuggestionType(SuggestionType.CREDIT_CARD_ENTRY)
                                .setFeatureForIPH("")
                                .setApplyDeactivatedStyle(false)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));
        itemWithIPH.setFeatureForIPH(FeatureConstants.KEYBOARD_ACCESSORY_PAYMENT_OFFER_FEATURE);

        TestTracker tracker = new TestTracker();
        TrackerFactory.setTrackerForTests(tracker);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {itemWithIPH, createSheetOpener()});
                });

        onViewWaiting(withText("Johnathan"));
        waitForHelpBubble(withText(itemTag));
        assertThat(mKeyboardAccessoryView.take().areClicksAllowedWhenObscured(), is(true));
        onView(withText("Johnathan")).perform(click());

        assertThat(tracker.wasDismissed(), is(true));
        assertThat(
                tracker.getLastEmittedEvent(),
                is(EventConstants.KEYBOARD_ACCESSORY_PAYMENT_AUTOFILLED));
    }

    @Test
    @MediumTest
    public void testNotifiesAboutPartiallyVisibleSuggestions() throws InterruptedException {
        // Ensure that the callback isn't triggered while all items are visible:
        AtomicInteger obfuscatedChildAt = new AtomicInteger(-1);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(OBFUSCATED_CHILD_AT_CALLBACK, obfuscatedChildAt::set);
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(createAutofillChipAndTab("John", null));
                });
        KeyboardAccessoryView view = mKeyboardAccessoryView.take();
        CriteriaHelper.pollUiThread(() -> view.mBarItemsView.getChildCount() > 0);
        assertThat(obfuscatedChildAt.get(), is(-1));

        // As soon as at least one item can't be displayed in full, trigger the swiping callback.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.get(BAR_ITEMS)
                            .set(
                                    new BarItem[] {
                                        createAutofillBarItem("JohnathanSmith", null),
                                        createAutofillBarItem("TroyMcSpartanGregor", null),
                                        createAutofillBarItem("SomeOtherRandomLongName", null),
                                        createAutofillBarItem("ToddTester", null),
                                        createAutofillBarItem("MayaPark", null),
                                        createAutofillBarItem("ThisChipIsProbablyHiddenNow", null),
                                        createSheetOpener()
                                    });
                });
        onViewWaiting(withText("JohnathanSmith"));
        CriteriaHelper.pollUiThread(() -> obfuscatedChildAt.get() > -1);
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES)
    public void testCustomIconUrlSet_imageReturnedByPersonalDataManager_customIconSetOnChipView()
            throws InterruptedException {
        GURL customIconUrl = mock(GURL.class);
        when(customIconUrl.isValid()).thenReturn(true);
        when(customIconUrl.getSpec()).thenReturn(CUSTOM_ICON_URL);
        // Return the cached image when
        // PersonalDataManager.getCustomImageForAutofillSuggestionIfAvailable is called for the
        // above url.
        when(mMockPersonalDataManager.getCustomImageForAutofillSuggestionIfAvailable(any(), any()))
                .thenReturn(Optional.of(TEST_CARD_ART_IMAGE));
        // Create an autofill suggestion and set the `customIconUrl`.
        AutofillBarItem customIconItem =
                new AutofillBarItem(
                        getDefaultAutofillSuggestionBuilder()
                                .setCustomIconUrl(customIconUrl)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {customIconItem, createSheetOpener()});
                });
        KeyboardAccessoryView view = mKeyboardAccessoryView.take();

        CriteriaHelper.pollUiThread(() -> view.mBarItemsView.getChildCount() > 0);
        CriteriaHelper.pollUiThread(
                () -> {
                    ChipView chipView = (ChipView) view.mBarItemsView.getChildAt(0);
                    ChromeImageView iconImageView = (ChromeImageView) chipView.getChildAt(0);
                    return ((BitmapDrawable) iconImageView.getDrawable())
                            .getBitmap()
                            .equals(TEST_CARD_ART_IMAGE);
                });
    }

    @Test
    @MediumTest
    public void testCustomIconUrlSet_imageNotCachedInPersonalDataManager_defaultIconSetOnChipView()
            throws InterruptedException {
        GURL customIconUrl = mock(GURL.class);
        when(customIconUrl.isValid()).thenReturn(true);
        when(customIconUrl.getSpec()).thenReturn(CUSTOM_ICON_URL);
        // Return the response of PersonalDataManager.getCustomImageForAutofillSuggestionIfAvailable
        // to null to indicate that the image is not present in the cache.
        when(mMockPersonalDataManager.getCustomImageForAutofillSuggestionIfAvailable(any(), any()))
                .thenReturn(Optional.empty());
        AutofillBarItem customIconItem =
                new AutofillBarItem(
                        getDefaultAutofillSuggestionBuilder()
                                .setCustomIconUrl(customIconUrl)
                                .build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS).set(new BarItem[] {customIconItem, createSheetOpener()});
                });
        KeyboardAccessoryView view = mKeyboardAccessoryView.take();

        CriteriaHelper.pollUiThread(() -> view.mBarItemsView.getChildCount() > 0);
        CriteriaHelper.pollUiThread(
                () -> {
                    ChipView chipView = (ChipView) view.mBarItemsView.getChildAt(0);
                    ChromeImageView iconImageView = (ChromeImageView) chipView.getChildAt(0);
                    Drawable expectedIcon =
                            mActivityTestRule.getActivity().getDrawable(R.drawable.visa_card);
                    return getBitmap(expectedIcon).sameAs(getBitmap(iconImageView.getDrawable()));
                });
    }

    @Test
    @MediumTest
    public void testCustomIconUrlNotSet_defaultIconSetOnChipView() throws InterruptedException {
        // Create an autofill suggestion without setting the `customIconUrl`.
        AutofillBarItem itemWithoutCustomIconUrl =
                new AutofillBarItem(
                        getDefaultAutofillSuggestionBuilder().build(),
                        new Action(AUTOFILL_SUGGESTION, unused -> {}));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS)
                            .set(new BarItem[] {itemWithoutCustomIconUrl, createSheetOpener()});
                });
        KeyboardAccessoryView view = mKeyboardAccessoryView.take();

        CriteriaHelper.pollUiThread(() -> view.mBarItemsView.getChildCount() > 0);
        CriteriaHelper.pollUiThread(
                () -> {
                    ChipView chipView = (ChipView) view.mBarItemsView.getChildAt(0);
                    ChromeImageView iconImageView = (ChromeImageView) chipView.getChildAt(0);
                    Drawable expectedIcon =
                            mActivityTestRule.getActivity().getDrawable(R.drawable.visa_card);
                    return getBitmap(expectedIcon).sameAs(getBitmap(iconImageView.getDrawable()));
                });
    }

    @Test
    @MediumTest
    public void testClickDisabledForNonAcceptableAutofillSuggestions() throws InterruptedException {
        AtomicReference<Boolean> clickRecorded = new AtomicReference<>(false);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.set(VISIBLE, true);
                    mModel.get(BAR_ITEMS)
                            .set(
                                    new BarItem[] {
                                        new AutofillBarItem(
                                                new AutofillSuggestion.Builder()
                                                        .setLabel("Virtual Card")
                                                        .setSubLabel("Disabled")
                                                        .setItemTag("")
                                                        .setSuggestionType(
                                                                SuggestionType.CREDIT_CARD_ENTRY)
                                                        .setFeatureForIPH("")
                                                        .setApplyDeactivatedStyle(true)
                                                        .build(),
                                                new Action(
                                                        AUTOFILL_SUGGESTION,
                                                        result -> clickRecorded.set(true),
                                                        result -> clickRecorded.set(true))),
                                        createSheetOpener()
                                    });
                });

        onView(withText("Virtual Card")).perform(click());
        assertFalse(clickRecorded.get());
    }

    private static AutofillSuggestion.Builder getDefaultAutofillSuggestionBuilder() {
        return new AutofillSuggestion.Builder()
                .setLabel("Johnathan")
                .setSubLabel("Smith")
                .setIconId(R.drawable.visa_card)
                .setSuggestionType(SuggestionType.ADDRESS_ENTRY);
    }

    // Convert a drawable to a Bitmap for comparison.
    private static Bitmap getBitmap(Drawable drawable) {
        Bitmap bitmap =
                Bitmap.createBitmap(
                        drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight(),
                        Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }

    private ViewInteraction waitForHelpBubble(Matcher<View> matcher) {
        View mainDecorView = mActivityTestRule.getActivity().getWindow().getDecorView();
        return onView(isRoot())
                .inRoot(RootMatchers.withDecorView(not(is(mainDecorView))))
                .check(ViewUtils.isEventuallyVisible(matcher));
    }

    private void rotateActivityToLandscape() {
        mActivityTestRule
                .getActivity()
                .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    try {
                        String result =
                                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                                        mActivityTestRule.getWebContents(),
                                        "screen.orientation.type.split('-')[0]");
                        Criteria.checkThat(result, is("\"landscape\""));
                    } catch (TimeoutException ex) {
                        throw new CriteriaNotSatisfiedException(ex);
                    }
                });
    }

    private Runnable viewsAreRightAligned(View staticView, View changingView) {
        Rect accessoryViewRect = new Rect();
        staticView.getGlobalVisibleRect(accessoryViewRect);
        return () -> {
            Rect keyItemRect = new Rect();
            changingView.getGlobalVisibleRect(keyItemRect);
            Criteria.checkThat(keyItemRect.right, is(accessoryViewRect.right));
        };
    }

    private BarItem[] createAutofillChipAndTab(String label, Callback<Action> chipCallback) {
        return new BarItem[] {createAutofillBarItem(label, chipCallback), createSheetOpener()};
    }

    private AutofillBarItem createAutofillBarItem(String label, Callback<Action> chipCallback) {
        return new AutofillBarItem(
                new AutofillSuggestion.Builder()
                        .setLabel(label)
                        .setSubLabel("Smith")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.ADDRESS_ENTRY)
                        .setFeatureForIPH("")
                        .setApplyDeactivatedStyle(false)
                        .build(),
                new Action(AUTOFILL_SUGGESTION, chipCallback));
    }

    private SheetOpenerBarItem createSheetOpener() {
        return new SheetOpenerBarItem(
                new KeyboardAccessoryButtonGroupCoordinator.SheetOpenerCallbacks() {
                    @Override
                    public void onViewBound(View buttons) {
                        if (((KeyboardAccessoryButtonGroupView) buttons).getButtons().size() > 0) {
                            return;
                        }
                        ((KeyboardAccessoryButtonGroupView) buttons)
                                .addButton(
                                        buttons.getContext()
                                                .getDrawable(R.drawable.ic_password_manager_key),
                                        "Key Icon");
                    }

                    @Override
                    public void onViewUnbound(View buttons) {}
                });
    }
}