chromium/chrome/browser/tab_resumption/junit/src/org/chromium/chrome/browser/tab_resumption/MixedTabResumptionDataProviderUnitTest.java

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.tab_resumption;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
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.robolectric.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_resumption.TabResumptionDataProvider.ResultStrength;
import org.chromium.chrome.browser.tab_resumption.TabResumptionDataProvider.SuggestionsResult;

import java.util.Arrays;

@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class MixedTabResumptionDataProviderUnitTest extends TestSupport {
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock private LocalTabTabResumptionDataProvider mLocalTabProvider;
    @Mock private SyncDerivedTabResumptionDataProvider mSyncDerivedProvider;

    @Captor private ArgumentCaptor<Callback<SuggestionsResult>> mLocalTabCallbackCaptor;
    @Captor private ArgumentCaptor<Callback<SuggestionsResult>> mSyncDerivedCallbackCaptor;
    Callback<SuggestionsResult> mLocalTabCallback;
    Callback<SuggestionsResult> mSyncDerivedCallback;

    private MixedTabResumptionDataProvider mMixedProvider;
    private SuggestionsResult mResults;
    private int mLocalTabFetchCount;
    private int mSyncDerivedFetchCount;
    private int mFetchSuggestionsCallbackCounter;

    @Test
    @SmallTest
    public void testLocalTabOnlyEmpty() {
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, null, /* disableBlend= */ false);

        // Empty Local Tab result, which is necessarily FORCED_NULL.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ false);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.FORCED_NULL, mResults.strength);
        assertNoSuggestions();
    }

    @Test
    @SmallTest
    public void testLocalTabOnlySingle() {
        Tab tab = makeMockBrowserTab();
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, null, /* disableBlend= */ false);

        // Non-empty Local Tab result, which is STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ false);
        mLocalTabCallback.onResult(makeLocalTabSuggestionResult(ResultStrength.STABLE, tab));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertOneSuggestionWithTitle("Blue 1");

        // Local Tab result becomes empty. This can happen if user closes the suggested tab.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ false);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.FORCED_NULL, mResults.strength);
        assertNoSuggestions();
    }

    @Test
    @SmallTest
    public void testSyncDerivedOnlyEmpty() {
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        null, mSyncDerivedProvider, /* disableBlend= */ false);

        // Empty Sync Derived result, initilaly TENTATIVE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ false, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.TENTATIVE, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertNoSuggestions();

        // Empty Sync Derived result becomes STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ false, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.STABLE, null));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertNoSuggestions();

        // Sync Derived result becomes FORCED_NULL. This can happen if user disables sync.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ false, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(3, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.FORCED_NULL, mResults.strength);
        assertNoSuggestions();
    }

    @Test
    @SmallTest
    public void testSyncDerivedOnlyVarious() {
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        null, mSyncDerivedProvider, /* disableBlend= */ false);

        // Start from non-empty TENTATIVE Sync Derived result.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ false, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.TENTATIVE, 0));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertOneSuggestionWithTitle("Google Dog");

        // Different non-empty Sync Derived result, now STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ false, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.STABLE, 1, 0));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertTwoSuggestionsWithTitles("Google Cat", "Google Dog");

        // Sync Derived result becomes FORCED_NULL, e.g., if user disables sync.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ false, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(3, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.FORCED_NULL, mResults.strength);
        assertNoSuggestions();
    }

    @Test
    @SmallTest
    public void testMixedEmpty() {
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, mSyncDerivedProvider, /* disableBlend= */ false);

        // Empty results, with Sync Derived result TENTATIVE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        // Callback passed to MixedTabResumptionDataProvider.fetchSuggestions() only gets called
        // after the callback of both sub-providers' fetchSuggestions() are called.
        Assert.assertEquals(0, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.TENTATIVE, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertNoSuggestions();

        // Empty Sync Derived result now STABLE, swap order for variety,
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.STABLE, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertNoSuggestions();

        // Everything is empty and FORCED_NULL.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(3, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.FORCED_NULL, mResults.strength);
        assertNoSuggestions();
    }

    @Test
    @SmallTest
    public void testMixedSingleSyncDerived() {
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, mSyncDerivedProvider, /* disableBlend= */ false);

        // Non-empty Sync Derived result starts out TENTATIVE, no Local Tab result.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.TENTATIVE, 0));
        Assert.assertEquals(0, mFetchSuggestionsCallbackCounter);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertOneSuggestionWithTitle("Google Dog");

        // Sync Derived result changes and becomes STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.STABLE, 1));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertOneSuggestionWithTitle("Google Cat");
    }

    @Test
    @SmallTest
    public void testMixedDoubleSyncDerived() {
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, mSyncDerivedProvider, /* disableBlend= */ false);

        // Non-empty Sync Derived result starts out TENTATIVE, no Local Tab result.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(0, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.TENTATIVE, 0, 1));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertTwoSuggestionsWithTitles("Google Dog", "Google Cat");

        // Non-empty Sync Derived result becomes STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.STABLE, 1, 0));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertTwoSuggestionsWithTitles("Google Cat", "Google Dog");
    }

    @Test
    @SmallTest
    public void testMixedSingleLocalTabToSingleSyncDerived() {
        Tab tab = makeMockBrowserTab();
        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, mSyncDerivedProvider, /* disableBlend= */ false);

        // Non-empty Local Tab result starts out STABLE; empty Sync Derived result is TENTATIVE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(makeLocalTabSuggestionResult(ResultStrength.STABLE, tab));
        Assert.assertEquals(0, mFetchSuggestionsCallbackCounter);
        // Empty Sync Derived result starts out TENTATIVE (gets ignored).
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.TENTATIVE, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertOneSuggestionWithTitle("Blue 1");

        // Local Tab result becomes empty, no change in Sync Derived results.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(new SuggestionsResult(ResultStrength.TENTATIVE, null));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertNoSuggestions();

        // Sync Derived result becomes non-empty and STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.STABLE, 0));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(3, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertOneSuggestionWithTitle("Google Dog");
    }

    @Test
    @SmallTest
    public void testMixedDoubleLocalTabAndSyncDerived() {
        Tab tab = makeMockBrowserTab();
        SuggestionsResult stableSyncDerivedResult =
                makeSuggestionResult(ResultStrength.STABLE, 1, 0);

        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, mSyncDerivedProvider, /* disableBlend= */ false);

        // Non-empty Sync Derived result starts out TENTATIVE, no Local Tab result.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.TENTATIVE, 0, 1));
        Assert.assertEquals(0, mFetchSuggestionsCallbackCounter);
        mLocalTabCallback.onResult(makeLocalTabSuggestionResult(ResultStrength.STABLE, tab));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        // Local Tab results always show up first.
        assertTwoSuggestionsWithTitles("Blue 1", "Google Dog");

        // Non-empty Sync Derived result change and becomes STABLE.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(makeLocalTabSuggestionResult(ResultStrength.STABLE, tab));
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(stableSyncDerivedResult);
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        // Local Tab results always show up first.
        assertTwoSuggestionsWithTitles("Blue 1", "Google Cat");

        // Local Tab result becomes empty; no change to Sync Derived result.
        startFetchAndCaptureCallbacks(
                /* expectLocalTabUsage= */ true, /* expectSyncDerivedUsage= */ true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.FORCED_NULL, null));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        mSyncDerivedCallback.onResult(stableSyncDerivedResult);
        Assert.assertEquals(3, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        // Now Sync Derived results take up both tiles.
        assertTwoSuggestionsWithTitles("Google Cat", "Google Dog");
    }

    @Test
    @SmallTest
    public void testDisableBlend() {
        Tab tab = makeMockBrowserTab();

        mMixedProvider =
                new MixedTabResumptionDataProvider(
                        mLocalTabProvider, mSyncDerivedProvider, /* disableBlend= */ true);

        // Not using startFetchAndCaptureCallbacks() since events are now more granular.

        // Non-empty Local Tab result. Sync Derived won't have fetch request.
        startFetch();
        maybeCaptureLocalTabCallback(true);
        mLocalTabCallback.onResult(makeLocalTabSuggestionResult(ResultStrength.TENTATIVE, tab));
        maybeCaptureSyncDerivedCallback(false); // Sync Derived Provider elided.
        Assert.assertNull(mSyncDerivedCallback);
        Assert.assertEquals(1, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        // Only Local Tab results are shown, even though Sync Derived results are available.
        assertOneSuggestionWithTitle("Blue 1");

        // Local Tab result becomes empty. Now Sync Derived results will be requested and used.
        startFetch();
        maybeCaptureLocalTabCallback(true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.STABLE, null));
        maybeCaptureSyncDerivedCallback(true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.TENTATIVE, 0, 1));
        Assert.assertEquals(2, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.TENTATIVE, mResults.strength);
        assertTwoSuggestionsWithTitles("Google Dog", "Google Cat");

        // Sync Derived results become STABLE.
        startFetch();
        maybeCaptureLocalTabCallback(true);
        mLocalTabCallback.onResult(new SuggestionsResult(ResultStrength.STABLE, null));
        maybeCaptureSyncDerivedCallback(true);
        mSyncDerivedCallback.onResult(makeSuggestionResult(ResultStrength.STABLE, 1));
        Assert.assertEquals(3, mFetchSuggestionsCallbackCounter);
        Assert.assertEquals(ResultStrength.STABLE, mResults.strength);
        assertOneSuggestionWithTitle("Google Cat");
    }

    /** Initiate Mixed Provider fetch. Still need to capture and call callbacks to continue. */
    private void startFetch() {
        mMixedProvider.fetchSuggestions(
                (SuggestionsResult results) -> {
                    mResults = results;
                    ++mFetchSuggestionsCallbackCounter;
                });
    }

    /**
     * Checks Local Tab Provider usage. If expected, capture callback as `mLocalTabCallback` to
     * inject suggestions. Else nullifies the variable.
     *
     * @param expectUsage Whether to expect Local Tab Provider usage.
     */
    private void maybeCaptureLocalTabCallback(boolean expectUsage) {
        if (expectUsage) {
            ++mLocalTabFetchCount;
            verify(mLocalTabProvider, times(mLocalTabFetchCount))
                    .fetchSuggestions(mLocalTabCallbackCaptor.capture());
            mLocalTabCallback = mLocalTabCallbackCaptor.getValue();
            Assert.assertNotNull(mLocalTabCallback);
        } else {
            verify(mLocalTabProvider, never()).fetchSuggestions(mLocalTabCallbackCaptor.capture());
            mLocalTabCallback = null;
        }
    }

    /**
     * Checks Sync Derived Provider usage. If expected, capture callback as `mSyncDerivedCallback`
     * to inject suggestions. Else nullifies the variable.
     *
     * @param expectUsage Whether to expect Sync Derived Provider usage.
     */
    private void maybeCaptureSyncDerivedCallback(boolean expectUsage) {
        if (expectUsage) {
            ++mSyncDerivedFetchCount;
            verify(mSyncDerivedProvider, times(mSyncDerivedFetchCount))
                    .fetchSuggestions(mSyncDerivedCallbackCaptor.capture());
            mSyncDerivedCallback = mSyncDerivedCallbackCaptor.getValue();
            Assert.assertNotNull(mSyncDerivedCallback);
        } else {
            verify(mSyncDerivedProvider, never())
                    .fetchSuggestions(mSyncDerivedCallbackCaptor.capture());
            mSyncDerivedCallback = null;
        }
    }

    /**
     * @param expectLocalTabUsage Whether to expect Local Tab Provider usage.
     * @param expectSyncDerivedUsage Whether to expect Sync Derived Provider usage.
     */
    private void startFetchAndCaptureCallbacks(
            boolean expectLocalTabUsage, boolean expectSyncDerivedUsage) {
        startFetch();
        maybeCaptureLocalTabCallback(expectLocalTabUsage);
        maybeCaptureSyncDerivedCallback(expectSyncDerivedUsage);
    }

    /** Helper to make a Local Tab suggestion result. */
    private SuggestionsResult makeLocalTabSuggestionResult(@ResultStrength int strength, Tab tab) {
        return new SuggestionsResult(
                strength, Arrays.asList(SuggestionEntry.createFromLocalTab(tab)));
    }

    /** Helper to make a Sync Derived suggestion result with 1 suggestion. */
    private SuggestionsResult makeSuggestionResult(@ResultStrength int strength, int index1) {
        return new SuggestionsResult(strength, Arrays.asList(makeSyncDerivedSuggestion(index1)));
    }

    /** Helper to make a Sync Derived suggestion result with 2 suggestions. */
    private SuggestionsResult makeSuggestionResult(
            @ResultStrength int strength, int index1, int index2) {
        return new SuggestionsResult(
                strength,
                Arrays.asList(
                        makeSyncDerivedSuggestion(index1), makeSyncDerivedSuggestion(index2)));
    }

    /** Helper to assert that suggestion results are empty. */
    private void assertNoSuggestions() {
        Assert.assertNull(mResults.suggestions);
    }

    /**
     * Helper to assert that there is at least one suggestion result. Using `title` as a quick way
     * to identify the suggestion. Not bothering to check other fields, e.g., `url`.
     */
    private void assertOneSuggestionWithTitle(String title1) {
        Assert.assertTrue(mResults.suggestions.size() >= 1);
        Assert.assertEquals(title1, mResults.suggestions.get(0).title);
    }

    /**
     * Helper to assert that there are at least two suggestion results. Using `title` as a quick way
     * to identify the suggestions. Not bothering to check other fields, e.g., `url`.
     */
    private void assertTwoSuggestionsWithTitles(String title1, String title2) {
        Assert.assertTrue(mResults.suggestions.size() >= 2);
        Assert.assertEquals(title1, mResults.suggestions.get(0).title);
        Assert.assertEquals(title2, mResults.suggestions.get(1).title);
    }
}