blob: 280c0773abb0150314577db228f2c88e832e9db2 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.autofill.keyboard_accessory;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
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.autofill.keyboard_accessory.AccessoryAction.AUTOFILL_SUGGESTION;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC;
import android.view.ViewStub;
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.metrics.RecordHistogram;
import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.asynctask.CustomShadowAsyncTask;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.PropertyProvider;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable.PropertyObserver;
/**
* Controller tests for the keyboard accessory component.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE,
shadows = {CustomShadowAsyncTask.class, ShadowRecordHistogram.class})
public class KeyboardAccessoryControllerTest {
@Mock
private PropertyObserver<KeyboardAccessoryModel.PropertyKey> mMockPropertyObserver;
@Mock
private ListObservable.ListObserver<Void> mMockTabListObserver;
@Mock
private ListObservable.ListObserver<Void> mMockActionListObserver;
@Mock
private KeyboardAccessoryCoordinator.VisibilityDelegate mMockVisibilityDelegate;
@Mock
private ViewStub mMockViewStub;
@Mock
private KeyboardAccessoryView mMockView;
private final KeyboardAccessoryData.Tab mTestTab =
new KeyboardAccessoryData.Tab(null, null, 0, 0, null);
private KeyboardAccessoryCoordinator mCoordinator;
private KeyboardAccessoryModel mModel;
private KeyboardAccessoryMediator mMediator;
@Before
public void setUp() {
ShadowRecordHistogram.reset();
MockitoAnnotations.initMocks(this);
when(mMockViewStub.inflate()).thenReturn(mMockView);
mCoordinator = new KeyboardAccessoryCoordinator(mMockViewStub, mMockVisibilityDelegate);
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.setVisible(true);
verify(mMockPropertyObserver)
.onPropertyChanged(mModel, KeyboardAccessoryModel.PropertyKey.VISIBLE);
assertThat(mModel.isVisible(), is(true));
// Resetting the visibility on the model to should make it propagate that it's visible.
mModel.setVisible(false);
verify(mMockPropertyObserver, times(2))
.onPropertyChanged(mModel, KeyboardAccessoryModel.PropertyKey.VISIBLE);
assertThat(mModel.isVisible(), is(false));
}
@Test
public void testChangingTabsNotifiesTabObserver() {
mModel.addTabListObserver(mMockTabListObserver);
// Calling addTab on the coordinator should make model propagate that it has a new tab.
mCoordinator.addTab(mTestTab);
verify(mMockTabListObserver).onItemRangeInserted(mModel.getTabList(), 0, 1);
assertThat(mModel.getTabList().size(), is(1));
assertThat(mModel.getTabList().get(0), is(mTestTab));
// Calling hide on the coordinator should make model propagate that it's invisible.
mCoordinator.removeTab(mTestTab);
verify(mMockTabListObserver).onItemRangeRemoved(mModel.getTabList(), 0, 1);
assertThat(mModel.getTabList().size(), is(0));
}
@Test
public void testModelNotifiesAboutActionsChangedByProvider() {
final PropertyProvider<Action> testProvider =
new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
final Action testAction = new Action(null, 0, null);
mModel.addActionListObserver(mMockActionListObserver);
mCoordinator.registerActionListProvider(testProvider);
// If the coordinator receives an initial actions, the model should report an insertion.
testProvider.notifyObservers(new Action[] {testAction});
verify(mMockActionListObserver).onItemRangeInserted(mModel.getActionList(), 0, 1);
assertThat(mModel.getActionList().size(), is(1));
assertThat(mModel.getActionList().get(0), 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.getActionList(), 0, 1, null);
assertThat(mModel.getActionList().size(), is(1));
assertThat(mModel.getActionList().get(0), is(equalTo(testAction)));
// If the coordinator receives an empty set of actions, the model should report a deletion.
testProvider.notifyObservers(new Action[] {});
verify(mMockActionListObserver).onItemRangeRemoved(mModel.getActionList(), 0, 1);
assertThat(mModel.getActionList().size(), is(0));
// There should be no notification if no actions are reported repeatedly.
testProvider.notifyObservers(new Action[] {});
verifyNoMoreInteractions(mMockActionListObserver);
}
@Test
public void testModelDoesntNotifyUnchangedData() {
mModel.addObserver(mMockPropertyObserver);
// Setting the visibility on the model should make it propagate that it's visible.
mModel.setVisible(true);
verify(mMockPropertyObserver)
.onPropertyChanged(mModel, KeyboardAccessoryModel.PropertyKey.VISIBLE);
assertThat(mModel.isVisible(), is(true));
// Marking it as visible again should not result in a notification.
mModel.setVisible(true);
verify(mMockPropertyObserver) // Unchanged number of invocations.
.onPropertyChanged(mModel, KeyboardAccessoryModel.PropertyKey.VISIBLE);
assertThat(mModel.isVisible(), is(true));
}
@Test
public void testIsVisibleWithSuggestionsBeforeKeyboardComesUp() {
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
// Without suggestions, the accessory should remain invisible - even if the keyboard shows.
assertThat(mModel.getActionList().size(), is(0));
assertThat(mModel.isVisible(), is(false));
mCoordinator.requestShowing();
assertThat(mModel.isVisible(), is(false));
mCoordinator.close();
// Adding suggestions doesn't change the visibility by itself.
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion, suggestion});
assertThat(mModel.getActionList().size(), is(2));
assertThat(mModel.isVisible(), is(false));
// But as soon as the keyboard comes up, it should be showing.
mCoordinator.requestShowing();
assertThat(mModel.isVisible(), is(true));
}
@Test
public void testIsVisibleWithSuggestionsAfterKeyboardComesUp() {
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
// Without any suggestions, the accessory should remain invisible.
assertThat(mModel.isVisible(), is(false));
assertThat(mModel.getActionList().size(), is(0));
// If the keyboard comes up, but there are no suggestions set, keep the accessory hidden.
mCoordinator.requestShowing();
assertThat(mModel.isVisible(), is(false));
// Adding suggestions while the keyboard is visible triggers the accessory.
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion, suggestion});
assertThat(mModel.getActionList().size(), is(2));
assertThat(mModel.isVisible(), is(true));
}
@Test
public void testIsVisibleWithActions() {
// Without any actions, the accessory should remain invisible.
assertThat(mModel.getActionList().size(), is(0));
mCoordinator.requestShowing();
assertThat(mModel.isVisible(), is(false));
// Adding actions while the keyboard is visible triggers the accessory.
mModel.getActionList().add(new Action(null, 0, null));
assertThat(mModel.isVisible(), is(true));
}
@Test
public void testSortsActionsBasedOnType() {
KeyboardAccessoryData.PropertyProvider<Action> generationProvider =
new KeyboardAccessoryData.PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
mCoordinator.registerActionListProvider(generationProvider);
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
Action suggestion1 = new Action("FirstSuggestion", AUTOFILL_SUGGESTION, (a) -> {});
Action suggestion2 = new Action("SecondSuggestion", AUTOFILL_SUGGESTION, (a) -> {});
Action generationAction = new Action("Generate", GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion1, suggestion2});
generationProvider.notifyObservers(new Action[] {generationAction});
// Autofill suggestions should always come last, independent of when they were added.
assertThat(mModel.getActionList().size(), is(3));
assertThat(mModel.getActionList().indexOf(generationAction), is(0));
assertThat(mModel.getActionList().indexOf(suggestion1), is(1));
assertThat(mModel.getActionList().indexOf(suggestion2), is(2));
}
@Test
public void testDeletingActionsAffectsOnlyOneType() {
KeyboardAccessoryData.PropertyProvider<Action> generationProvider =
new KeyboardAccessoryData.PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
mCoordinator.registerActionListProvider(generationProvider);
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
Action suggestion = new Action("NewSuggestion", AUTOFILL_SUGGESTION, (a) -> {});
Action generationAction = new Action("Generate", GENERATE_PASSWORD_AUTOMATIC, (a) -> {});
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion, suggestion});
generationProvider.notifyObservers(new Action[] {generationAction});
assertThat(mModel.getActionList().size(), is(3));
// Drop all Autofill suggestions. Only the generation action should remain.
autofillSuggestionProvider.notifyObservers(new Action[0]);
assertThat(mModel.getActionList().size(), is(1));
assertThat(mModel.getActionList().indexOf(generationAction), is(0));
// Readd an Autofill suggestion and drop the generation. Only the suggestion should remain.
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion});
generationProvider.notifyObservers(new Action[0]);
assertThat(mModel.getActionList().size(), is(1));
assertThat(mModel.getActionList().indexOf(suggestion), is(0));
}
@Test
public void testActionsRemovedWhenNotVisible() {
// Make the accessory visible and add an action to it.
mCoordinator.requestShowing();
mModel.getActionList().add(new Action(null, 0, null));
// Hiding the accessory should also remove actions.
mCoordinator.close();
assertThat(mModel.getActionList().size(), is(0));
}
@Test
public void testIsVisibleWithTabs() {
// Without any actions, the accessory should remain invisible.
assertThat(mModel.getActionList().size(), is(0));
mCoordinator.requestShowing();
assertThat(mModel.isVisible(), is(false));
// Adding actions while the keyboard is visible triggers the accessory.
mCoordinator.addTab(mTestTab);
assertThat(mModel.isVisible(), is(true));
}
@Test
public void testClosingTabDismissesOpenSheet() {
mModel.setActiveTab(0);
mModel.addObserver(mMockPropertyObserver);
assertThat(mModel.activeTab(), is(0));
// Closing the active tab should reset the tab which should trigger the visibility delegate.
mCoordinator.closeActiveTab();
assertThat(mModel.activeTab(), is(nullValue()));
verify(mMockPropertyObserver)
.onPropertyChanged(mModel, KeyboardAccessoryModel.PropertyKey.ACTIVE_TAB);
verify(mMockVisibilityDelegate).onCloseAccessorySheet();
}
@Test
public void testClosingTabIsNoOpForAlreadyClosedTab() {
mModel.setActiveTab(null);
mModel.addObserver(mMockPropertyObserver);
mCoordinator.closeActiveTab();
verifyNoMoreInteractions(mMockPropertyObserver, mMockVisibilityDelegate);
}
@Test
public void testRecordsOneImpressionForEveryInitialContentOnVisibilityChange() {
assertThat(RecordHistogram.getHistogramTotalCountForTesting(
KeyboardAccessoryMetricsRecorder.UMA_KEYBOARD_ACCESSORY_BAR_SHOWN),
is(0));
// Adding a tab contributes to the tabs and the total bucket.
mCoordinator.addTab(mTestTab);
mCoordinator.requestShowing();
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(1));
// Adding an action contributes to the actions bucket. Tabs and total are logged again.
mCoordinator.close(); // Hide, so it's brought up again.
mModel.getActionList().add(new Action(null, 0, null));
mCoordinator.requestShowing();
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(2));
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(2));
// Adding suggestions adds to the suggestions bucket - and again to tabs and total.
mCoordinator.close(); // Hide, so it's brought up again.
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion});
mCoordinator.requestShowing();
// Hiding the keyboard clears actions, so don't log more actions from here on out.
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(3));
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(3));
// Removing suggestions adds to everything but the suggestions bucket. The value remains.
mCoordinator.close(); // Hide, so it's brought up again.
autofillSuggestionProvider.notifyObservers(new Action[0]);
mCoordinator.requestShowing();
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(4));
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(4));
}
@Test
public void testRecordsContentBarImpressionOnceAndContentsUpToOnce() {
assertThat(RecordHistogram.getHistogramTotalCountForTesting(
KeyboardAccessoryMetricsRecorder.UMA_KEYBOARD_ACCESSORY_BAR_SHOWN),
is(0));
// First showing contains actions only.
mCoordinator.addTab(mTestTab);
mCoordinator.requestShowing();
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(1));
// Adding a tabs doesn't change the total impression count but the specific bucket.
mModel.getActionList().add(new Action(null, 0, null));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.ANY_CONTENTS), is(1));
KeyboardAccessoryData.PropertyProvider<Action> autofillSuggestionProvider =
new KeyboardAccessoryData.PropertyProvider<>(AUTOFILL_SUGGESTION);
Action suggestion = new Action("Suggestion", AUTOFILL_SUGGESTION, (a) -> {});
mCoordinator.registerActionListProvider(autofillSuggestionProvider);
autofillSuggestionProvider.notifyObservers(new Action[] {suggestion});
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_AUTOFILL_SUGGESTIONS), is(1));
// The other changes were not recorded again - just the changes.
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_TABS), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.NO_CONTENTS), is(0));
}
@Test
public void testRecordsAgainIfExistingItemsChange() {
assertThat(RecordHistogram.getHistogramTotalCountForTesting(
KeyboardAccessoryMetricsRecorder.UMA_KEYBOARD_ACCESSORY_BAR_SHOWN),
is(0));
// Add a tab and show, so the accessory is permanently visible.
mCoordinator.addTab(mTestTab);
mCoordinator.requestShowing();
// Adding an action fills the bar impression bucket and the actions set once.
mModel.getActionList().set(
new Action[] {new Action("One", AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, null),
new Action("Two", AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, null)});
assertThat(getActionImpressionCount(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC), is(1));
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
// Adding another action leaves bar impressions unchanged but affects the actions bucket.
mModel.getActionList().set(
new Action[] {new Action("Uno", AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, null),
new Action("Dos", AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, null)});
assertThat(getShownMetricsCount(AccessoryBarContents.WITH_ACTIONS), is(1));
assertThat(getActionImpressionCount(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC), is(2));
}
private int getActionImpressionCount(@AccessoryAction int bucket) {
return RecordHistogram.getHistogramValueCountForTesting(
KeyboardAccessoryMetricsRecorder.UMA_KEYBOARD_ACCESSORY_ACTION_IMPRESSION, bucket);
}
private int getShownMetricsCount(@AccessoryBarContents int bucket) {
return RecordHistogram.getHistogramValueCountForTesting(
KeyboardAccessoryMetricsRecorder.UMA_KEYBOARD_ACCESSORY_BAR_SHOWN, bucket);
}
}