// Copyright 2020 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.omnibox.suggestions;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
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.components.omnibox.GroupConfigTestSupport.SECTION_1_NO_HEADER;
import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_2_WITH_HEADER;
import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_3_WITH_HEADER;
import static org.chromium.components.omnibox.GroupConfigTestSupport.SECTION_MOST_VISITED;
import android.content.Context;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.omnibox.suggestions.groupseparator.GroupSeparatorProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderProcessor;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.AutocompleteMatchBuilder;
import org.chromium.components.omnibox.AutocompleteResult;
import org.chromium.components.omnibox.GroupsProto.GroupConfig;
import org.chromium.components.omnibox.GroupsProto.GroupsInfo;
import org.chromium.components.omnibox.OmniboxSuggestionType;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.List;
/** Tests for {@link DropdownItemViewInfoListBuilder}. */
@RunWith(BaseRobolectricTestRunner.class)
public class DropdownItemViewInfoListBuilderUnitTest {
public @Rule MockitoRule mockitoRule = MockitoJUnit.rule();
private Context mContext = ContextUtils.getApplicationContext();
private @Mock AutocompleteController mAutocompleteController;
private @Mock SuggestionProcessor mMockSuggestionProcessor;
private @Spy HeaderProcessor mMockHeaderProcessor = new HeaderProcessor(mContext);
private GroupSeparatorProcessor mGroupSeparatorProcessor =
new GroupSeparatorProcessor(mContext);
DropdownItemViewInfoListBuilder mBuilder;
@Before
public void setUp() {
when(mMockSuggestionProcessor.createModel())
.thenAnswer((mock) -> new PropertyModel(SuggestionCommonProperties.ALL_KEYS));
when(mMockSuggestionProcessor.getViewTypeId()).thenReturn(OmniboxSuggestionUiType.DEFAULT);
mBuilder = new DropdownItemViewInfoListBuilder(() -> null, (url) -> false);
mBuilder.registerSuggestionProcessor(mMockSuggestionProcessor);
mBuilder.setGroupSeparatorProcessorForTest(mGroupSeparatorProcessor);
mBuilder.setHeaderProcessorForTest(mMockHeaderProcessor);
}
/**
* Verify corner rounding and separator presence on a specific model.
*
* @param model the model to verify
* @param wantTopCornersRounded expected rounding state of top corners
* @param wantBottomCornersRounded expected rounding state of bottom corners
* @param wantSeparator expected state of the separator
*/
void verifyRounding(
PropertyModel model,
boolean wantTopCornersRounded,
boolean wantBottomCornersRounded,
boolean wantSeparator) {
Assert.assertEquals(
wantTopCornersRounded, model.get(DropdownCommonProperties.BG_TOP_CORNER_ROUNDED));
Assert.assertEquals(
wantBottomCornersRounded,
model.get(DropdownCommonProperties.BG_BOTTOM_CORNER_ROUNDED));
Assert.assertEquals(wantSeparator, model.get(DropdownCommonProperties.SHOW_DIVIDER));
}
@Test
public void buildDropdownViewInfoList_mixedGroups() {
final var groupsDetails =
GroupsInfo.newBuilder()
.putGroupConfigs(1, SECTION_MOST_VISITED)
.putGroupConfigs(2, SECTION_2_WITH_HEADER)
.build();
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
AutocompleteMatch horizontal =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
AutocompleteMatch vertical =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(2)
.build();
var actualList =
List.of(horizontal, horizontal, vertical, vertical, horizontal, horizontal);
var model =
mBuilder.buildDropdownViewInfoList(
AutocompleteResult.fromCache(actualList, groupsDetails));
// 1 horizontal + 1 header + 2 vertical + 1 horizontal.
Assert.assertEquals(5, model.size());
// Check reported positions in list.
verify(mMockSuggestionProcessor, atLeastOnce()).doesProcessSuggestion(horizontal, 0);
verify(mMockSuggestionProcessor).doesProcessSuggestion(vertical, 1);
verify(mMockSuggestionProcessor).doesProcessSuggestion(vertical, 2);
verify(mMockSuggestionProcessor, atLeastOnce()).doesProcessSuggestion(horizontal, 3);
verify(mMockSuggestionProcessor, times(2)).populateModel(eq(horizontal), any(), eq(0));
verify(mMockSuggestionProcessor).populateModel(eq(vertical), any(), eq(1));
verify(mMockSuggestionProcessor).populateModel(eq(vertical), any(), eq(2));
verify(mMockSuggestionProcessor, times(2)).populateModel(eq(horizontal), any(), eq(3));
// Other calls we expect to see.
verify(mMockSuggestionProcessor).onSuggestionsReceived();
verify(mMockSuggestionProcessor, times(4)).createModel();
verify(mMockSuggestionProcessor, atLeastOnce()).getViewTypeId();
verifyNoMoreInteractions(mMockSuggestionProcessor);
}
@Test
public void headers_buildsHeadersOnlyWhenGroupChanges() {
final List<AutocompleteMatch> actualList = new ArrayList<>();
final var groupsDetails =
GroupsInfo.newBuilder()
.putGroupConfigs(1, SECTION_2_WITH_HEADER)
.putGroupConfigs(2, SECTION_3_WITH_HEADER)
.build();
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
AutocompleteMatch suggestionWithNoGroup =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.build();
AutocompleteMatch suggestionForGroup1 =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
AutocompleteMatch suggestionForGroup2 =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(2)
.build();
actualList.add(suggestionWithNoGroup);
actualList.add(suggestionForGroup1);
actualList.add(suggestionForGroup1);
actualList.add(suggestionForGroup2);
actualList.add(suggestionForGroup2);
final InOrder verifier = inOrder(mMockSuggestionProcessor, mMockHeaderProcessor);
final List<DropdownItemViewInfo> model =
mBuilder.buildDropdownViewInfoList(
AutocompleteResult.fromCache(actualList, groupsDetails));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionWithNoGroup), any(), eq(0));
verifier.verify(mMockHeaderProcessor, times(1))
.populateModel(any(), eq(SECTION_2_WITH_HEADER.getHeaderText()));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup1), any(), eq(1));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup1), any(), eq(2));
verifier.verify(mMockHeaderProcessor, times(1))
.populateModel(any(), eq(SECTION_3_WITH_HEADER.getHeaderText()));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup2), any(), eq(3));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup2), any(), eq(4));
Assert.assertEquals(7, model.size()); // 2 headers + 5 suggestions.
var defaultGroupConfig = GroupConfig.getDefaultInstance();
Assert.assertEquals(model.get(0).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(0).groupConfig, defaultGroupConfig);
verifyRounding(model.get(0).model, true, true, false);
Assert.assertEquals(model.get(1).type, OmniboxSuggestionUiType.HEADER);
Assert.assertEquals(model.get(1).groupConfig, SECTION_2_WITH_HEADER);
verifyRounding(model.get(1).model, false, false, false);
Assert.assertEquals(model.get(2).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(2).groupConfig, SECTION_2_WITH_HEADER);
verifyRounding(model.get(2).model, true, false, true);
Assert.assertEquals(model.get(3).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(3).groupConfig, SECTION_2_WITH_HEADER);
verifyRounding(model.get(3).model, false, true, false);
Assert.assertEquals(model.get(4).type, OmniboxSuggestionUiType.HEADER);
Assert.assertEquals(model.get(4).groupConfig, SECTION_3_WITH_HEADER);
verifyRounding(model.get(4).model, false, false, false);
Assert.assertEquals(model.get(5).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(5).groupConfig, SECTION_3_WITH_HEADER);
verifyRounding(model.get(5).model, true, false, true);
Assert.assertEquals(model.get(6).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(6).groupConfig, SECTION_3_WITH_HEADER);
verifyRounding(model.get(6).model, false, true, false);
}
@Test
public void headers_respectGroupHeadersWithNoTitle() {
final List<AutocompleteMatch> actualList = new ArrayList<>();
final var groupsDetails =
GroupsInfo.newBuilder()
.putGroupConfigs(1, SECTION_1_NO_HEADER)
.putGroupConfigs(2, SECTION_2_WITH_HEADER)
.build();
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
AutocompleteMatch suggestionWithNoGroup =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.build();
AutocompleteMatch suggestionForGroup1 =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
AutocompleteMatch suggestionForGroup2 =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(2)
.build();
actualList.add(suggestionWithNoGroup);
actualList.add(suggestionForGroup1);
actualList.add(suggestionForGroup1);
actualList.add(suggestionForGroup2);
actualList.add(suggestionForGroup2);
final InOrder verifier = inOrder(mMockSuggestionProcessor, mMockHeaderProcessor);
final List<DropdownItemViewInfo> model =
mBuilder.buildDropdownViewInfoList(
AutocompleteResult.fromCache(actualList, groupsDetails));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionWithNoGroup), any(), eq(0));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup1), any(), eq(1));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup1), any(), eq(2));
verifier.verify(mMockHeaderProcessor, times(1))
.populateModel(any(), eq(SECTION_2_WITH_HEADER.getHeaderText()));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup2), any(), eq(3));
verifier.verify(mMockSuggestionProcessor, times(1))
.populateModel(eq(suggestionForGroup2), any(), eq(4));
// Make sure no other headers were ever constructed.
verify(mMockHeaderProcessor, times(1)).populateModel(any(), any());
var defaultGroupConfig = GroupConfig.getDefaultInstance();
// We're showing:
// - 1 suggestion,
// - <separator>
// - 2 suggestions (grouped),
// - <header>
// - 2 suggestions (grouped).
Assert.assertEquals(7, model.size());
Assert.assertEquals(model.get(0).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(0).groupConfig, defaultGroupConfig);
Assert.assertEquals(model.get(1).type, OmniboxSuggestionUiType.GROUP_SEPARATOR);
Assert.assertEquals(model.get(1).groupConfig, SECTION_1_NO_HEADER);
Assert.assertEquals(model.get(2).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(2).groupConfig, SECTION_1_NO_HEADER);
Assert.assertEquals(model.get(3).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(3).groupConfig, SECTION_1_NO_HEADER);
Assert.assertEquals(model.get(4).type, OmniboxSuggestionUiType.HEADER);
Assert.assertEquals(model.get(4).groupConfig, SECTION_2_WITH_HEADER);
Assert.assertEquals(model.get(5).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(5).groupConfig, SECTION_2_WITH_HEADER);
Assert.assertEquals(model.get(6).type, OmniboxSuggestionUiType.DEFAULT);
Assert.assertEquals(model.get(6).groupConfig, SECTION_2_WITH_HEADER);
}
@Test
public void builder_propagatesOmniboxSessionStateChangeEvents() {
mBuilder.onOmniboxSessionStateChange(true);
verify(mMockHeaderProcessor, times(1)).onOmniboxSessionStateChange(eq(true));
verify(mMockSuggestionProcessor, times(1)).onOmniboxSessionStateChange(eq(true));
mBuilder.onOmniboxSessionStateChange(false);
verify(mMockHeaderProcessor, times(1)).onOmniboxSessionStateChange(eq(false));
verify(mMockSuggestionProcessor, times(1)).onOmniboxSessionStateChange(eq(false));
verifyNoMoreInteractions(mMockHeaderProcessor);
verifyNoMoreInteractions(mMockSuggestionProcessor);
}
@Test
public void builder_propagatesNativeInitializedEvent() {
mBuilder.onNativeInitialized();
verify(mMockHeaderProcessor, times(1)).onNativeInitialized();
verify(mMockSuggestionProcessor, times(1)).onNativeInitialized();
verifyNoMoreInteractions(mMockHeaderProcessor);
verifyNoMoreInteractions(mMockSuggestionProcessor);
}
@Test
public void buildVerticalSuggestionsGroup_withoutGroupHeader_noPreviousGroup() {
var match =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
var matches = List.of(match, match);
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
clearInvocations(mMockHeaderProcessor, mMockSuggestionProcessor);
var result =
mBuilder.buildVerticalSuggestionsGroup(
SECTION_1_NO_HEADER, null, matches, /* firstVerticalPosition= */ 5);
verify(mMockSuggestionProcessor, times(2)).createModel();
verify(mMockSuggestionProcessor, atLeastOnce()).getViewTypeId();
verify(mMockSuggestionProcessor).doesProcessSuggestion(match, 5);
verify(mMockSuggestionProcessor).populateModel(eq(match), any(), eq(5));
verify(mMockSuggestionProcessor).doesProcessSuggestion(match, 6);
verify(mMockSuggestionProcessor).populateModel(eq(match), any(), eq(6));
verifyNoMoreInteractions(mMockHeaderProcessor, mMockSuggestionProcessor);
assertEquals(/* 0 header + 2 suggestions = */ 2, result.size());
}
@Test
public void buildVerticalSuggestionsGroup_withoutGroupHeader_verticalPreviousGroup() {
var match =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
var matches = List.of(match, match);
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
clearInvocations(mMockHeaderProcessor, mMockSuggestionProcessor);
var result =
mBuilder.buildVerticalSuggestionsGroup(
SECTION_1_NO_HEADER,
SECTION_2_WITH_HEADER,
matches,
/* firstVerticalPosition= */ 5);
verify(mMockSuggestionProcessor, times(2)).createModel();
verify(mMockSuggestionProcessor, atLeastOnce()).getViewTypeId();
verify(mMockSuggestionProcessor).doesProcessSuggestion(match, 5);
verify(mMockSuggestionProcessor).populateModel(eq(match), any(), eq(5));
verify(mMockSuggestionProcessor).doesProcessSuggestion(match, 6);
verify(mMockSuggestionProcessor).populateModel(eq(match), any(), eq(6));
verifyNoMoreInteractions(mMockHeaderProcessor, mMockSuggestionProcessor);
assertEquals(/* 1 space + 2 suggestions = */ 3, result.size());
assertEquals(OmniboxSuggestionUiType.GROUP_SEPARATOR, result.get(0).type);
assertEquals(OmniboxSuggestionUiType.DEFAULT, result.get(1).type);
assertEquals(OmniboxSuggestionUiType.DEFAULT, result.get(2).type);
}
@Test
public void buildVerticalSuggestionsGroup_withGroupHeader_noPreviousGroup() {
var match =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
var matches = List.of(match, match);
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
clearInvocations(mMockHeaderProcessor, mMockSuggestionProcessor);
var result =
mBuilder.buildVerticalSuggestionsGroup(
SECTION_2_WITH_HEADER, null, matches, /* firstVerticalPosition= */ 7);
verify(mMockHeaderProcessor).createModel();
verify(mMockHeaderProcessor, atLeastOnce()).getViewTypeId();
verify(mMockHeaderProcessor)
.populateModel(any(), eq(SECTION_2_WITH_HEADER.getHeaderText()));
verify(mMockSuggestionProcessor, times(2)).createModel();
verify(mMockSuggestionProcessor, atLeastOnce()).getViewTypeId();
verify(mMockSuggestionProcessor).doesProcessSuggestion(match, 7);
verify(mMockSuggestionProcessor).populateModel(eq(match), any(), eq(7));
verify(mMockSuggestionProcessor).doesProcessSuggestion(match, 8);
verify(mMockSuggestionProcessor).populateModel(eq(match), any(), eq(8));
verifyNoMoreInteractions(mMockHeaderProcessor, mMockSuggestionProcessor);
assertEquals(/* 1 header + 2 suggestions = */ 3, result.size());
assertEquals(OmniboxSuggestionUiType.HEADER, result.get(0).type);
assertEquals(OmniboxSuggestionUiType.DEFAULT, result.get(1).type);
assertEquals(OmniboxSuggestionUiType.DEFAULT, result.get(2).type);
}
@Test
public void buildHorizontalSuggestionsGroup_withoutGroupHeader() {
var match =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
var matches = List.of(match, match);
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
clearInvocations(mMockHeaderProcessor, mMockSuggestionProcessor);
var result =
mBuilder.buildHorizontalSuggestionsGroup(
SECTION_1_NO_HEADER, matches, /* firstVerticalPosition= */ 5);
var captor = ArgumentCaptor.forClass(PropertyModel.class);
verify(mMockSuggestionProcessor).getViewTypeId();
verify(mMockSuggestionProcessor).createModel();
verify(mMockSuggestionProcessor, atLeastOnce()).doesProcessSuggestion(match, 5);
verify(mMockSuggestionProcessor, times(2))
.populateModel(eq(match), captor.capture(), eq(5));
verifyNoMoreInteractions(mMockHeaderProcessor, mMockSuggestionProcessor);
assertEquals(/* 0 header + 1 suggestion row = */ 1, result.size());
// Verify that the same PropertyModel was used to build UI element, and it's the one that
// was returned.
assertEquals(captor.getAllValues().get(0), captor.getAllValues().get(1));
assertEquals(captor.getValue(), result.get(0).model);
}
@Test
public void buildHorizontalSuggestionsGroup_withGroupHeader() {
var match =
AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setGroupId(1)
.build();
var matches = List.of(match, match);
when(mMockSuggestionProcessor.doesProcessSuggestion(any(), anyInt())).thenReturn(true);
clearInvocations(mMockHeaderProcessor, mMockSuggestionProcessor);
var result =
mBuilder.buildHorizontalSuggestionsGroup(
SECTION_2_WITH_HEADER, matches, /* firstVerticalPosition= */ 7);
verify(mMockHeaderProcessor).createModel();
verify(mMockHeaderProcessor, atLeastOnce()).getViewTypeId();
verify(mMockHeaderProcessor)
.populateModel(any(), eq(SECTION_2_WITH_HEADER.getHeaderText()));
var captor = ArgumentCaptor.forClass(PropertyModel.class);
verify(mMockSuggestionProcessor).getViewTypeId();
verify(mMockSuggestionProcessor).createModel();
verify(mMockSuggestionProcessor, atLeastOnce()).doesProcessSuggestion(match, 7);
verify(mMockSuggestionProcessor, times(2))
.populateModel(eq(match), captor.capture(), eq(7));
verifyNoMoreInteractions(mMockHeaderProcessor, mMockSuggestionProcessor);
assertEquals(/* 1 header + 1 suggestion row = */ 2, result.size());
// Verify that the same PropertyModel was used to build UI element, and it's the one that
// was returned.
assertEquals(captor.getAllValues().get(0), captor.getAllValues().get(1));
assertEquals(captor.getValue(), result.get(1).model);
}
}