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

// Copyright 2023 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.assertNotNull;
import static org.junit.Assert.assertNull;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.test.core.app.ApplicationProvider;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.components.omnibox.OmniboxFeatureList;
import org.chromium.components.omnibox.OmniboxFeatures;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;

import java.util.Arrays;

/** Unit tests for {@link PreWarmingRecycledViewPool}. */
@RunWith(BaseRobolectricTestRunner.class)
public class PreWarmingRecycledViewPoolTest {
    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
    private @Mock View mView;

    private Context mContext;
    private OmniboxSuggestionsDropdownAdapter mAdapter;
    private PreWarmingRecycledViewPool mPool;

    @Before
    public void setUp() {
        mContext = ApplicationProvider.getApplicationContext();
        mAdapter =
                Mockito.spy(
                        new OmniboxSuggestionsDropdownAdapter(new ModelList()) {
                            @Override
                            protected View createView(ViewGroup parent, int viewType) {
                                return mView;
                            }

                            @Override
                            public @NonNull ViewHolder onCreateViewHolder(
                                    @NonNull ViewGroup parent, int viewType) {
                                return new ViewHolder(mView, null);
                            }
                        });
        mPool = new PreWarmingRecycledViewPool(mAdapter, mContext);
    }

    private void ensureNoViewsCreated() {
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.EDIT_URL_SUGGESTION));
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.TILE_NAVSUGGEST));
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.HEADER));
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION));
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.DEFAULT));
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.ENTITY_SUGGESTION));
        assertEquals(0, mPool.getRecycledViewCount(OmniboxSuggestionUiType.ENTITY_SUGGESTION));
    }

    private void ensureAllViewsCreated() {
        assertEquals(1, mPool.getRecycledViewCount(OmniboxSuggestionUiType.EDIT_URL_SUGGESTION));
        assertEquals(1, mPool.getRecycledViewCount(OmniboxSuggestionUiType.TILE_NAVSUGGEST));
        assertEquals(1, mPool.getRecycledViewCount(OmniboxSuggestionUiType.HEADER));
        assertEquals(1, mPool.getRecycledViewCount(OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION));
        assertEquals(15, mPool.getRecycledViewCount(OmniboxSuggestionUiType.DEFAULT));
        assertEquals(3, mPool.getRecycledViewCount(OmniboxSuggestionUiType.ENTITY_SUGGESTION));

        View expectedView = mView;
        // null out mView so that newly-created ViewHolders will be distinct from pre-warmed ones.
        mView = null;
        for (var uiType :
                Arrays.asList(
                        OmniboxSuggestionUiType.EDIT_URL_SUGGESTION,
                        OmniboxSuggestionUiType.TILE_NAVSUGGEST,
                        OmniboxSuggestionUiType.HEADER,
                        OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION,
                        OmniboxSuggestionUiType.DEFAULT,
                        OmniboxSuggestionUiType.ENTITY_SUGGESTION)) {
            ViewHolder viewHolder = mPool.getRecycledView(uiType);
            assertNotNull(viewHolder);
            assertEquals(expectedView, viewHolder.itemView);
        }
    }

    @DisableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void testCreateViews() {
        mPool.onNativeInitialized();

        ensureNoViewsCreated();

        // Run first, then cancel.
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        mPool.stopCreatingViews();
        ensureAllViewsCreated();
    }

    @DisableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void stopCreatingViews_noViewsCreatedWhenCanceled() {
        mPool.onNativeInitialized();
        ensureNoViewsCreated();

        // Cancel, then run.
        mPool.stopCreatingViews();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        ensureNoViewsCreated();
    }

    @DisableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void destroy_cancelsViewCreation() {
        mPool.onNativeInitialized();
        ensureNoViewsCreated();

        // Destroy, then run.
        mPool.destroy();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        ensureNoViewsCreated();
    }

    @EnableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void createViews_noViewsCreatedOnLowEndDevices() {
        OmniboxFeatures.setIsLowMemoryDeviceForTesting(true);
        mPool.onNativeInitialized();
        ensureNoViewsCreated();
    }

    @EnableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void createViews_noViewsCreatedIfCanceledBeforeNative() {
        mPool.stopCreatingViews();
        mPool.onNativeInitialized();
        ensureNoViewsCreated();
    }

    @EnableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void createViews_recordViewCreated() {
        ensureNoViewsCreated();

        try (var watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.Omnibox.SuggestionView.CreatedType",
                        OmniboxSuggestionUiType.DEFAULT)) {
            assertNull(mPool.getRecycledView(OmniboxSuggestionUiType.DEFAULT));
        }
    }

    @EnableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void createViews_recordViewReused() {
        mPool.onNativeInitialized();
        ensureAllViewsCreated();

        try (var watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.Omnibox.SuggestionView.ReusedType",
                        OmniboxSuggestionUiType.DEFAULT)) {
            assertNotNull(mPool.getRecycledView(OmniboxSuggestionUiType.DEFAULT));
        }
    }

    @EnableFeatures(OmniboxFeatureList.OMNIBOX_ASYNC_VIEW_INFLATION)
    @Test
    public void createViews_viewsCreatedSynchronouslyWhenUsingAsyncViewInflater() {
        mPool.onNativeInitialized();
        ensureAllViewsCreated();
    }
}