chromium/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediatorTest.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.bookmarks;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import static org.chromium.ui.test.util.MockitoHelper.doCallback;
import static org.chromium.ui.test.util.MockitoHelper.doRunnable;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.Pair;

import androidx.annotation.ColorInt;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import androidx.test.ext.junit.rules.ActivityScenarioRule;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.task.test.ShadowPostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Batch;
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.base.test.util.JniMocker;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.bookmarks.BookmarkListEntry.ViewType;
import org.chromium.chrome.browser.bookmarks.BookmarkMetrics.BookmarkManagerFilter;
import org.chromium.chrome.browser.bookmarks.BookmarkUiPrefs.BookmarkRowDisplayPref;
import org.chromium.chrome.browser.bookmarks.BookmarkUiPrefs.BookmarkRowSortOrder;
import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
import org.chromium.chrome.browser.bookmarks.ImprovedBookmarkRow.Location;
import org.chromium.chrome.browser.bookmarks.ImprovedBookmarkRowProperties.ImageVisibility;
import org.chromium.chrome.browser.commerce.PriceTrackingUtils;
import org.chromium.chrome.browser.commerce.PriceTrackingUtilsJni;
import org.chromium.chrome.browser.commerce.ShoppingFeatures;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.signin.SyncPromoController.SyncPromoState;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkItem;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableRecyclerViewAdapter;
import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableRecyclerViewAdapter.DragListener;
import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableRecyclerViewAdapter.DraggabilityProvider;
import org.chromium.components.browser_ui.widget.dragreorder.DragStateDelegate;
import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate.SelectionObserver;
import org.chromium.components.commerce.core.CommerceSubscription;
import org.chromium.components.commerce.core.ShoppingService;
import org.chromium.components.commerce.core.SubscriptionsObserver;
import org.chromium.components.favicon.IconType;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.favicon.LargeIconBridge.LargeIconCallback;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.payments.CurrencyFormatter;
import org.chromium.components.payments.CurrencyFormatterJni;
import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
import org.chromium.components.power_bookmarks.ProductPrice;
import org.chromium.components.power_bookmarks.ShoppingSpecifics;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.sync.SyncFeatureMap;
import org.chromium.components.sync.SyncService;
import org.chromium.components.sync.SyncService.SyncStateChangedListener;
import org.chromium.components.url_formatter.SchemeDisplay;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.ui.accessibility.AccessibilityState;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.listmenu.BasicListMenu;
import org.chromium.ui.listmenu.ListMenuItemProperties;
import org.chromium.ui.modelutil.ListObservable;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

/** Unit tests for {@link BookmarkManagerMediator}. */
@Batch(Batch.UNIT_TESTS)
@RunWith(BaseRobolectricTestRunner.class)
@Config(shadows = {ShadowPostTask.class})
@EnableFeatures({
    SyncFeatureMap.SYNC_ENABLE_BOOKMARKS_IN_TRANSPORT_MODE,
    ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS,
    ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS
})
public class BookmarkManagerMediatorTest {
    private static final GURL EXAMPLE_URL = JUnitTestGURLs.EXAMPLE_URL;
    private static final String EXAMPLE_URL_FORMATTED =
            UrlFormatter.formatUrlForSecurityDisplay(
                    EXAMPLE_URL, SchemeDisplay.OMIT_HTTP_AND_HTTPS);

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.LENIENT);
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private BookmarkModel mBookmarkModel;
    @Mock private BookmarkOpener mBookmarkOpener;
    @Mock private SelectableListLayout<BookmarkId> mSelectableListLayout;
    @Mock private SelectionDelegate<BookmarkId> mSelectionDelegate;
    @Mock private RecyclerView mRecyclerView;
    @Mock private LargeIconBridge mLargeIconBridge;
    @Mock private BookmarkUiObserver mBookmarkUiObserver;
    @Mock private Profile mProfile;
    @Mock private SyncService mSyncService;
    @Mock private IdentityServicesProvider mIdentityServicesProvider;
    @Mock private SigninManager mSigninManager;
    @Mock private IdentityManager mIdentityManager;
    @Mock private AccountManagerFacade mAccountManagerFacade;
    @Mock private BookmarkUndoController mBookmarkUndoController;
    @Mock private Runnable mHideKeyboardRunnable;
    @Mock private CurrencyFormatter.Natives mCurrencyFormatterJniMock;
    @Mock private Tracker mTracker;
    @Mock private BookmarkImageFetcher mBookmarkImageFetcher;
    @Mock private Drawable mDrawable;
    @Mock private ShoppingService mShoppingService;
    @Mock private SnackbarManager mSnackbarManager;
    @Mock private PriceTrackingUtils.Natives mPriceTrackingUtilsJniMock;
    @Mock private ListObservable.ListObserver<Void> mListObserver;
    @Mock private Consumer<OnScrollListener> mOnScrollListenerConsumer;
    @Mock private BookmarkMoveSnackbarManager mBookmarkMoveSnackbarManager;

    @Captor private ArgumentCaptor<BookmarkModelObserver> mBookmarkModelObserverArgumentCaptor;
    @Captor private ArgumentCaptor<SelectionObserver> mSelectionObserver;
    @Captor private ArgumentCaptor<DragListener> mDragListenerArgumentCaptor;
    @Captor private ArgumentCaptor<SyncStateChangedListener> mSyncStateChangedListenerCaptor;
    @Captor private ArgumentCaptor<Runnable> mFinishLoadingBookmarkModelCaptor;
    @Captor private ArgumentCaptor<OnScrollListener> mOnScrollListenerCaptor;
    @Captor private ArgumentCaptor<SubscriptionsObserver> mSubscriptionsObserver;

    private int mId = 1;
    private final ObservableSupplierImpl<Boolean> mBackPressStateSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean>
            mSelectableListLayoutHandleBackPressChangedSupplier = new ObservableSupplierImpl<>();
    private final BookmarkId mRootFolderId = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mDesktopFolderId = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mMobileFolderId = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mOtherFolderId = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mFolderId1 = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mFolderId2 = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mFolderId3 = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mBookmarkId21 = new BookmarkId(mId++, BookmarkType.NORMAL);
    private final BookmarkId mReadingListFolderId =
            new BookmarkId(mId++, BookmarkType.READING_LIST);
    private final BookmarkId mReadingListId = new BookmarkId(mId++, BookmarkType.READING_LIST);
    private final BookmarkId mPriceTrackedBookmarkId = new BookmarkId(mId++, BookmarkType.NORMAL);

    private final BookmarkItem mDesktopFolderItem =
            new BookmarkItem(
                    mDesktopFolderId,
                    "Bookmarks bar",
                    null,
                    true,
                    mRootFolderId,
                    false,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mMobileFolderItem =
            new BookmarkItem(
                    mMobileFolderId,
                    "Mobile bookmarks",
                    null,
                    true,
                    mRootFolderId,
                    false,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mOtherFolderItem =
            new BookmarkItem(
                    mOtherFolderId,
                    "Other bookmarks",
                    null,
                    true,
                    mRootFolderId,
                    false,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mFolderItem1 =
            new BookmarkItem(
                    mFolderId1,
                    "Folder1",
                    null,
                    true,
                    mRootFolderId,
                    true,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mFolderItem2 =
            new BookmarkItem(
                    mFolderId2, "Folder2", null, true, mFolderId1, true, false, 0, false, 0, false);
    private final BookmarkItem mFolderItem3 =
            new BookmarkItem(
                    mFolderId3, "Folder3", null, true, mFolderId1, true, false, 0, false, 0, false);
    private final BookmarkItem mBookmarkItem21 =
            new BookmarkItem(
                    mBookmarkId21,
                    "Bookmark21",
                    EXAMPLE_URL,
                    false,
                    mFolderId2,
                    true,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mReadingListFolderItem =
            new BookmarkItem(
                    mReadingListFolderId,
                    "Reading List",
                    null,
                    true,
                    mRootFolderId,
                    false,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mReadingListItem =
            new BookmarkItem(
                    mReadingListId,
                    EXAMPLE_URL.getSpec(),
                    EXAMPLE_URL,
                    false,
                    mReadingListFolderId,
                    true,
                    false,
                    0,
                    false,
                    0,
                    false);
    private final BookmarkItem mPriceTrackedBookmarkItem =
            new BookmarkItem(
                    mPriceTrackedBookmarkId,
                    "Price tracked bookmark",
                    EXAMPLE_URL,
                    false,
                    mMobileFolderId,
                    true,
                    false,
                    0,
                    false,
                    0,
                    false);

    private final ModelList mModelList = new ModelList();
    private final Bitmap mBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);

    private BookmarkUiPrefs mBookmarkUiPrefs =
            new BookmarkUiPrefs(ChromeSharedPreferences.getInstance());
    private Activity mActivity;
    private BookmarkManagerMediator mMediator;
    private DragReorderableRecyclerViewAdapter mDragReorderableRecyclerViewAdapter;

    @Before
    public void setUp() {
        // The mediator will respond to model changes by posting a task to update for performance.
        // This just runs all of those posts synchronously to simplify test code.
        ShadowPostTask.setTestImpl(
                (taskTraits, task, delay) -> {
                    assert delay == 0;
                    assert taskTraits >= TaskTraits.UI_TRAITS_START;
                    task.run();
                });
        mActivityScenarioRule.getScenario().onActivity(this::onActivity);
    }

    private void onActivity(Activity activity) {
        mActivity = spy(activity);

        // Setup CurrencyFormatter.
        mJniMocker.mock(CurrencyFormatterJni.TEST_HOOKS, mCurrencyFormatterJniMock);

        // Setup TrackerFactory.
        TrackerFactory.setTrackerForTests(mTracker);

        // Setup BookmarkModel.
        doReturn(false).when(mBookmarkModel).areAccountBookmarkFoldersActive();
        doReturn(mRootFolderId).when(mBookmarkModel).getRootFolderId();
        doReturn(mDesktopFolderId).when(mBookmarkModel).getDesktopFolderId();
        doReturn(mDesktopFolderItem).when(mBookmarkModel).getBookmarkById(mDesktopFolderId);
        doReturn(mMobileFolderId).when(mBookmarkModel).getMobileFolderId();
        doReturn(mMobileFolderItem).when(mBookmarkModel).getBookmarkById(mMobileFolderId);
        doReturn(mPriceTrackedBookmarkItem)
                .when(mBookmarkModel)
                .getBookmarkById(mPriceTrackedBookmarkId);
        doReturn(Arrays.asList(mPriceTrackedBookmarkId))
                .when(mBookmarkModel)
                .getChildIds(mMobileFolderId);
        doReturn(mOtherFolderId).when(mBookmarkModel).getOtherFolderId();
        doReturn(mOtherFolderItem).when(mBookmarkModel).getBookmarkById(mOtherFolderId);
        doReturn(mReadingListFolderId).when(mBookmarkModel).getLocalOrSyncableReadingListFolder();
        doReturn(mReadingListFolderItem).when(mBookmarkModel).getBookmarkById(mReadingListFolderId);
        doReturn(true).when(mBookmarkModel).doesBookmarkExist(any());
        doReturn(Arrays.asList(mFolderId2, mFolderId3))
                .when(mBookmarkModel)
                .getChildIds(mFolderId1);
        doReturn(mFolderItem1).when(mBookmarkModel).getBookmarkById(mFolderId1);
        doReturn(mFolderItem2).when(mBookmarkModel).getBookmarkById(mFolderId2);
        doReturn(mFolderItem3).when(mBookmarkModel).getBookmarkById(mFolderId3);
        doReturn(mBookmarkItem21).when(mBookmarkModel).getBookmarkById(mBookmarkId21);
        doReturn(Arrays.asList(mBookmarkId21)).when(mBookmarkModel).getChildIds(mFolderId2);
        doReturn(1).when(mBookmarkModel).getTotalBookmarkCount(mFolderId2);
        doReturn(Arrays.asList(mReadingListId))
                .when(mBookmarkModel)
                .getChildIds(mReadingListFolderId);
        doReturn(mReadingListFolderItem).when(mBookmarkModel).getBookmarkById(mReadingListFolderId);
        doReturn(mReadingListItem).when(mBookmarkModel).getBookmarkById(mReadingListId);
        doReturn(true).when(mBookmarkModel).isFolderVisible(any());
        doReturn(
                        Arrays.asList(
                                mDesktopFolderId,
                                mMobileFolderId,
                                mOtherFolderId,
                                mReadingListFolderId))
                .when(mBookmarkModel)
                .getTopLevelFolderIds();

        // Setup SelectableListLayout.
        doReturn(mActivity).when(mSelectableListLayout).getContext();
        doReturn(mSelectableListLayoutHandleBackPressChangedSupplier)
                .when(mSelectableListLayout)
                .getHandleBackPressChangedSupplier();

        // Setup BookmarkUIObserver.
        doRunnable(() -> mMediator.removeUiObserver(mBookmarkUiObserver))
                .when(mBookmarkUiObserver)
                .onDestroy();

        // Setup LargeIconBridge.
        doAnswer(
                        invocation -> {
                            LargeIconCallback cb = invocation.getArgument(3);
                            cb.onLargeIconAvailable(mBitmap, Color.GREEN, false, IconType.FAVICON);
                            return null;
                        })
                .when(mLargeIconBridge)
                .getLargeIconForUrl(any(), anyInt(), anyInt(), any());

        // Setup BookmarkUiPrefs.
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.COMPACT);

        // Setup sync/identify mocks.
        SyncServiceFactory.setInstanceForTesting(mSyncService);
        IdentityServicesProvider.setInstanceForTests(mIdentityServicesProvider);
        doReturn(mSigninManager).when(mIdentityServicesProvider).getSigninManager(any());
        doReturn(mIdentityManager).when(mSigninManager).getIdentityManager();
        doReturn(mIdentityManager).when(mIdentityServicesProvider).getIdentityManager(any());
        AccountManagerFacadeProvider.setInstanceForTests(mAccountManagerFacade);

        // Setup image fetching.
        doAnswer(
                        (invocation) -> {
                            Callback<Pair<Drawable, Drawable>> callback = invocation.getArgument(1);
                            callback.onResult(new Pair<>(mDrawable, mDrawable));
                            return null;
                        })
                .when(mBookmarkImageFetcher)
                .fetchFirstTwoImagesForFolder(any(), any());
        doAnswer(
                        (invocation) -> {
                            Callback<Drawable> callback = invocation.getArgument(1);
                            callback.onResult(mDrawable);
                            return null;
                        })
                .when(mBookmarkImageFetcher)
                .fetchImageForBookmarkWithFaviconFallback(any(), any());
        doAnswer(
                        (invocation) -> {
                            Callback<Drawable> callback = invocation.getArgument(1);
                            callback.onResult(mDrawable);
                            return null;
                        })
                .when(mBookmarkImageFetcher)
                .fetchFaviconForBookmark(any(), any());

        // Setup price tracking utils.
        doReturn(true).when(mShoppingService).isShoppingListEligible();
        mJniMocker.mock(PriceTrackingUtilsJni.TEST_HOOKS, mPriceTrackingUtilsJniMock);
        doCallback(3, (Callback<Boolean> callback) -> callback.onResult(true))
                .when(mPriceTrackingUtilsJniMock)
                .setPriceTrackingStateForBookmark(
                        any(), anyLong(), anyBoolean(), any(), anyBoolean());
        doCallback(
                        0,
                        (Callback<List<BookmarkId>> callback) -> {
                            callback.onResult(Arrays.asList(mPriceTrackedBookmarkId));
                        })
                .when(mShoppingService)
                .getAllPriceTrackedBookmarks(any());
        ShoppingSpecifics trackedShoppingSpecifics =
                ShoppingSpecifics.newBuilder().setProductClusterId(1).build();
        PowerBookmarkMeta shoppingMetaTracked =
                PowerBookmarkMeta.newBuilder()
                        .setShoppingSpecifics(trackedShoppingSpecifics)
                        .build();
        doReturn(true)
                .when(mShoppingService)
                .isSubscribedFromCache(
                        PowerBookmarkUtils.createCommerceSubscriptionForShoppingSpecifics(
                                trackedShoppingSpecifics));
        doReturn(shoppingMetaTracked)
                .when(mBookmarkModel)
                .getPowerBookmarkMeta(mPriceTrackedBookmarkId);
        ShoppingFeatures.setShoppingListEligibleForTesting(true);

        mDragReorderableRecyclerViewAdapter =
                spy(new DragReorderableRecyclerViewAdapter(mActivity, mModelList));
        mMediator =
                new BookmarkManagerMediator(
                        mActivity,
                        mBookmarkModel,
                        mBookmarkOpener,
                        mSelectableListLayout,
                        mSelectionDelegate,
                        mRecyclerView,
                        mDragReorderableRecyclerViewAdapter,
                        mLargeIconBridge,
                        /* isDialogUi= */ true,
                        mBackPressStateSupplier,
                        mProfile,
                        mBookmarkUndoController,
                        mModelList,
                        mBookmarkUiPrefs,
                        mHideKeyboardRunnable,
                        mBookmarkImageFetcher,
                        mShoppingService,
                        mSnackbarManager,
                        mOnScrollListenerConsumer,
                        mBookmarkMoveSnackbarManager);
        mMediator.addUiObserver(mBookmarkUiObserver);
    }

    @After
    public void tearDown() {
        ShadowPostTask.reset();
    }

    private void finishLoading() {
        when(mBookmarkModel.isBookmarkModelLoaded()).thenReturn(true);
        verify(mBookmarkModel, atLeast(0))
                .finishLoadingBookmarkModel(mFinishLoadingBookmarkModelCaptor.capture());
        for (Runnable finishLoadingBookmarkModel :
                mFinishLoadingBookmarkModelCaptor.getAllValues()) {
            finishLoadingBookmarkModel.run();
        }
    }

    private void verifyBookmarkListMenuItem(
            ListItem item, @StringRes int titleId, boolean enabled) {
        assertEquals(item.model.get(ListMenuItemProperties.TITLE_ID), titleId);
        assertEquals(item.model.get(ListMenuItemProperties.ENABLED), enabled);
    }

    private void verifyCurrentViewTypes(int... expectedViewTypes) {
        BookmarkModelListTestUtil.verifyModelListHasViewTypes(mModelList, expectedViewTypes);
    }

    private void verifyCurrentBookmarkIds(BookmarkId... expectedBookmarkIds) {
        BookmarkModelListTestUtil.verifyModelListHasBookmarkIds(mModelList, expectedBookmarkIds);
    }


    @Test
    public void initAndLoadBookmarkModel() {
        finishLoading();
        assertEquals(BookmarkUiMode.LOADING, mMediator.getCurrentUiMode());
    }

    @Test
    public void setUrlBeforeModelLoaded() {
        // Setting a URL prior to the model loading should set the state for when it loads.
        mMediator.updateForUrl("chrome-native://bookmarks/folder/" + mFolderId1.getId());

        finishLoading();
        assertEquals(BookmarkUiMode.FOLDER, mMediator.getCurrentUiMode());
    }

    @Test
    public void testEmptyView_Bookmark() {
        doReturn(Arrays.asList()).when(mBookmarkModel).getChildIds(mMobileFolderId);

        // Setup and open Bookmark folder.
        finishLoading();
        assertEquals(BookmarkUiMode.LOADING, mMediator.getCurrentUiMode());
        mMediator.openFolder(mMobileFolderId);

        assertBookmarkListEmpty();

        ListItem emptyListItem = mModelList.get(1);
        assertEquals(ViewType.EMPTY_STATE, emptyListItem.type);
        assertEquals(
                R.string.bookmark_manager_empty_state,
                emptyListItem.model.get(BookmarkManagerEmptyStateProperties.EMPTY_STATE_TITLE_RES));
        assertEquals(
                R.string.bookmark_manager_back_to_page_by_adding_bookmark,
                emptyListItem.model.get(
                        BookmarkManagerEmptyStateProperties.EMPTY_STATE_DESCRIPTION_RES));
        assertEquals(
                R.drawable.bookmark_empty_state_illustration,
                emptyListItem.model.get(BookmarkManagerEmptyStateProperties.EMPTY_STATE_IMAGE_RES));
    }

    @Test
    public void testEmptyView_ReadingList() {
        doReturn(Arrays.asList()).when(mBookmarkModel).getChildIds(mReadingListFolderId);

        // Setup and open Bookmark folder.
        finishLoading();
        assertEquals(BookmarkUiMode.LOADING, mMediator.getCurrentUiMode());
        mMediator.openFolder(mReadingListFolderId);

        assertBookmarkListEmpty();

        ListItem emptyListItem = mModelList.get(1);
        assertEquals(ViewType.EMPTY_STATE, emptyListItem.type);
        assertEquals(
                R.string.reading_list_manager_empty_state,
                emptyListItem.model.get(BookmarkManagerEmptyStateProperties.EMPTY_STATE_TITLE_RES));
        assertEquals(
                R.string.reading_list_manager_save_page_to_read_later,
                emptyListItem.model.get(
                        BookmarkManagerEmptyStateProperties.EMPTY_STATE_DESCRIPTION_RES));
        assertEquals(
                R.drawable.reading_list_empty_state_illustration,
                emptyListItem.model.get(BookmarkManagerEmptyStateProperties.EMPTY_STATE_IMAGE_RES));
    }

    @Test
    public void testEmptyView_Search() {
        // Setup and open Bookmark folder.
        finishLoading();
        assertEquals(BookmarkUiMode.LOADING, mMediator.getCurrentUiMode());
        mMediator.openFolder(mMobileFolderId);
        mMediator.openSearchUi();

        assertBookmarkListEmpty();

        ListItem emptyListItem = mModelList.get(1);
        assertEquals(ViewType.EMPTY_STATE, emptyListItem.type);
        assertEquals(
                R.string.bookmark_manager_empty_state,
                emptyListItem.model.get(BookmarkManagerEmptyStateProperties.EMPTY_STATE_TITLE_RES));
        assertEquals(
                R.string.bookmark_manager_back_to_page_by_adding_bookmark,
                emptyListItem.model.get(
                        BookmarkManagerEmptyStateProperties.EMPTY_STATE_DESCRIPTION_RES));
        assertEquals(
                R.drawable.bookmark_empty_state_illustration,
                emptyListItem.model.get(BookmarkManagerEmptyStateProperties.EMPTY_STATE_IMAGE_RES));
    }

    @Test
    public void syncStateChangedBeforeModelLoaded() {
        verify(mSyncService, atLeast(1))
                .addSyncStateChangedListener(mSyncStateChangedListenerCaptor.capture());
        for (SyncStateChangedListener syncStateChangedListener :
                mSyncStateChangedListenerCaptor.getAllValues()) {
            syncStateChangedListener.syncStateChanged();
        }

        verify(mBookmarkModel, times(0)).getTopLevelFolderIds();

        finishLoading();
        mMediator.openFolder(mBookmarkModel.getRootFolderId());
        verify(mBookmarkModel, times(1)).getTopLevelFolderIds();
    }

    @Test
    public void testDestroy() {
        finishLoading();

        mMediator.onDestroy();
        verify(mBookmarkUiObserver).onDestroy();
        verify(mBookmarkUndoController).destroy();
        verify(mBookmarkImageFetcher).destroy();
        verify(mLargeIconBridge).destroy();
        verify(mBookmarkMoveSnackbarManager).destroy();
    }

    @Test
    public void onBackPressed_SelectableListLayoutIntercepts() {
        finishLoading();

        doReturn(true).when(mSelectableListLayout).onBackPressed();

        assertTrue(mMediator.onBackPressed());
    }

    @Test
    public void onBackPressed_EmptyStateStack() {
        finishLoading();

        mMediator.clearStateStackForTesting();
        assertFalse(mMediator.onBackPressed());
    }

    @Test
    public void onBackPressed_SingleStateStack() {
        finishLoading();

        assertFalse(mMediator.onBackPressed());
    }

    @Test
    public void onBackPressed_MultipleStateStack() {
        finishLoading();

        mMediator.openFolder(mFolderId1);
        mMediator.openFolder(mFolderId2);
        assertTrue(mMediator.onBackPressed());
    }

    @Test
    public void onBackPressed_AndThenModelEvent() {
        initAndLoadBookmarkModel();
        assertFalse(mMediator.onBackPressed());

        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor.getValue().bookmarkModelChanged();
        // This test is verifying the observer event doesn't crash.
    }

    @Test
    public void testMoveDownUp() {
        finishLoading();
        mMediator.openFolder(mFolderId1);

        mMediator.moveDownOne(mFolderId2);
        verify(mBookmarkModel)
                .reorderBookmarks(mFolderId1, new long[] {mFolderId3.getId(), mFolderId2.getId()});

        mMediator.moveUpOne(mFolderId2);
        verify(mBookmarkModel)
                .reorderBookmarks(mFolderId1, new long[] {mFolderId2.getId(), mFolderId3.getId()});
    }

    @Test
    public void testDrag() {
        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.MANUAL);

        finishLoading();
        mMediator.openFolder(mFolderId1);
        DraggabilityProvider draggabilityProvider = mMediator.getDraggabilityProvider();
        assertTrue(draggabilityProvider.isPassivelyDraggable(mModelList.get(1).model));
        assertTrue(draggabilityProvider.isActivelyDraggable(mModelList.get(1).model));
        DragStateDelegate dragStateDelegate = mMediator.getDragStateDelegate();
        assertTrue(dragStateDelegate.getDragEnabled());

        when(mSelectionDelegate.isItemSelected(mFolderId2)).thenReturn(true);
        assertTrue(draggabilityProvider.isActivelyDraggable(mModelList.get(1).model));

        mModelList.move(1, 2);
        verify(mDragReorderableRecyclerViewAdapter)
                .addDragListener(mDragListenerArgumentCaptor.capture());
        mDragListenerArgumentCaptor.getValue().onSwap();
        verify(mBookmarkModel)
                .reorderBookmarks(mFolderId1, new long[] {mFolderId3.getId(), mFolderId2.getId()});

        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.CHRONOLOGICAL);
        assertFalse(dragStateDelegate.getDragEnabled());
    }

    @Test
    public void testDrag_improvedBookmarks_whileFiltering() {
        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.MANUAL);

        finishLoading();
        mMediator.openFolder(mMobileFolderId);
        DraggabilityProvider draggabilityProvider = mMediator.getDraggabilityProvider();
        assertTrue(draggabilityProvider.isPassivelyDraggable(mModelList.get(1).model));
        assertTrue(draggabilityProvider.isActivelyDraggable(mModelList.get(1).model));
        DragStateDelegate dragStateDelegate = mMediator.getDragStateDelegate();
        assertTrue(dragStateDelegate.getDragEnabled());

        // When a filter is selected, dragging should be disabled.
        PropertyModel model = mModelList.get(0).model;
        assertTrue(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
        model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_TOGGLE_CALLBACK).onResult(true);
        assertTrue(draggabilityProvider.isPassivelyDraggable(mModelList.get(1).model));
        assertTrue(draggabilityProvider.isActivelyDraggable(mModelList.get(1).model));
        assertFalse(dragStateDelegate.getDragEnabled());
    }

    @Test
    public void testSearch() {
        when(mBookmarkModel.searchBookmarks(anyString(), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId3));
        finishLoading();

        mMediator.openFolder(mFolderId1);
        assertEquals(3, mModelList.size());

        mMediator.openSearchUi();
        mMediator.search("3");
        assertEquals(2, mModelList.size());

        mMediator.onEndSearch();
        assertEquals(3, mModelList.size());
    }

    @Test
    public void testBookmarkRemoved() {
        finishLoading();
        // Additional folder so that there's 3, and all the locations can be tested.
        final BookmarkId folderId4 = new BookmarkId(mId++, BookmarkType.NORMAL);
        final BookmarkItem folderItem4 =
                new BookmarkItem(
                        folderId4,
                        "Folder4",
                        null,
                        true,
                        mFolderId1,
                        true,
                        false,
                        0,
                        false,
                        0,
                        false);
        doReturn(folderItem4).when(mBookmarkModel).getBookmarkById(folderId4);
        doReturn(Arrays.asList(mFolderId2, mFolderId3, folderId4))
                .when(mBookmarkModel)
                .getChildIds(mFolderId1);

        mMediator.openFolder(mFolderId1);
        assertEquals(4, mModelList.size());
        assertEquals(
                Location.MIDDLE, mModelList.get(2).model.get(BookmarkManagerProperties.LOCATION));

        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(
                        mFolderItem1, 0, mFolderItem2, /* isDoingExtensiveChanges= */ false);
        assertEquals(3, mModelList.size());
        assertEquals(Location.TOP, mModelList.get(1).model.get(BookmarkManagerProperties.LOCATION));
    }

    @Test
    public void testAttachmentChanges() {
        mMediator.onAttachedToWindow();
        verify(mBookmarkUndoController).setEnabled(true);

        mMediator.onDetachedFromWindow();
        verify(mBookmarkUndoController).setEnabled(false);
    }

    @Test
    public void onPreferenceChanged_ViewPreferenceUpdated() {
        mMediator.onBookmarkModelLoaded();
        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);
        assertEquals(ViewType.IMPROVED_BOOKMARK_VISUAL, mModelList.get(1).type);
    }

    @Test
    public void testBuildImprovedBookmarkRow() {
        finishLoading();
        mMediator.openFolder(mFolderId2);
        assertEquals(2, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_COMPACT, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mBookmarkItem21,
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem());
        assertEquals(mBookmarkId21, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(mBookmarkItem21.getTitle(), model.get(ImprovedBookmarkRowProperties.TITLE));
        assertEquals(EXAMPLE_URL_FORMATTED, model.get(ImprovedBookmarkRowProperties.DESCRIPTION));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertNull(model.get(ImprovedBookmarkRowProperties.START_ICON_TINT));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.POPUP_LISTENER));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.DRAG_ENABLED));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE));
        assertEquals(true, model.get(ImprovedBookmarkRowProperties.EDITABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER));
        assertEquals(
                ImageVisibility.MENU,
                model.get(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY));
    }

    @Test
    public void testBuildImprovedBookmarkRow_NotEditable() {
        finishLoading();
        mMediator.openFolder(mRootFolderId);
        assertEquals(5, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_COMPACT, item.type);

        PropertyModel model = item.model;
        assertEquals(
                mDesktopFolderItem,
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem());
        assertEquals(
                ImageVisibility.NONE,
                model.get(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY));
    }

    @Test
    public void testBuildImprovedBookmarkRow_ReadingList() {
        finishLoading();
        mMediator.openFolder(mReadingListFolderId);
        assertEquals(4, mModelList.size());

        ListItem item = mModelList.get(2);
        assertEquals(ViewType.IMPROVED_BOOKMARK_COMPACT, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mReadingListItem,
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem());
        assertEquals(mReadingListId, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(mReadingListItem.getTitle(), model.get(ImprovedBookmarkRowProperties.TITLE));
        assertEquals(EXAMPLE_URL_FORMATTED, model.get(ImprovedBookmarkRowProperties.DESCRIPTION));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
    }

    @Test
    public void testBuildImprovedBookmarkRow_Visual() {
        finishLoading();
        mMediator.openFolder(mFolderId2);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);
        assertEquals(2, mModelList.size());

        ListItem item =
                mMediator.buildImprovedBookmarkRow(
                        BookmarkListEntry.createBookmarkEntry(
                                mBookmarkItem21,
                                null,
                                mBookmarkUiPrefs.getBookmarkRowDisplayPref()));
        assertEquals(ViewType.IMPROVED_BOOKMARK_VISUAL, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mBookmarkItem21,
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem());
        assertEquals(mBookmarkId21, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(mBookmarkItem21.getTitle(), model.get(ImprovedBookmarkRowProperties.TITLE));
        assertEquals(EXAMPLE_URL_FORMATTED, model.get(ImprovedBookmarkRowProperties.DESCRIPTION));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertNull(model.get(ImprovedBookmarkRowProperties.START_ICON_TINT));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.POPUP_LISTENER));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.DRAG_ENABLED));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE));
        assertEquals(true, model.get(ImprovedBookmarkRowProperties.EDITABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER));
    }

    @Test
    public void testBuildImprovedBookmarkRow_SelectionStateCarriedOver() {
        doReturn(true).when(mSelectionDelegate).isItemSelected(mBookmarkItem21.getId());
        ListItem item =
                mMediator.buildImprovedBookmarkRow(
                        BookmarkListEntry.createBookmarkEntry(
                                mBookmarkItem21,
                                null,
                                mBookmarkUiPrefs.getBookmarkRowDisplayPref()));
        assertTrue(item.model.get(ImprovedBookmarkRowProperties.SELECTED));

        doReturn(false).when(mSelectionDelegate).isItemSelected(mBookmarkItem21.getId());
        item =
                mMediator.buildImprovedBookmarkRow(
                        BookmarkListEntry.createBookmarkEntry(
                                mBookmarkItem21,
                                null,
                                mBookmarkUiPrefs.getBookmarkRowDisplayPref()));
        assertFalse(item.model.get(ImprovedBookmarkRowProperties.SELECTED));
    }

    @Test
    public void testBuildImprovedBookmarkRow_Shopping() {
        ShoppingSpecifics specifics =
                ShoppingSpecifics.newBuilder()
                        .setCurrentPrice(
                                ProductPrice.newBuilder()
                                        .setCurrencyCode("USD")
                                        .setAmountMicros(100)
                                        .build())
                        .setPreviousPrice(
                                ProductPrice.newBuilder()
                                        .setCurrencyCode("USD")
                                        .setAmountMicros(100)
                                        .build())
                        .setIsPriceTracked(true)
                        .build();
        PowerBookmarkMeta meta =
                PowerBookmarkMeta.newBuilder().setShoppingSpecifics(specifics).build();
        doReturn(meta).when(mBookmarkModel).getPowerBookmarkMeta(mBookmarkId21);

        finishLoading();
        mMediator.openFolder(mFolderId2);
        assertEquals(2, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_COMPACT, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ACCESSORY_VIEW));
    }

    @Test
    public void testBuildImprovedBookmarkRow_Folder() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        assertEquals(3, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_COMPACT, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mFolderItem2.getId(),
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem().getId());
        assertEquals(mFolderId2, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(
                mFolderItem2.getTitle() + " (1)", model.get(ImprovedBookmarkRowProperties.TITLE));
        assertFalse(model.get(ImprovedBookmarkRowProperties.DESCRIPTION_VISIBLE));
        assertEquals(
                ImageVisibility.DRAWABLE,
                model.get(ImprovedBookmarkRowProperties.START_IMAGE_VISIBILITY));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_TINT));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.POPUP_LISTENER));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.DRAG_ENABLED));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE));
        assertEquals(true, model.get(ImprovedBookmarkRowProperties.EDITABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER));
    }

    @Test
    @DisableFeatures({
        SyncFeatureMap.SYNC_ENABLE_BOOKMARKS_IN_TRANSPORT_MODE,
        ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS
    })
    public void testBuildImprovedBookmarkRow_Folder_Visual() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);
        assertEquals(3, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_VISUAL, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mFolderItem2.getId(),
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem().getId());
        assertEquals(mFolderId2, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(mFolderItem2.getTitle(), model.get(ImprovedBookmarkRowProperties.TITLE));
        assertFalse(model.get(ImprovedBookmarkRowProperties.DESCRIPTION_VISIBLE));
        assertEquals(
                ImageVisibility.FOLDER_DRAWABLE,
                model.get(ImprovedBookmarkRowProperties.START_IMAGE_VISIBILITY));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.POPUP_LISTENER));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.DRAG_ENABLED));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE));
        assertEquals(true, model.get(ImprovedBookmarkRowProperties.EDITABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.FOLDER_START_IMAGE_FOLDER_DRAWABLES));
    }

    @Test
    public void testBuildImprovedBookmarkRow_readingListFolder() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        assertEquals(3, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_COMPACT, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mFolderItem2.getId(),
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem().getId());
        assertEquals(mFolderId2, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(
                mFolderItem2.getTitle() + " (1)", model.get(ImprovedBookmarkRowProperties.TITLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_TINT));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.POPUP_LISTENER));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.DRAG_ENABLED));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE));
        assertEquals(true, model.get(ImprovedBookmarkRowProperties.EDITABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER));
    }

    @Test
    public void testBuildImprovedBookmarkRow_FolderVisual() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);
        assertEquals(3, mModelList.size());

        ListItem item = mModelList.get(1);
        assertEquals(ViewType.IMPROVED_BOOKMARK_VISUAL, item.type);

        PropertyModel model = item.model;
        assertNotNull(model);
        assertEquals(
                mFolderItem2,
                model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY).getBookmarkItem());
        assertEquals(mFolderId2, model.get(BookmarkManagerProperties.BOOKMARK_ID));
        assertEquals(mFolderItem2.getTitle(), model.get(ImprovedBookmarkRowProperties.TITLE));
        assertFalse(model.get(ImprovedBookmarkRowProperties.DESCRIPTION_VISIBLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.POPUP_LISTENER));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertEquals(false, model.get(ImprovedBookmarkRowProperties.DRAG_ENABLED));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE));
        assertEquals(true, model.get(ImprovedBookmarkRowProperties.EDITABLE));
        assertNotNull(model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER));
    }

    @Test
    public void testCreateListMenuModelList() {
        finishLoading();
        mMediator.openFolder(mFolderId2);

        BookmarkListEntry entry =
                BookmarkListEntry.createBookmarkEntry(
                        mBookmarkItem21, null, BookmarkRowDisplayPref.COMPACT);
        ModelList modelList = mMediator.createListMenuModelList(entry, Location.MIDDLE);
        assertEquals(6, modelList.size());
        verifyBookmarkListMenuItem(modelList.get(0), R.string.bookmark_item_select, true);
        verifyBookmarkListMenuItem(modelList.get(1), R.string.bookmark_item_edit, true);
        verifyBookmarkListMenuItem(modelList.get(2), R.string.bookmark_item_move, true);
        verifyBookmarkListMenuItem(modelList.get(3), R.string.bookmark_item_delete, true);

        mMediator.openSearchUi();
        modelList = mMediator.createListMenuModelList(entry, Location.MIDDLE);
        assertEquals(5, modelList.size());
        verifyBookmarkListMenuItem(modelList.get(4), R.string.bookmark_show_in_folder, true);
    }

    @Test
    public void testCreateListMenuModelList_ReadingList() {
        finishLoading();
        mMediator.openFolder(mReadingListFolderId);

        BookmarkListEntry entry =
                BookmarkListEntry.createBookmarkEntry(
                        mReadingListItem, null, BookmarkRowDisplayPref.COMPACT);
        ModelList modelList = mMediator.createListMenuModelList(entry, Location.MIDDLE);
        assertEquals(5, modelList.size());
        verifyBookmarkListMenuItem(modelList.get(0), R.string.reading_list_mark_as_read, true);
        verifyBookmarkListMenuItem(modelList.get(1), R.string.bookmark_item_select, true);
        verifyBookmarkListMenuItem(modelList.get(2), R.string.bookmark_item_edit, true);
        verifyBookmarkListMenuItem(modelList.get(3), R.string.bookmark_item_move, true);
        verifyBookmarkListMenuItem(modelList.get(4), R.string.bookmark_item_delete, true);
    }

    @Test
    public void testcreateListMenuModelList_shopping() {
        finishLoading();
        mMediator.openFolder(mFolderId2);

        doReturn(true).when(mShoppingService).isSubscribedFromCache(any());
        PowerBookmarkMeta meta =
                PowerBookmarkMeta.newBuilder()
                        .setShoppingSpecifics(
                                ShoppingSpecifics.newBuilder()
                                        .setProductClusterId(123)
                                        .setOfferId(456)
                                        .setCountryCode("us")
                                        .setCurrentPrice(
                                                ProductPrice.newBuilder()
                                                        .setAmountMicros(100)
                                                        .build())
                                        .build())
                        .build();
        BookmarkListEntry entry =
                BookmarkListEntry.createBookmarkEntry(
                        mBookmarkItem21, meta, BookmarkRowDisplayPref.COMPACT);
        ModelList modelList = mMediator.createListMenuModelList(entry, Location.MIDDLE);
        assertEquals(7, modelList.size());
        verifyBookmarkListMenuItem(
                modelList.get(6), R.string.disable_price_tracking_menu_item, true);

        doReturn(false).when(mShoppingService).isSubscribedFromCache(any());
        modelList = mMediator.createListMenuModelList(entry, Location.MIDDLE);
        assertEquals(7, modelList.size());
        verifyBookmarkListMenuItem(
                modelList.get(6), R.string.enable_price_tracking_menu_item, true);
    }

    @Test
    public void testcreateListMenuModelList_shopping_notEligible() {
        ShoppingFeatures.setShoppingListEligibleForTesting(false);

        finishLoading();
        mMediator.openFolder(mFolderId2);

        doReturn(true).when(mShoppingService).isSubscribedFromCache(any());
        doReturn(false).when(mShoppingService).isShoppingListEligible();
        PowerBookmarkMeta meta =
                PowerBookmarkMeta.newBuilder()
                        .setShoppingSpecifics(
                                ShoppingSpecifics.newBuilder()
                                        .setProductClusterId(123)
                                        .setOfferId(456)
                                        .setCountryCode("us")
                                        .setCurrentPrice(
                                                ProductPrice.newBuilder()
                                                        .setAmountMicros(100)
                                                        .build())
                                        .build())
                        .build();
        BookmarkListEntry entry =
                BookmarkListEntry.createBookmarkEntry(
                        mBookmarkItem21, meta, BookmarkRowDisplayPref.COMPACT);
        ModelList modelList = mMediator.createListMenuModelList(entry, Location.MIDDLE);
        // The 7th item would be the enable/disable price tracking.
        assertEquals(6, modelList.size());
    }

    @Test
    @EnableFeatures(ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS)
    public void testShoppingFilterUpdatedBySubscriptions() {
        finishLoading();

        verify(mShoppingService).addSubscriptionsObserver(mSubscriptionsObserver.capture());
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        assertNotNull(mSubscriptionsObserver.getValue());

        doReturn(new ArrayList<BookmarkItem>())
                .when(mBookmarkModel)
                .searchBookmarks(anyString(), anyInt());
        doReturn(new ArrayList<BookmarkId>()).when(mBookmarkModel).getChildIds(mFolderId1);

        mMediator.openFolder(mFolderId1);

        PropertyModel model = mModelList.get(0).model;
        assertTrue(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
        model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_TOGGLE_CALLBACK).onResult(true);

        assertBookmarkListEmpty();

        // Simulate creation of a tracked product.
        BookmarkId trackedProductBookmarkId = new BookmarkId(9999L, 0);
        BookmarkItem trackedProductBookmark =
                new BookmarkItem(
                        trackedProductBookmarkId,
                        "Tracked product",
                        new GURL("http://example.com/product"),
                        false,
                        mFolderId1,
                        true,
                        false,
                        0,
                        false,
                        0,
                        false);
        doReturn(Arrays.asList(trackedProductBookmarkId))
                .when(mBookmarkModel)
                .searchBookmarks(anyString(), anyInt());
        doReturn(Arrays.asList(trackedProductBookmarkId))
                .when(mBookmarkModel)
                .getChildIds(mFolderId1);
        doReturn(trackedProductBookmark)
                .when(mBookmarkModel)
                .getBookmarkById(trackedProductBookmarkId);

        ShoppingSpecifics shoppingSpecifics =
                ShoppingSpecifics.newBuilder().setProductClusterId(1).build();
        PowerBookmarkMeta shoppingMeta =
                PowerBookmarkMeta.newBuilder().setShoppingSpecifics(shoppingSpecifics).build();
        doReturn(shoppingMeta).when(mBookmarkModel).getPowerBookmarkMeta(trackedProductBookmarkId);
        CommerceSubscription sub =
                PowerBookmarkUtils.createCommerceSubscriptionForShoppingSpecifics(
                        shoppingSpecifics);

        mBookmarkModelObserverArgumentCaptor.getValue().bookmarkModelChanged();

        doReturn(true).when(mShoppingService).isSubscribedFromCache(any());

        mSubscriptionsObserver.getValue().onSubscribe(sub, true);

        assertEquals(2, mModelList.size());
    }

    @Test
    public void testCreateListMenuForBookmark() {
        finishLoading();
        mMediator.openFolder(mFolderId2);

        // This is the first item mFolderId2.
        BasicListMenu menu =
                (BasicListMenu) mMediator.createListMenuForBookmark(mModelList.get(1).model);
        assertNotNull(menu);

        // Select.
        menu.onItemClick(null, null, 0, 0);
        verify(mSelectionDelegate).toggleSelectionForItem(mBookmarkId21);

        // Edit.
        // TODO(crbug.com/40267749): This doesn't actually open the activity yet.
        menu.onItemClick(null, null, 1, 0);

        // Move.
        // TODO(crbug.com/40267749): This doesn't actually open the activity yet.
        menu.onItemClick(null, null, 2, 0);

        // Delete.
        menu.onItemClick(null, null, 3, 0);
        verify(mBookmarkModel).deleteBookmarks(mBookmarkId21);
    }

    @Test
    public void testCreateListMenuForBookmark_priceTracking() {
        finishLoading();

        PowerBookmarkMeta meta =
                PowerBookmarkMeta.newBuilder()
                        .setShoppingSpecifics(
                                ShoppingSpecifics.newBuilder()
                                        .setProductClusterId(123)
                                        .setOfferId(456)
                                        .setCountryCode("us")
                                        .setCurrentPrice(
                                                ProductPrice.newBuilder()
                                                        .setAmountMicros(100)
                                                        .build())
                                        .build())
                        .build();
        doReturn(meta).when(mBookmarkModel).getPowerBookmarkMeta(mBookmarkId21);
        mMediator.openFolder(mFolderId2);

        doReturn(true).when(mShoppingService).isSubscribedFromCache(any());
        PropertyModel model = mModelList.get(1).model;
        ModelList menuModelList =
                mMediator.createListMenuModelList(
                        model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY),
                        model.get(BookmarkManagerProperties.LOCATION));
        verifyMenuListItemTitles(
                menuModelList,
                R.string.bookmark_item_select,
                R.string.bookmark_item_edit,
                R.string.bookmark_item_move,
                R.string.bookmark_item_delete,
                R.string.disable_price_tracking_menu_item);

        BasicListMenu menu =
                (BasicListMenu) mMediator.createListMenuForBookmark(mModelList.get(1).model);
        assertNotNull(menu);

        // Delete.
        menu.onItemClick(null, null, 4, 0);
        verify(mPriceTrackingUtilsJniMock)
                .setPriceTrackingStateForBookmark(
                        any(), anyLong(), anyBoolean(), any(), anyBoolean());
    }

    @Test
    @DisableFeatures({
        SyncFeatureMap.SYNC_ENABLE_BOOKMARKS_IN_TRANSPORT_MODE,
        ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS
    })
    public void testParentFolderUpdatedWhenChildDeleted() {
        finishLoading();

        doReturn(1).when(mBookmarkModel).getTotalBookmarkCount(mFolderId2);
        doReturn(2).when(mBookmarkModel).getTotalBookmarkCount(mFolderId3);
        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);

        assertEquals(3, mModelList.size());
        assertEquals(
                1, mModelList.get(1).model.get(ImprovedBookmarkRowProperties.FOLDER_CHILD_COUNT));

        doReturn(0).when(mBookmarkModel).getTotalBookmarkCount(mFolderId2);
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(mFolderItem2, 0, mBookmarkItem21, false);

        assertEquals(
                0, mModelList.get(1).model.get(ImprovedBookmarkRowProperties.FOLDER_CHILD_COUNT));
    }

    @Test
    public void testChildRemoved() {
        finishLoading();

        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);

        assertEquals(3, mModelList.size());

        reset(mBookmarkUiObserver);
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(mFolderItem2, 0, mFolderItem2, false);
        assertEquals(2, mModelList.size());
        verify(mBookmarkUiObserver, times(0)).onUiModeChanged(BookmarkUiMode.FOLDER);
    }

    @Test
    public void testChildRemoved_indexNotFound() {
        finishLoading();

        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);

        assertEquals(3, mModelList.size());

        reset(mBookmarkUiObserver);
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(mFolderItem2, 0, mBookmarkItem21, false);

        assertEquals(3, mModelList.size());
        verify(mBookmarkUiObserver, times(1)).onUiModeChanged(BookmarkUiMode.FOLDER);
    }

    @Test
    public void testPromoHeader() {
        BookmarkPromoHeader.forcePromoStateForTesting(SyncPromoState.PROMO_FOR_SIGNED_IN_STATE);
        mMediator.getPromoHeaderManager().syncStateChanged();
        finishLoading();
        mMediator.openFolder(mFolderId1);

        verifyCurrentViewTypes(
                ViewType.SEARCH_BOX,
                ViewType.PERSONALIZED_SYNC_PROMO,
                ViewType.IMPROVED_BOOKMARK_COMPACT,
                ViewType.IMPROVED_BOOKMARK_COMPACT);
        assertNotEquals(
                Resources.ID_NULL,
                mModelList.get(1).model.get(BookmarkManagerProperties.PROMO_TOP_MARGIN_RES));

        BookmarkPromoHeader.forcePromoStateForTesting(SyncPromoState.NO_PROMO);
        mMediator.getPromoHeaderManager().syncStateChanged();

        verifyCurrentViewTypes(
                ViewType.SEARCH_BOX,
                ViewType.IMPROVED_BOOKMARK_COMPACT,
                ViewType.IMPROVED_BOOKMARK_COMPACT);
    }

    @Test
    public void testPromoHeaderDefaultMargin() {
        BookmarkPromoHeader.forcePromoStateForTesting(SyncPromoState.PROMO_FOR_SIGNED_IN_STATE);
        mMediator.getPromoHeaderManager().syncStateChanged();
        finishLoading();
        mMediator.openFolder(mFolderId1);

        verifyCurrentViewTypes(
                ViewType.SEARCH_BOX,
                ViewType.PERSONALIZED_SYNC_PROMO,
                ViewType.IMPROVED_BOOKMARK_COMPACT,
                ViewType.IMPROVED_BOOKMARK_COMPACT);
        assertEquals(
                Resources.ID_NULL,
                mModelList.get(2).model.get(BookmarkManagerProperties.PROMO_TOP_MARGIN_RES));
    }

    @Test
    public void testSearchBox() {
        when(mBookmarkModel.searchBookmarks(eq("3"), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId3));
        finishLoading();
        mMediator.openFolder(mFolderId1);
        verifyCurrentViewTypes(
                ViewType.SEARCH_BOX,
                ViewType.IMPROVED_BOOKMARK_COMPACT,
                ViewType.IMPROVED_BOOKMARK_COMPACT);

        mModelList.addObserver(mListObserver);
        mModelList
                .get(0)
                .model
                .get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK)
                .onResult("3");
        verifyCurrentViewTypes(ViewType.SEARCH_BOX, ViewType.IMPROVED_BOOKMARK_COMPACT);
        verify(mListObserver, never()).onItemRangeChanged(any(), eq(0), anyInt(), any());
        verify(mListObserver, never()).onItemRangeRemoved(any(), eq(0), anyInt());
        verify(mListObserver, never()).onItemRangeInserted(any(), eq(0), anyInt());
        verify(mListObserver).onItemRangeChanged(any(), eq(1), anyInt(), any());
    }

    @Test
    public void testDeleteDuringSelection() {
        // Inspired by https://crbug.com/1449447 where the search row didn't have a property and
        // we crashed when trying to handle deletion during selection.

        finishLoading();
        mMediator.openFolder(mFolderId1);
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);

        // Setup selection mock to make it seem like folder 2 is selected.
        when(mSelectionDelegate.isItemSelected(mFolderId2)).thenReturn(true);
        doReturn(Arrays.asList(mFolderId2)).when(mSelectionDelegate).getSelectedItemsAsList();

        // Pretend to delete folder 2.
        doReturn(Arrays.asList(mFolderId3)).when(mBookmarkModel).getChildIds(mFolderId1);
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(
                        mFolderItem1, 0, mFolderItem2, /* isDoingExtensiveChanges= */ false);

        // Mostly just verifying that #syncAdapterAndSelectionDelegate() doesn't crash.
        assertEquals(2, mModelList.size());
    }

    @Test
    public void testDeleteMultipleDuringSelection() {
        // Inspired by https://crbug.com/1490506 where deleting multiple items were unselected one
        // by one. But this caused selection notifications with deleted items to still reach other
        // components.

        String queryString = "test";
        when(mBookmarkModel.searchBookmarks(eq(queryString), anyInt()))
                .thenReturn(Arrays.asList(mFolderId2, mFolderId3, mBookmarkId21));

        finishLoading();
        mMediator.openFolder(mFolderId1);
        PropertyModel searchBoxModel = mModelList.get(0).model;
        searchBoxModel
                .get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK)
                .onResult(queryString);
        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3, mBookmarkId21);

        // Setup selection mock to make it seem like everything is selected
        when(mSelectionDelegate.isItemSelected(mFolderId2)).thenReturn(true);
        when(mSelectionDelegate.isItemSelected(mFolderId3)).thenReturn(true);
        when(mSelectionDelegate.isItemSelected(mBookmarkId21)).thenReturn(true);
        doReturn(Arrays.asList(mFolderId2, mFolderId3, mBookmarkId21))
                .when(mSelectionDelegate)
                .getSelectedItemsAsList();

        // Pretend to delete folder 2 and folder 3. Pause the looper to get the removes to dedupe.
        ShadowPostTask.reset();
        doReturn(Arrays.asList(mBookmarkId21)).when(mBookmarkModel).getChildIds(mFolderId1);
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        when(mBookmarkModel.searchBookmarks(eq(queryString), anyInt()))
                .thenReturn(Arrays.asList(mBookmarkId21));
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(
                        mFolderItem1, 0, mFolderItem2, /* isDoingExtensiveChanges= */ false);
        mBookmarkModelObserverArgumentCaptor
                .getValue()
                .bookmarkNodeRemoved(
                        mFolderItem1, 0, mFolderItem3, /* isDoingExtensiveChanges= */ false);

        ShadowLooper.idleMainLooper();
        verifyCurrentBookmarkIds(null, mBookmarkId21);
        // Only 1 selection update should be sent out. This minimizes event notification spam and
        // complexity for observers.
        verify(mSelectionDelegate).setSelectedItems(Collections.singleton(mBookmarkId21));
    }

    @Test
    public void testSearchTextChangeCallback() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        reset(mHideKeyboardRunnable);
        assertEquals(3, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel searchBoxModel = mModelList.get(0).model;
        Callback<String> searchTextChangeCallback =
                searchBoxModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK);
        assertNotNull(searchTextChangeCallback);

        String searchText = "foo";
        searchTextChangeCallback.onResult(searchText);
        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        assertEquals(searchText, searchBoxModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT));
        verify(mBookmarkModel).searchBookmarks(eq(searchText), anyInt());
        assertTrue(
                searchBoxModel.get(
                        BookmarkSearchBoxRowProperties.CLEAR_SEARCH_TEXT_BUTTON_VISIBILITY));
        verifyNoInteractions(mHideKeyboardRunnable);

        searchText = "";
        searchTextChangeCallback.onResult(searchText);
        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        assertEquals(searchText, searchBoxModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT));
        verify(mBookmarkModel, never()).searchBookmarks(eq(searchText), anyInt());
        assertFalse(
                searchBoxModel.get(
                        BookmarkSearchBoxRowProperties.CLEAR_SEARCH_TEXT_BUTTON_VISIBILITY));
        verifyNoInteractions(mHideKeyboardRunnable);

        searchTextChangeCallback.onResult("bar");
        mMediator.onBackPressed();
        assertEquals(BookmarkUiMode.FOLDER, mMediator.getCurrentUiMode());
        assertEquals("", searchBoxModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT));
        assertFalse(
                searchBoxModel.get(
                        BookmarkSearchBoxRowProperties.CLEAR_SEARCH_TEXT_BUTTON_VISIBILITY));
        verify(mHideKeyboardRunnable).run();
    }

    @Test
    public void testImprovedSpecialFolders() {
        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.ALPHABETICAL);
        final @ColorInt int specialBackgroundColor =
                SemanticColorUtils.getColorPrimaryContainer(mActivity);
        final @ColorInt int normalBackgroundColor =
                ChromeColors.getSurfaceColor(mActivity, R.dimen.default_elevation_1);
        finishLoading();

        mMediator.openFolder(mRootFolderId);

        assertEquals(5, mModelList.size());
        verifyCurrentBookmarkIds(
                null, mDesktopFolderId, mMobileFolderId, mOtherFolderId, mReadingListFolderId);
        assertEquals(
                specialBackgroundColor,
                mModelList
                        .get(1)
                        .model
                        .get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertEquals(
                specialBackgroundColor,
                mModelList
                        .get(2)
                        .model
                        .get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertEquals(
                specialBackgroundColor,
                mModelList
                        .get(3)
                        .model
                        .get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertEquals(
                specialBackgroundColor,
                mModelList
                        .get(4)
                        .model
                        .get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));

        mMediator.openFolder(mFolderId1);

        assertEquals(3, mModelList.size());
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);
        assertEquals(
                normalBackgroundColor,
                mModelList
                        .get(1)
                        .model
                        .get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
        assertEquals(
                normalBackgroundColor,
                mModelList
                        .get(2)
                        .model
                        .get(ImprovedBookmarkRowProperties.START_AREA_BACKGROUND_COLOR));
    }

    @Test
    @EnableFeatures({ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS})
    public void testRootLevelFolders_accountFoldersPresent() {
        doReturn(true).when(mBookmarkModel).areAccountBookmarkFoldersActive();
        BookmarkId accountReadingListId = new BookmarkId(mId++, BookmarkType.READING_LIST);
        BookmarkItem accountReadingListItem =
                new BookmarkItem(
                        accountReadingListId,
                        "Reading list",
                        null,
                        /* isFolder= */ true,
                        mRootFolderId,
                        /* isEditable= */ false,
                        /* isManaged= */ false,
                        /* dateAdded= */ 0,
                        /* read= */ false,
                        /* dateLastOpened= */ 0,
                        /* isAccountBookmark= */ true);
        doReturn(accountReadingListItem).when(mBookmarkModel).getBookmarkById(accountReadingListId);
        doReturn(
                        Arrays.asList(
                                accountReadingListId,
                                mDesktopFolderId,
                                mMobileFolderId,
                                mOtherFolderId,
                                mReadingListFolderId))
                .when(mBookmarkModel)
                .getTopLevelFolderIds();

        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.ALPHABETICAL);
        finishLoading();

        mMediator.openFolder(mRootFolderId);

        assertEquals(8, mModelList.size());
        verifyCurrentBookmarkIds(
                null, // Search bar
                null, // Account heading
                accountReadingListId,
                null, //  Local heading.
                mDesktopFolderId,
                mMobileFolderId,
                mOtherFolderId,
                mReadingListFolderId);

        assertEquals(ViewType.SECTION_HEADER, mModelList.get(1).type);
        BookmarkListEntry entry =
                mModelList.get(1).model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY);
        assertEquals(
                R.string.account_bookmarks_section_header, entry.getSectionHeaderData().titleRes);

        assertEquals(ViewType.SECTION_HEADER, mModelList.get(3).type);
        entry = mModelList.get(3).model.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY);
        assertEquals(
                R.string.local_bookmarks_section_header, entry.getSectionHeaderData().titleRes);
    }

    // Tests directly related to a regression.

    @Test
    public void testShowInFolder() { // https://crbug.com/1456275
        when(mBookmarkModel.searchBookmarks(eq("test"), anyInt()))
                .thenReturn(Collections.singletonList(mBookmarkId21));
        finishLoading();

        mMediator.openFolder(mFolderId1);
        mMediator.openSearchUi();
        mMediator.search("test");
        BasicListMenu menu =
                (BasicListMenu) mMediator.createListMenuForBookmark(mModelList.get(1).model);
        assertNotNull(menu);
        assertFalse(mModelList.get(1).model.get(BookmarkManagerProperties.IS_HIGHLIGHTED));

        // Show in folder.
        menu.onItemClick(null, null, 4, 0);
        assertTrue(mModelList.get(1).model.get(BookmarkManagerProperties.IS_HIGHLIGHTED));
    }

    @Test
    public void testChangeSortOrderDuringSearch() { // https://crbug.com/1464965
        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.ALPHABETICAL);
        when(mBookmarkModel.searchBookmarks(eq("test"), anyInt()))
                .thenReturn(Arrays.asList(mFolderId1, mFolderId2));
        finishLoading();
        mMediator.openFolder(mFolderId1);

        mMediator.search("test");
        verifyCurrentBookmarkIds(null, mFolderId1, mFolderId2);

        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.REVERSE_ALPHABETICAL);
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId1);
    }

    @Test
    public void testSelectionMoved_dropSelection() {
        finishLoading();
        mMediator.openFolder(mFolderId1);

        doReturn(true).when(mSelectionDelegate).isItemSelected(mFolderId2);
        mModelList.removeAt(mMediator.getPositionForBookmark(mFolderId2));

        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor.getValue().bookmarkNodeChanged(mFolderItem2);

        verify(mSelectionDelegate).toggleSelectionForItem(mFolderId2);
    }

    @Test
    public void testSelection_multiple() {
        finishLoading();

        mMediator.openFolder(mFolderId1);
        mMediator.toggleSelectionForRow(mFolderId2);

        verify(mSelectionDelegate).addObserver(mSelectionObserver.capture());
        mSelectionObserver.getValue().onSelectionStateChange(Arrays.asList(mFolderId2));
        doReturn(true).when(mSelectionDelegate).isSelectionEnabled();

        mMediator.bookmarkRowClicked(mFolderId3);

        verify(mSelectionDelegate).toggleSelectionForItem(mFolderId2);
        verify(mSelectionDelegate).toggleSelectionForItem(mFolderId3);
    }

    @Test
    public void testChangeSelectionMode() {
        finishLoading();

        mMediator.openFolder(mFolderId1);
        mMediator.changeSelectionMode(true);

        assertTrue(mModelList.get(1).model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertTrue(mModelList.get(2).model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));

        mModelList.get(1).model.set(ImprovedBookmarkRowProperties.SELECTED, true);
        mMediator.changeSelectionMode(false);

        assertFalse(mModelList.get(1).model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
        assertFalse(mModelList.get(2).model.get(ImprovedBookmarkRowProperties.SELECTED));
        assertFalse(mModelList.get(2).model.get(ImprovedBookmarkRowProperties.SELECTION_ACTIVE));
    }

    @Test
    public void testClearFocusOnScroll() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        reset(mHideKeyboardRunnable);

        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);
        verify(mOnScrollListenerConsumer).accept(mOnScrollListenerCaptor.capture());
        OnScrollListener onScrollListener = mOnScrollListenerCaptor.getValue();

        PropertyModel searchBoxRowPropertyModel = mModelList.get(0).model;
        searchBoxRowPropertyModel
                .get(BookmarkSearchBoxRowProperties.FOCUS_CHANGE_CALLBACK)
                .onResult(true);
        assertTrue(searchBoxRowPropertyModel.get(BookmarkSearchBoxRowProperties.HAS_FOCUS));

        onScrollListener.onScrolled(mRecyclerView, 0, -1);
        verifyNoInteractions(mHideKeyboardRunnable);
        assertTrue(searchBoxRowPropertyModel.get(BookmarkSearchBoxRowProperties.HAS_FOCUS));

        onScrollListener.onScrolled(mRecyclerView, 0, 1);
        assertFalse(searchBoxRowPropertyModel.get(BookmarkSearchBoxRowProperties.HAS_FOCUS));
        verify(mHideKeyboardRunnable).run();
    }

    @Test
    public void testHideKeyboardOnLostSearchFocus() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        reset(mHideKeyboardRunnable);

        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);
        PropertyModel searchBoxRowPropertyModel = mModelList.get(0).model;

        searchBoxRowPropertyModel
                .get(BookmarkSearchBoxRowProperties.FOCUS_CHANGE_CALLBACK)
                .onResult(true);
        assertTrue(searchBoxRowPropertyModel.get(BookmarkSearchBoxRowProperties.HAS_FOCUS));
        verifyNoInteractions(mHideKeyboardRunnable);

        searchBoxRowPropertyModel
                .get(BookmarkSearchBoxRowProperties.FOCUS_CHANGE_CALLBACK)
                .onResult(false);
        assertFalse(searchBoxRowPropertyModel.get(BookmarkSearchBoxRowProperties.HAS_FOCUS));
        verify(mHideKeyboardRunnable).run();
    }

    @Test
    public void testChangeSelectionIndexWhenNoBookmarksPresent() {
        finishLoading();

        mMediator.openFolder(mFolderId3);
        mMediator.changeSelectionMode(true);

        assertEquals(2, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);
    }

    @Test
    public void testClearSearchTextRunnable() {
        when(mBookmarkModel.searchBookmarks(anyString(), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId1));

        finishLoading();
        mMediator.openFolder(mFolderId1);
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel propertyModel = mModelList.get(0).model;
        Callback<String> searchTextCallback =
                propertyModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK);
        assertNotNull(searchTextCallback);
        Runnable clearSearchTextRunnable =
                propertyModel.get(BookmarkSearchBoxRowProperties.CLEAR_SEARCH_TEXT_RUNNABLE);
        assertNotNull(clearSearchTextRunnable);

        String searchText = "foo";
        searchTextCallback.onResult(searchText);
        assertEquals(searchText, propertyModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT));
        assertTrue(
                propertyModel.get(
                        BookmarkSearchBoxRowProperties.CLEAR_SEARCH_TEXT_BUTTON_VISIBILITY));
        verify(mBookmarkModel, times(1)).searchBookmarks(anyString(), anyInt());
        verifyCurrentBookmarkIds(null, mFolderId1);

        clearSearchTextRunnable.run();
        assertEquals("", propertyModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT));
        assertFalse(
                propertyModel.get(
                        BookmarkSearchBoxRowProperties.CLEAR_SEARCH_TEXT_BUTTON_VISIBILITY));
        // It shouldn't search again.
        verify(mBookmarkModel, times(1)).searchBookmarks(anyString(), anyInt());
        assertBookmarkListEmpty();
    }

    @Test
    public void testBackNavigationDoesNotRestoreSearch() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        assertEquals(BookmarkUiMode.FOLDER, mMediator.getCurrentUiMode());
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);

        Callback<String> searchTextChangeCallback =
                mModelList
                        .get(0)
                        .model
                        .get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK);
        searchTextChangeCallback.onResult("foo");
        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());

        mMediator.openFolder(mFolderId2);
        assertEquals(BookmarkUiMode.FOLDER, mMediator.getCurrentUiMode());
        verifyCurrentBookmarkIds(null, mBookmarkId21);

        assertTrue(mMediator.onBackPressed());
        // Should have gone back to mFolderId1.
        assertEquals(BookmarkUiMode.FOLDER, mMediator.getCurrentUiMode());
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);
    }

    @Test
    public void testSearchBox_priceTrackingFilterVisible() {
        finishLoading();

        mMediator.openFolder(mFolderId3);

        assertBookmarkListEmpty();
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel model = mModelList.get(0).model;
        assertTrue(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
    }

    @Test
    public void testSearchBox_priceTrackingFilterClicked() {
        finishLoading();

        try (HistogramWatcher ignored =
                HistogramWatcher.newSingleRecordWatcher(
                        "Bookmarks.MobileBookmarkManager.FilterShown",
                        BookmarkManagerFilter.SHOPPING)) {
            mMediator.openFolder(mMobileFolderId);
        }

        assertEquals(2, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel model = mModelList.get(0).model;
        assertTrue(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
        try (HistogramWatcher ignored =
                HistogramWatcher.newSingleRecordWatcher(
                        "Bookmarks.MobileBookmarkManager.FilterUsed2",
                        BookmarkManagerFilter.SHOPPING)) {
            model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_TOGGLE_CALLBACK).onResult(true);
        }

        // The price-tracked bookmark item should still be there.
        assertEquals(2, mModelList.size());
        // The item shouldn't be reorderable because there's a power filter active.
        assertFalse(
                mMediator.isReorderable(
                        mModelList
                                .get(1)
                                .model
                                .get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY)));

        model = mModelList.get(0).model;
        try (HistogramWatcher ignored =
                HistogramWatcher.newSingleRecordWatcher(
                        "Bookmarks.MobileBookmarkManager.FilterUsed2",
                        BookmarkManagerFilter.SHOPPING)) {
            model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_TOGGLE_CALLBACK).onResult(false);
        }
        // Going back should still show the one bookmark.
        assertEquals(2, mModelList.size());
    }

    @Test
    public void testSearchBox_searchBoxFocused() {
        finishLoading();

        mMediator.openFolder(mMobileFolderId);

        assertEquals(2, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel searchBoxModel = mModelList.get(0).model;
        assertTrue(searchBoxModel.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));

        // Focusing the searchbox will start a search which will clear out existing bookmarks.
        when(mBookmarkModel.searchBookmarks(anyString(), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId1));
        searchBoxModel.get(BookmarkSearchBoxRowProperties.FOCUS_CHANGE_CALLBACK).onResult(true);

        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        assertBookmarkListEmpty();

        verify(mBookmarkModel, never()).searchBookmarks(anyString(), anyInt());

        // Ending the search will put us back to the state prior to searching.
        mMediator.onEndSearch();
        assertEquals(2, mModelList.size());
    }

    @Test
    public void testSearchBox_priceTrackingFilterClicked_noResults() {
        finishLoading();

        mMediator.openFolder(mFolderId1);

        assertEquals(3, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel model = mModelList.get(0).model;
        assertTrue(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
        model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_TOGGLE_CALLBACK).onResult(true);

        assertBookmarkListEmpty();
    }

    @Test
    public void testSearchBox_priceTrackingFilterGoneInReadingList() {
        finishLoading();

        mMediator.openFolder(mReadingListFolderId);

        assertEquals(4, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel model = mModelList.get(0).model;
        assertFalse(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
    }

    @Test
    public void testSearchBox_priceTrackingFilterGoneWithoutBookmarks() {
        finishLoading();

        mMediator.openFolder(mFolderId3);

        assertBookmarkListEmpty();
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel model = mModelList.get(0).model;
        assertTrue(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));

        // The price-tracked filter shouldn't be visible without any price-tracked bookmarks.
        doCallback(
                        0,
                        (Callback<List<BookmarkId>> callback) -> {
                            callback.onResult(Arrays.asList());
                        })
                .when(mShoppingService)
                .getAllPriceTrackedBookmarks(any());

        mMediator.updateShoppingFilterVisible();
        assertBookmarkListEmpty();
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        model = mModelList.get(0).model;
        assertFalse(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
    }

    @Test
    public void testSearchBox_priceTrackingFilterGoneWithEligibility() {
        ShoppingFeatures.setShoppingListEligibleForTesting(false);
        finishLoading();

        mMediator.openFolder(mFolderId3);

        assertBookmarkListEmpty();
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel model = mModelList.get(0).model;
        assertFalse(model.get(BookmarkSearchBoxRowProperties.SHOPPING_CHIP_VISIBILITY));
    }

    @Test
    public void testSearchWithWhitespace() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);

        PropertyModel propertyModel = mModelList.get(0).model;
        Callback<String> searchTextCallback =
                propertyModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK);
        assertNotNull(searchTextCallback);

        String queryWithWhitespace = " foo ";
        searchTextCallback.onResult(queryWithWhitespace);
        // Model queries should be trimmed, but the View property should still have whitespace.
        assertEquals(
                queryWithWhitespace, propertyModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT));
        verify(mBookmarkModel).searchBookmarks(eq("foo"), anyInt());
    }

    @Test
    public void testModelChangeDuringSearch() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        PropertyModel searchBoxModel = mModelList.get(0).model;

        when(mBookmarkModel.searchBookmarks(anyString(), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId1));
        searchBoxModel
                .get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK)
                .onResult("test");
        searchBoxModel.get(BookmarkSearchBoxRowProperties.FOCUS_CHANGE_CALLBACK).onResult(true);

        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        verifyCurrentBookmarkIds(null, mFolderId1);

        when(mBookmarkModel.searchBookmarks(anyString(), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId2));
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor.getValue().bookmarkModelChanged();

        // Should still be in search mode, and should have refreshed and picked up new results.
        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        verifyCurrentBookmarkIds(null, mFolderId2);
    }

    @Test
    public void testModelChangeDuringSearch_emptySearchQuery() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        PropertyModel searchBoxModel = mModelList.get(0).model;

        when(mBookmarkModel.searchBookmarks(anyString(), anyInt()))
                .thenReturn(Collections.singletonList(mFolderId1));
        searchBoxModel.get(BookmarkSearchBoxRowProperties.SEARCH_TEXT_CHANGE_CALLBACK).onResult("");
        searchBoxModel.get(BookmarkSearchBoxRowProperties.FOCUS_CHANGE_CALLBACK).onResult(true);

        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        assertBookmarkListEmpty();

        verify(mBookmarkModel, never()).searchBookmarks(anyString(), anyInt());
        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        mBookmarkModelObserverArgumentCaptor.getValue().bookmarkModelChanged();

        // Should still be in search mode, and should have refreshed and picked up new results.
        assertEquals(BookmarkUiMode.SEARCHING, mMediator.getCurrentUiMode());
        assertBookmarkListEmpty();
    }

    @Test
    public void testModelChangesDeduped() {
        // Remove test impl from setUp, to resume paused behavior.
        ShadowPostTask.reset();

        finishLoading();
        mMediator.openFolder(mFolderId1);
        verify(mBookmarkModel, times(1)).getChildIds(mFolderId1);
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);

        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        BookmarkModelObserver observer = mBookmarkModelObserverArgumentCaptor.getValue();
        doReturn(Arrays.asList(mFolderId2)).when(mBookmarkModel).getChildIds(mFolderId1);

        // All of the event spam should result in a single async #setBookmarks, and no more.
        for (int i = 0; i < 3; i++) {
            observer.bookmarkModelChanged();
            observer.bookmarkNodeChildrenReordered(mFolderItem1);
            observer.bookmarkNodeChanged(mFolderItem1);
            observer.bookmarkNodeRemoved(mFolderItem1, 1, mFolderItem3, false);
        }
        ShadowLooper.idleMainLooper();

        // Measure number of #setBookmarks by counting #getChildIds.
        verify(mBookmarkModel, times(2)).getChildIds(mFolderId1);
        verifyCurrentBookmarkIds(null, mFolderId2);
    }

    @Test
    public void onPreferenceChanged_sortOrderChanged_readsAccessibility() {
        AccessibilityState.setIsTouchExplorationEnabledForTesting(true);

        mMediator.onBookmarkModelLoaded();
        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowSortOrder(BookmarkRowSortOrder.ALPHABETICAL);
        verify(mRecyclerView).announceForAccessibility("Sorting from A to Z");
    }

    @Test
    public void onPreferenceChanged_viewPreferenceUpdated_readsAccessibility() {
        AccessibilityState.setIsTouchExplorationEnabledForTesting(true);

        mMediator.onBookmarkModelLoaded();
        mMediator.openFolder(mFolderId1);
        mBookmarkUiPrefs.setBookmarkRowDisplayPref(BookmarkRowDisplayPref.VISUAL);
        verify(mRecyclerView).announceForAccessibility("Showing visual view");
    }

    @Test
    public void testDestroyDuringPendingRefresh() {
        // Remove test impl from setUp, to resume paused behavior.
        ShadowPostTask.reset();

        finishLoading();
        mMediator.openFolder(mFolderId1);
        verify(mBookmarkModel, times(1)).getChildIds(mFolderId1);
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);

        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        BookmarkModelObserver observer = mBookmarkModelObserverArgumentCaptor.getValue();
        doReturn(Arrays.asList(mFolderId2)).when(mBookmarkModel).getChildIds(mFolderId1);
        observer.bookmarkModelChanged();

        // Hasn't updated for changes yet, the refresh is pending.
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);

        mMediator.onDestroy();
        // Now give the pending task time to run. It should no-op, and not crash.
        ShadowLooper.idleMainLooper();
    }

    @Test
    public void testClickDeletedBookmark() {
        finishLoading();
        mMediator.openFolder(mFolderId1);
        verify(mBookmarkModel, times(1)).getChildIds(mFolderId1);
        verifyCurrentBookmarkIds(null, mFolderId2, mFolderId3);

        // Hang onto the on click handlers. After removing views it's not normal for users to be
        // able to click on rows. But there's asynchronicity, especially between model changes and
        // the views. While this test case is a bit more extreme than in practice, our logic still
        // need to be robust.
        Runnable onClick2 =
                mModelList.get(1).model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER);
        Runnable onClick3 =
                mModelList.get(2).model.get(ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER);

        // Folder 3 is deleted, but folder 2 is still in the model.
        when(mBookmarkModel.getBookmarkById(mFolderId3)).thenReturn(null);
        // Neither children are present in folder 1 anymore.
        when(mBookmarkModel.getChildIds(mFolderId1)).thenReturn(Collections.emptyList());

        verify(mBookmarkModel).addObserver(mBookmarkModelObserverArgumentCaptor.capture());
        BookmarkModelObserver observer = mBookmarkModelObserverArgumentCaptor.getValue();
        observer.bookmarkModelChanged();
        assertBookmarkListEmpty();

        // Neither of these can do anything, the models are gone. But more importantly, they should
        // not crash.
        when(mSelectionDelegate.isSelectionEnabled()).thenReturn(true);
        onClick2.run();
        onClick3.run();

        when(mSelectionDelegate.isSelectionEnabled()).thenReturn(false);
        // Handling here is a bit arbitrary. So rare we don't need to support it, but we can and the
        // code does currently open the folder correctly.
        onClick2.run();
        verifyCurrentBookmarkIds(null, mBookmarkId21);
        verify(mBookmarkModel).getChildIds(mFolderId2);

        // This should no-op as the folder is gone.
        onClick3.run();
        verify(mBookmarkModel, never()).getChildIds(mFolderId3);
    }

    private void verifyMenuListItemTitles(ModelList modelList, int... expectedTitleIds) {
        assertEquals(expectedTitleIds.length, modelList.size());
        for (int i = 0; i < expectedTitleIds.length; ++i) {
            int expected = expectedTitleIds[i];
            int actual = modelList.get(i).model.get(ListMenuItemProperties.TITLE_ID);
            assertEquals(
                    String.format(
                            "Title ids did not match at index %d. Expected \"%s\" but got \"%s\"",
                            i, mActivity.getString(expected), mActivity.getString(actual)),
                    expected,
                    actual);
        }
    }

    private void assertBookmarkListEmpty() {
        assertEquals(2, mModelList.size());
        assertEquals(ViewType.SEARCH_BOX, mModelList.get(0).type);
        assertEquals(ViewType.EMPTY_STATE, mModelList.get(1).type);
    }
}