chromium/chrome/android/features/keyboard_accessory/junit/src/org/chromium/chrome/browser/keyboard_accessory/bar_component/KeyboardAccessoryControllerTest.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.matcher.ViewMatchers.assertThat;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
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.ANIMATION_LISTENER;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.BAR_ITEMS;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.HAS_SUGGESTIONS;
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.SHOW_SWIPING_IPH;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.SKIP_CLOSING_ANIMATION;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.VISIBLE;

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

import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.test.CustomShadowAsyncTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManagerFactory;
import org.chromium.chrome.browser.keyboard_accessory.AccessoryAction;
import org.chromium.chrome.browser.keyboard_accessory.ManualFillingMetricsRecorder;
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.button_group_component.KeyboardAccessoryButtonGroupCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.keyboard_accessory.data.PropertyProvider;
import org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetCoordinator;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.autofill.AutofillDelegate;
import org.chromium.components.autofill.AutofillSuggestion;
import org.chromium.components.autofill.SuggestionType;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.ui.modelutil.ListObservable;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyObservable.PropertyObserver;
import org.chromium.ui.test.util.modelutil.FakeViewProvider;

import java.util.List;

/** Controller tests for the keyboard accessory component. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {CustomShadowAsyncTask.class})
public class KeyboardAccessoryControllerTest {
    @Mock private PropertyObserver<PropertyKey> mMockPropertyObserver;
    @Mock private ListObservable.ListObserver<Void> mMockActionListObserver;
    @Mock private KeyboardAccessoryCoordinator.BarVisibilityDelegate mMockBarVisibilityDelegate;
    @Mock private AccessorySheetCoordinator.SheetVisibilityDelegate mMockSheetVisibilityDelegate;
    @Mock private KeyboardAccessoryView mMockView;
    @Mock private KeyboardAccessoryButtonGroupCoordinator mMockButtonGroup;
    @Mock private KeyboardAccessoryCoordinator.TabSwitchingDelegate mMockTabSwitchingDelegate;
    @Mock private AutofillDelegate mMockAutofillDelegate;
    @Mock private Profile mMockProfile;
    @Mock private PersonalDataManager mMockPersonalDataManager;

    private final KeyboardAccessoryData.Tab mTestTab =
            new KeyboardAccessoryData.Tab("Passwords", null, null, 0, 0, null);

    private KeyboardAccessoryCoordinator mCoordinator;
    private PropertyModel mModel;
    private KeyboardAccessoryMediator mMediator;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(mMockButtonGroup.getTabSwitchingDelegate()).thenReturn(mMockTabSwitchingDelegate);
        PersonalDataManagerFactory.setInstanceForTesting(mMockPersonalDataManager);
        mCoordinator =
                new KeyboardAccessoryCoordinator(
                        ContextUtils.getApplicationContext(),
                        mMockProfile,
                        mMockButtonGroup,
                        mMockBarVisibilityDelegate,
                        mMockSheetVisibilityDelegate,
                        new FakeViewProvider<>(mMockView));
        mMediator = mCoordinator.getMediatorForTesting();
        mModel = mMediator.getModelForTesting();
    }

    @Test
    public void testCreatesValidSubComponents() {
        assertThat(mCoordinator, is(notNullValue()));
        assertThat(mMediator, is(notNullValue()));
        assertThat(mModel, is(notNullValue()));
    }

    @Test
    public void testModelNotifiesVisibilityChangeOnShowAndHide() {
        mModel.addObserver(mMockPropertyObserver);

        // Setting the visibility on the model should make it propagate that it's visible.
        mModel.set(VISIBLE, true);
        verify(mMockPropertyObserver).onPropertyChanged(mModel, VISIBLE);

        assertThat(mModel.get(VISIBLE), is(true));

        // Resetting the visibility on the model to should make it propagate that it's visible.
        mModel.set(VISIBLE, false);
        verify(mMockPropertyObserver, times(2)).onPropertyChanged(mModel, VISIBLE);
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testModelNotifiesAboutActionsChangedByProvider() {
        // Set a default tab to prevent visibility changes to trigger now:
        setTabs(new KeyboardAccessoryData.Tab[] {mTestTab});
        mModel.get(BAR_ITEMS).addObserver(mMockActionListObserver);

        PropertyProvider<Action[]> testProvider =
                new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
        mCoordinator.registerActionProvider(testProvider);

        // If the coordinator receives an initial action, the model should report an insertion.
        mCoordinator.show();

        Action testAction = new Action(0, null);
        testProvider.notifyObservers(new Action[] {testAction});
        // 1 item inserted, sheet opener is moved to the end.
        verify(mMockActionListObserver).onItemRangeChanged(mModel.get(BAR_ITEMS), 0, 1, null);
        verify(mMockActionListObserver).onItemRangeInserted(mModel.get(BAR_ITEMS), 1, 1);
        assertThat(mModel.get(BAR_ITEMS).size(), is(2)); // Plus tab switcher.
        assertThat(mModel.get(BAR_ITEMS).get(0).getAction(), is(equalTo(testAction)));

        // If the coordinator receives a new set of actions, the model should report a change.
        testProvider.notifyObservers(new Action[] {testAction});
        verify(mMockActionListObserver).onItemRangeChanged(mModel.get(BAR_ITEMS), 0, 2, null);
        assertThat(mModel.get(BAR_ITEMS).size(), is(2)); // Plus tab switcher.
        assertThat(mModel.get(BAR_ITEMS).get(0).getAction(), is(equalTo(testAction)));

        // If the coordinator receives an empty set of actions, the model should report a deletion.
        testProvider.notifyObservers(new Action[] {});
        // First call of onItemRangeChanged(mModel.get(BAR_ITEMS), 0, 1, null);
        verify(mMockActionListObserver).onItemRangeRemoved(mModel.get(BAR_ITEMS), 1, 1);
        assertThat(mModel.get(BAR_ITEMS).size(), is(1)); // Only the tab switcher.

        // There should be no notification if no actions are reported repeatedly.
        testProvider.notifyObservers(new Action[] {});
        verify(mMockActionListObserver, times(3))
                .onItemRangeChanged(mModel.get(BAR_ITEMS), 0, 1, null);
        verifyNoMoreInteractions(mMockActionListObserver);
    }

    @Test
    public void testModelDoesntNotifyUnchangedVisibility() {
        mModel.addObserver(mMockPropertyObserver);

        // Setting the visibility on the model should make it propagate that it's visible.
        mModel.set(VISIBLE, true);
        verify(mMockPropertyObserver).onPropertyChanged(mModel, VISIBLE);
        assertThat(mModel.get(VISIBLE), is(true));

        // Marking it as visible again should not result in a notification.
        mModel.set(VISIBLE, true);
        verify(mMockPropertyObserver) // Unchanged number of invocations.
                .onPropertyChanged(mModel, VISIBLE);
        assertThat(mModel.get(VISIBLE), is(true));
    }

    @Test
    public void testTogglesVisibility() {
        mCoordinator.show();
        assertThat(mModel.get(VISIBLE), is(true));
        mCoordinator.dismiss();
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testSortsActionsBasedOnType() {
        PropertyProvider<Action[]> generationProvider =
                new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        PropertyProvider<Action[]> credManProvider =
                new PropertyProvider<>(CREDMAN_CONDITIONAL_UI_REENTRY);

        mCoordinator.registerActionProvider(generationProvider);
        mCoordinator.registerActionProvider(credManProvider);
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);

        AutofillSuggestion suggestion1 =
                new AutofillSuggestion.Builder()
                        .setLabel("FirstSuggestion")
                        .setSubLabel("")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.AUTOCOMPLETE_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        AutofillSuggestion suggestion2 =
                new AutofillSuggestion.Builder()
                        .setLabel("SecondSuggestion")
                        .setSubLabel("")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.AUTOCOMPLETE_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        Action generationAction = new Action(GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
        Action credManAction = new Action(CREDMAN_CONDITIONAL_UI_REENTRY, (a) -> {});
        autofillSuggestionProvider.notifyObservers(List.of(suggestion1, suggestion2));
        generationProvider.notifyObservers(new Action[] {generationAction});
        credManProvider.notifyObservers(new Action[] {credManAction});

        // CredManAction should come later than suggestions but before the tab layout.
        assertThat(mModel.get(BAR_ITEMS).size(), is(5));
        assertThat(mModel.get(BAR_ITEMS).get(0).getAction(), is(generationAction));
        assertThat(
                mModel.get(BAR_ITEMS).get(0).getCaptionId(),
                is(R.string.password_generation_accessory_button));
        assertThat(mModel.get(BAR_ITEMS).get(1), instanceOf(AutofillBarItem.class));
        AutofillBarItem autofillBarItem1 = (AutofillBarItem) mModel.get(BAR_ITEMS).get(1);
        assertThat(autofillBarItem1.getSuggestion(), is(suggestion1));
        assertThat(mModel.get(BAR_ITEMS).get(2), instanceOf(AutofillBarItem.class));
        AutofillBarItem autofillBarItem2 = (AutofillBarItem) mModel.get(BAR_ITEMS).get(2);
        assertThat(autofillBarItem2.getSuggestion(), is(suggestion2));
        assertThat(mModel.get(BAR_ITEMS).get(3).getAction(), is(credManAction));
        assertThat(mModel.get(BAR_ITEMS).get(3).getCaptionId(), is(R.string.select_passkey));
        assertThat(mModel.get(BAR_ITEMS).get(4).getViewType(), is(BarItem.Type.TAB_LAYOUT));
    }

    @Test
    public void testChangesCaptionIdForCredManEntry() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        PropertyProvider<Action[]> credManProvider =
                new PropertyProvider<>(CREDMAN_CONDITIONAL_UI_REENTRY);

        mCoordinator.registerActionProvider(credManProvider);
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);

        AutofillSuggestion suggestion =
                new AutofillSuggestion.Builder()
                        .setLabel("bulbasaur")
                        .setSubLabel("passkey")
                        .setSuggestionType(SuggestionType.WEBAUTHN_CREDENTIAL)
                        .build();
        Action credManAction = new Action(CREDMAN_CONDITIONAL_UI_REENTRY, (a) -> {});
        autofillSuggestionProvider.notifyObservers(List.of(suggestion));
        credManProvider.notifyObservers(new Action[] {credManAction});

        assertThat(mModel.get(BAR_ITEMS).size(), is(3));
        assertThat(mModel.get(BAR_ITEMS).get(0), instanceOf(AutofillBarItem.class));
        AutofillBarItem autofillBarItem = (AutofillBarItem) mModel.get(BAR_ITEMS).get(0);
        assertThat(autofillBarItem.getSuggestion(), is(suggestion));
        assertThat(mModel.get(BAR_ITEMS).get(1).getAction(), is(credManAction));
        assertThat(mModel.get(BAR_ITEMS).get(1).getCaptionId(), is(R.string.more_passkeys));
    }

    @Test
    public void testMovesTabSwitcherToEnd() {
        PropertyProvider<Action[]> generationProvider =
                new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);

        mCoordinator.registerActionProvider(generationProvider);
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);

        AutofillSuggestion.Builder builder = new AutofillSuggestion.Builder().setSubLabel("");
        AutofillSuggestion suggestion1 = builder.setLabel("kayseri").build();
        AutofillSuggestion suggestion2 = builder.setLabel("spor").build();
        Action generationAction = new Action(GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
        autofillSuggestionProvider.notifyObservers(List.of(suggestion1, suggestion2));
        generationProvider.notifyObservers(new Action[] {generationAction});

        // Autofill suggestions should always come last, independent of when they were added.
        assertThat(mModel.get(BAR_ITEMS).size(), is(4)); // Additional tab switcher
        assertThat(mModel.get(BAR_ITEMS).get(0).getAction(), is(generationAction));
        assertThat(mModel.get(BAR_ITEMS).get(1).getViewType(), is(BarItem.Type.SUGGESTION));
        assertThat(
                ((AutofillBarItem) mModel.get(BAR_ITEMS).get(1)).getSuggestion(), is(suggestion1));
        assertThat(mModel.get(BAR_ITEMS).get(2).getViewType(), is(BarItem.Type.SUGGESTION));
        assertThat(
                ((AutofillBarItem) mModel.get(BAR_ITEMS).get(2)).getSuggestion(), is(suggestion2));
        assertThat(mModel.get(BAR_ITEMS).get(3).getViewType(), is(BarItem.Type.TAB_LAYOUT));
    }

    @Test
    public void testDeletingActionsAffectsOnlyOneType() {
        PropertyProvider<Action[]> generationProvider =
                new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);

        mCoordinator.registerActionProvider(generationProvider);
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);

        AutofillSuggestion suggestion =
                new AutofillSuggestion.Builder()
                        .setLabel("Suggestion")
                        .setSubLabel("")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.AUTOCOMPLETE_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        Action generationAction = new Action(GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
        autofillSuggestionProvider.notifyObservers(List.of(suggestion, suggestion));
        generationProvider.notifyObservers(new Action[] {generationAction});
        assertThat(mModel.get(BAR_ITEMS).size(), is(4));

        // Drop all Autofill suggestions. Only the generation action should remain.
        autofillSuggestionProvider.notifyObservers(List.of());
        assertThat(mModel.get(BAR_ITEMS).size(), is(2));
        assertThat(mModel.get(BAR_ITEMS).get(0).getAction(), is(generationAction));

        // Readd an Autofill suggestion and drop the generation. Only the suggestion should remain.
        autofillSuggestionProvider.notifyObservers(List.of(suggestion));
        generationProvider.notifyObservers(new Action[0]);
        assertThat(mModel.get(BAR_ITEMS).size(), is(2));
        assertThat(mModel.get(BAR_ITEMS).get(0), instanceOf(AutofillBarItem.class));
        AutofillBarItem autofillBarItem = (AutofillBarItem) mModel.get(BAR_ITEMS).get(0);
        assertThat(autofillBarItem.getSuggestion(), is(suggestion));
    }

    @Test
    public void testGenerationActionsRemovedWhenNotVisible() {
        // Make the accessory visible and add an action to it.
        mCoordinator.show();
        // Ignore tab switcher item.
        assertThat(mModel.get(BAR_ITEMS).size(), is(1));
        mModel.get(BAR_ITEMS)
                .add(
                        new BarItem(
                                BarItem.Type.ACTION_BUTTON,
                                new Action(GENERATE_PASSWORD_AUTOMATIC, null),
                                /* captionId= */ 0));
        assertThat(mModel.get(BAR_ITEMS).size(), is(2));

        // Hiding the accessory should also remove actions.
        mCoordinator.dismiss();
        assertThat(mModel.get(BAR_ITEMS).size(), is(1));
    }

    @Test
    public void testCreatesAddressItemWithIPH() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        AutofillSuggestion addressSuggestion =
                new AutofillSuggestion.Builder()
                        .setLabel("John")
                        .setSubLabel("Main Str")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.ADDRESS_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);
        autofillSuggestionProvider.notifyObservers(
                List.of(addressSuggestion, addressSuggestion, addressSuggestion));

        // assertThat(getAutofillItemAt(0).getFeatureForIPH(), is(nullValue()));
        // mCoordinator.prepareUserEducation();
        assertThat(
                getAutofillItemAt(0).getFeatureForIPH(),
                is(FeatureConstants.KEYBOARD_ACCESSORY_ADDRESS_FILL_FEATURE));
        assertThat(getAutofillItemAt(1).getFeatureForIPH(), is(nullValue()));
        assertThat(getAutofillItemAt(2).getFeatureForIPH(), is(nullValue()));
    }

    @Test
    public void testCreatesPaymentItemWithIPH() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        AutofillSuggestion paymentSuggestion =
                new AutofillSuggestion.Builder()
                        .setLabel("John")
                        .setSubLabel("4828 ****")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.CREDIT_CARD_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);
        autofillSuggestionProvider.notifyObservers(
                List.of(paymentSuggestion, paymentSuggestion, paymentSuggestion));

        // assertThat(getAutofillItemAt(0).getFeatureForIPH(), is(nullValue()));
        // mCoordinator.prepareUserEducation();
        assertThat(
                getAutofillItemAt(0).getFeatureForIPH(),
                is(FeatureConstants.KEYBOARD_ACCESSORY_PAYMENT_FILLING_FEATURE));
        assertThat(getAutofillItemAt(1).getFeatureForIPH(), is(nullValue()));
        assertThat(getAutofillItemAt(2).getFeatureForIPH(), is(nullValue()));
    }

    @Test
    public void testIPHFeatureSetForAutofillSuggestion() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        AutofillSuggestion paymentSuggestion =
                new AutofillSuggestion.Builder()
                        .setLabel("John")
                        .setSubLabel("4828 ****")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.CREDIT_CARD_ENTRY)
                        .setFeatureForIPH(
                                FeatureConstants.KEYBOARD_ACCESSORY_PAYMENT_VIRTUAL_CARD_FEATURE)
                        .build();
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);
        autofillSuggestionProvider.notifyObservers(
                List.of(paymentSuggestion, paymentSuggestion, paymentSuggestion));

        // assertThat(getAutofillItemAt(0).getFeatureForIPH(), is(nullValue()));
        // mCoordinator.prepareUserEducation();
        assertThat(
                getAutofillItemAt(0).getFeatureForIPH(),
                is(FeatureConstants.KEYBOARD_ACCESSORY_PAYMENT_VIRTUAL_CARD_FEATURE));
        // Other suggestions also have explicit IPH strings, but only the first suggestion's string
        // is shown.
        assertThat(getAutofillItemAt(1).getFeatureForIPH(), is(nullValue()));
        assertThat(getAutofillItemAt(2).getFeatureForIPH(), is(nullValue()));
    }

    @Test
    public void testCreatesIPHForSecondPasswordItem() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        AutofillSuggestion passwordSuggestion1 =
                new AutofillSuggestion.Builder()
                        .setLabel("John")
                        .setSubLabel("****")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.PASSWORD_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        AutofillSuggestion passwordSuggestion2 =
                new AutofillSuggestion.Builder()
                        .setLabel("Eva")
                        .setSubLabel("*******")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.PASSWORD_ENTRY)
                        .setFeatureForIPH("")
                        .build();
        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);
        autofillSuggestionProvider.notifyObservers(
                List.of(passwordSuggestion1, passwordSuggestion2, passwordSuggestion2));

        // assertThat(getAutofillItemAt(0).getFeatureForIPH(), is(nullValue()));
        // mCoordinator.prepareUserEducation();
        assertThat(getAutofillItemAt(0).getFeatureForIPH(), is(nullValue()));
        assertThat(
                getAutofillItemAt(1).getFeatureForIPH(),
                is(FeatureConstants.KEYBOARD_ACCESSORY_PASSWORD_FILLING_FEATURE));
        assertThat(getAutofillItemAt(2).getFeatureForIPH(), is(nullValue()));
    }

    @Test
    public void testCreatesAddressItemWithExternallyProvidedIPH() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);
        AutofillSuggestion addressSuggestion =
                new AutofillSuggestion.Builder()
                        .setLabel("John")
                        .setSubLabel("Man Str")
                        .setItemTag("")
                        .setSuggestionType(SuggestionType.ADDRESS_ENTRY)
                        .setFeatureForIPH(
                                FeatureConstants
                                        .KEYBOARD_ACCESSORY_EXTERNAL_ACCOUNT_PROFILE_FEATURE)
                        .build();

        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);
        autofillSuggestionProvider.notifyObservers(
                List.of(addressSuggestion, addressSuggestion, addressSuggestion));

        // assertThat(getAutofillItemAt(0).getFeatureForIPH(), is(nullValue()));
        // mCoordinator.prepareUserEducation();
        assertThat(
                getAutofillItemAt(0).getFeatureForIPH(),
                is(FeatureConstants.KEYBOARD_ACCESSORY_EXTERNAL_ACCOUNT_PROFILE_FEATURE));
        assertThat(getAutofillItemAt(1).getFeatureForIPH(), is(nullValue()));
        assertThat(getAutofillItemAt(2).getFeatureForIPH(), is(nullValue()));
    }

    @Test
    public void testSkipAnimationsOnlyUntilNextShow() {
        assertThat(mModel.get(SKIP_CLOSING_ANIMATION), is(false));
        mCoordinator.skipClosingAnimationOnce();
        assertThat(mModel.get(SKIP_CLOSING_ANIMATION), is(true));
        mCoordinator.show();
        assertThat(mModel.get(SKIP_CLOSING_ANIMATION), is(false));
    }

    @Test
    public void testShowSwipingIphUntilVisibilityIsReset() {
        // By default, no IPH is shown but the model holds a callback to notify the mediator.
        mCoordinator.show();
        Callback<Integer> obfuscatedChildAt = mModel.get(OBFUSCATED_CHILD_AT_CALLBACK);
        assertThat(obfuscatedChildAt, notNullValue());
        assertThat(mModel.get(SHOW_SWIPING_IPH), is(false));

        // Notify the mediator to show the IPH because at least one of three items is not visible.
        mModel.get(BAR_ITEMS).add(mock(BarItem.class));
        mModel.get(BAR_ITEMS).add(mock(BarItem.class));
        mModel.get(BAR_ITEMS).add(mock(BarItem.class));
        obfuscatedChildAt.onResult(1);
        assertThat(mModel.get(SHOW_SWIPING_IPH), is(true));

        // Any change that changes the visibility should reset the swiping IPH.
        mModel.set(VISIBLE, false);
        assertThat(mModel.get(SHOW_SWIPING_IPH), is(false));
    }

    @Test
    public void testRecordsAgainIfExistingItemsChange() {
        // Add a tab and show, so the accessory is permanently visible.
        setTabs(new KeyboardAccessoryData.Tab[] {mTestTab});
        mCoordinator.show();

        // Adding an action fills the bar impression bucket and the actions set once.
        mModel.get(BAR_ITEMS)
                .set(
                        new BarItem[] {
                            new BarItem(
                                    BarItem.Type.ACTION_BUTTON,
                                    new Action(GENERATE_PASSWORD_AUTOMATIC, null),
                                    0),
                            new BarItem(
                                    BarItem.Type.ACTION_BUTTON,
                                    new Action(GENERATE_PASSWORD_AUTOMATIC, null),
                                    1)
                        });
        assertThat(getGenerationImpressionCount(), is(1));

        // Adding another action leaves bar impressions unchanged but affects the actions bucket.
        mModel.get(BAR_ITEMS)
                .set(
                        new BarItem[] {
                            new BarItem(
                                    BarItem.Type.ACTION_BUTTON,
                                    new Action(GENERATE_PASSWORD_AUTOMATIC, null),
                                    0),
                            new BarItem(
                                    BarItem.Type.ACTION_BUTTON,
                                    new Action(GENERATE_PASSWORD_AUTOMATIC, null),
                                    1)
                        });
        assertThat(getGenerationImpressionCount(), is(2));
    }

    @Test
    public void testModelChangesUpdatesTheContentDescription() {
        PropertyProvider<List<AutofillSuggestion>> autofillSuggestionProvider =
                new PropertyProvider<>(AUTOFILL_SUGGESTION);

        mCoordinator.registerAutofillProvider(autofillSuggestionProvider, mMockAutofillDelegate);
        autofillSuggestionProvider.notifyObservers(List.of(mock(AutofillSuggestion.class)));

        assertThat(mModel.get(HAS_SUGGESTIONS), is(true));

        autofillSuggestionProvider.notifyObservers(List.of());
        assertThat(mModel.get(HAS_SUGGESTIONS), is(false));
    }

    @Test
    public void testFowardsAnimationEventsToVisibilityDelegate() {
        mModel.get(ANIMATION_LISTENER).onFadeInEnd();
        verify(mMockBarVisibilityDelegate).onBarFadeInAnimationEnd();
    }

    private int getGenerationImpressionCount() {
        return RecordHistogram.getHistogramValueCountForTesting(
                ManualFillingMetricsRecorder.UMA_KEYBOARD_ACCESSORY_ACTION_IMPRESSION,
                AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);
    }

    private void setTabs(KeyboardAccessoryData.Tab[] tabs) {
        mCoordinator.setTabs(tabs);
        when(mMockTabSwitchingDelegate.hasTabs()).thenReturn(true);
    }

    private void setActiveTab(KeyboardAccessoryData.Tab tab) {
        when(mMockTabSwitchingDelegate.getActiveTab()).thenReturn(tab);
        when(mMockTabSwitchingDelegate.hasTabs()).thenReturn(true);
        mCoordinator.getMediatorForTesting().onActiveTabChanged(0);
    }

    private AutofillBarItem getAutofillItemAt(int position) {
        return (AutofillBarItem) mModel.get(BAR_ITEMS).get(position);
    }
}