chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionsDropdownAdapterUnitTest.java

// Copyright 2024 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.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;

import android.content.Context;
import android.view.ContextThemeWrapper;
import android.widget.FrameLayout;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.browser.omnibox.test.R;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.SimpleRecyclerViewAdapter.ViewHolder;

import java.util.Set;

/** Unit tests for the OmniboxSuggestionsDropdownAdapter component. */
@RunWith(BaseRobolectricTestRunner.class)
public class OmniboxSuggestionsDropdownAdapterUnitTest {
    public @Rule MockitoRule mockitoRule = MockitoJUnit.rule();
    private @Mock DropdownItemProcessor mProcessor;
    private Context mContext;
    private FrameLayout mContainer;
    private ModelList mModel;
    private OmniboxSuggestionsDropdownAdapter mAdapter;

    @Before
    public void setUp() {
        mContext =
                new ContextThemeWrapper(
                        ContextUtils.getApplicationContext(), R.style.Theme_BrowserUI_DayNight);
        mContainer = new FrameLayout(mContext);
        mModel = new ModelList();
        mAdapter = new OmniboxSuggestionsDropdownAdapter(mModel);
    }

    @Test
    public void createView_allUiTypesHaveAssociatedViewTypes() {
        Set<Integer> deprecatedViewTypes = Set.of();

        for (@OmniboxSuggestionUiType int type = OmniboxSuggestionUiType.DEFAULT;
                type < OmniboxSuggestionUiType.COUNT;
                type++) {
            if (deprecatedViewTypes.contains(type)) continue;

            var view = mAdapter.createView(mContainer, type);
            // Each view type should have a corresponding view object.
            // The only exception are the deprecated views - exceptions should be explicitly handled
            // here.
            assertNotNull(view);
            // View creation does not immediately mean view is retained.
            assertEquals(0, mAdapter.getItemCount());
        }
    }

    @Test
    public void onCreateViewHolder_retainsItemsByType() {
        for (@OmniboxSuggestionUiType int type = OmniboxSuggestionUiType.DEFAULT;
                type < OmniboxSuggestionUiType.COUNT;
                type++) {
            ViewHolder viewHolder = mAdapter.onCreateViewHolder(mContainer, type);
            assertNotNull(viewHolder);
            assertNotNull(viewHolder.itemView);
        }
    }

    @Test
    public void onViewRecycled_deselectAnyPreviouslySelectedViews() {
        // This test confirms that the recycled views do not carry Selected attribute when reused.
        var viewHolder = mAdapter.onCreateViewHolder(mContainer, OmniboxSuggestionUiType.DEFAULT);
        var view = viewHolder.itemView;
        view.setSelected(true);
        assertTrue(view.isSelected());
        mAdapter.onViewRecycled(viewHolder);
        assertFalse(view.isSelected());
    }

    @Test
    public void onBindViewHolder_allItemsMustSupportDropdownCommonProperties() {
        // These properties must be respected by all Dropdown items.
        var commonModel =
                new PropertyModel.Builder(DropdownCommonProperties.ALL_KEYS)
                        .with(DropdownCommonProperties.SHOW_DIVIDER, true)
                        .with(DropdownCommonProperties.BG_TOP_CORNER_ROUNDED, false)
                        .with(DropdownCommonProperties.BG_BOTTOM_CORNER_ROUNDED, false)
                        .build();

        for (@OmniboxSuggestionUiType int type = OmniboxSuggestionUiType.DEFAULT;
                type < OmniboxSuggestionUiType.COUNT;
                type++) {
            doReturn(type).when(mProcessor).getViewTypeId();
            doReturn(commonModel).when(mProcessor).createModel();

            mModel.add(new DropdownItemViewInfo(mProcessor, commonModel, null));

            var viewHolder = mAdapter.onCreateViewHolder(mContainer, type);
            mAdapter.onBindViewHolder(viewHolder, 0);
            assertNotNull(viewHolder.model);

            // Confirm that view recycling decouples View from Model.
            mAdapter.onViewRecycled(viewHolder);
            assertNull(viewHolder.model);

            mModel.clear();
        }
    }

    @Test
    public void recordSuggestionViewReuseStats_noViewsReused() {
        // Create suggestion of every type, and bind it once.
        // These suggestions are not reused, so the reuse stats should be 0.
        for (@OmniboxSuggestionUiType int type = OmniboxSuggestionUiType.DEFAULT;
                type < OmniboxSuggestionUiType.COUNT;
                type++) {
            doReturn(type).when(mProcessor).getViewTypeId();
            mModel.add(new DropdownItemViewInfo(mProcessor, new PropertyModel(), null));

            var viewHolder = mAdapter.onCreateViewHolder(mContainer, type);
            mAdapter.onBindViewHolder(viewHolder, 0);
            mAdapter.onViewRecycled(viewHolder);

            mModel.clear();
        }

        // Confirm all views created, but none of them have been reused (reuse at 0%).
        try (var watcher =
                HistogramWatcher.newBuilder()
                        .expectIntRecords("Android.Omnibox.SuggestionView.SessionViewsCreated", 10)
                        .expectIntRecords("Android.Omnibox.SuggestionView.SessionViewsReused", 0)
                        .build()) {
            mAdapter.recordSessionMetrics();
        }
    }

    @Test
    public void recordSuggestionViewReuseStats_allViewsReused() {
        // Create suggestion of every type, and bind it twice.
        // 100% reuse is a theoretical limit, indicating that all created views have been reused
        // infinite amount of times.
        for (@OmniboxSuggestionUiType int type = OmniboxSuggestionUiType.DEFAULT;
                type < OmniboxSuggestionUiType.COUNT;
                type++) {
            doReturn(type).when(mProcessor).getViewTypeId();
            mModel.add(new DropdownItemViewInfo(mProcessor, new PropertyModel(), null));

            var viewHolder = mAdapter.onCreateViewHolder(mContainer, type);
            mAdapter.onBindViewHolder(viewHolder, 0);
            mAdapter.onViewRecycled(viewHolder);

            // Reuse view.
            mAdapter.onBindViewHolder(viewHolder, 0);
            mAdapter.onViewRecycled(viewHolder);

            mModel.clear();
        }

        try (var watcher =
                HistogramWatcher.newBuilder()
                        .expectIntRecords("Android.Omnibox.SuggestionView.SessionViewsCreated", 10)
                        .expectIntRecords("Android.Omnibox.SuggestionView.SessionViewsReused", 50)
                        .build()) {
            mAdapter.recordSessionMetrics();
        }
    }

    @Test
    public void recordSuggestionViewReuseStats_noViewsBound() {
        // When views have been created but never bound - do not capture any metrics.
        for (@OmniboxSuggestionUiType int type = OmniboxSuggestionUiType.DEFAULT;
                type < OmniboxSuggestionUiType.COUNT;
                type++) {
            doReturn(type).when(mProcessor).getViewTypeId();
            mModel.add(new DropdownItemViewInfo(mProcessor, new PropertyModel(), null));
            mAdapter.onCreateViewHolder(mContainer, type);
            mModel.clear();
        }

        try (var watcher =
                HistogramWatcher.newBuilder()
                        .expectNoRecords("Android.Omnibox.SuggestionView.SessionViewsCreated")
                        .expectNoRecords("Android.Omnibox.SuggestionView.SessionViewsReused")
                        .build()) {
            mAdapter.recordSessionMetrics();
        }
    }
}