chromium/chrome/android/javatests/src/org/chromium/chrome/browser/firstrun/FirstRunIntegrationTest.java

// Copyright 2015 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.firstrun;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.when;

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

import android.app.Activity;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityMonitor;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;

import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
import org.chromium.base.Promise;
import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Features;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.ScalableTimeout;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.DeferredStartupHandler;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabsIntentTestUtils;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.enterprise.util.EnterpriseInfo;
import org.chromium.chrome.browser.enterprise.util.FakeEnterpriseInfo;
import org.chromium.chrome.browser.firstrun.FirstRunActivityTestObserver.ScopedObserverData;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.locale.LocaleManagerDelegate;
import org.chromium.chrome.browser.partnercustomizations.BasePartnerBrowserCustomizationIntegrationTestRule;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.search_engines.DefaultSearchEngineDialogHelperUtils;
import org.chromium.chrome.browser.search_engines.SearchEnginePromoType;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.signin.SigninFirstRunFragment;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.externalauth.ExternalAuthUtils;
import org.chromium.components.policy.AbstractAppRestrictionsProvider;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.signin.AccountManagerFacadeImpl;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.test.util.FakeAccountManagerDelegate;
import org.chromium.content_public.common.ContentUrlConstants;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/** Integration test suite for the first run experience. */
@RunWith(ChromeJUnit4ClassRunner.class)
@DoNotBatch(reason = "This test interacts with startup, native initialization, and first run.")
public class FirstRunIntegrationTest {
    private static final String TEST_URL = "https://test.com";
    private static final String FOO_URL = "https://foo.com";
    private static final long ACTIVITY_WAIT_LONG_MS = TimeUnit.SECONDS.toMillis(20);
    private static final String TEST_ENROLLMENT_TOKEN = "enrollment-token";

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Rule
    public BasePartnerBrowserCustomizationIntegrationTestRule mCustomizationRule =
            new BasePartnerBrowserCustomizationIntegrationTestRule();

    @Mock private ExternalAuthUtils mExternalAuthUtilsMock;
    @Mock public FirstRunAppRestrictionInfo mMockAppRestrictionInfo;
    @Mock private AccountManagerFacade mAccountManagerFacade;

    private Promise<List<CoreAccountInfo>> mAccountsPromise;

    private final Set<Class> mSupportedActivities =
            CollectionUtil.newHashSet(
                    ChromeLauncherActivity.class,
                    FirstRunActivity.class,
                    ChromeTabbedActivity.class,
                    CustomTabActivity.class);
    private final Map<Class, ActivityMonitor> mMonitorMap = new HashMap<>();
    private Instrumentation mInstrumentation;
    private Context mContext;

    private FirstRunActivityTestObserver mTestObserver = new FirstRunActivityTestObserver();
    private Activity mLastActivity;

    @Before
    public void setUp() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    AccountManagerFacadeProvider.setInstanceForTests(
                            new AccountManagerFacadeImpl(new FakeAccountManagerDelegate()));
                });
        MockitoAnnotations.initMocks(this);
        when(mExternalAuthUtilsMock.canUseGooglePlayServices()).thenReturn(false);
        ExternalAuthUtils.setInstanceForTesting(mExternalAuthUtilsMock);
        FirstRunStatus.setFirstRunSkippedByPolicy(false);
        FirstRunUtils.setDisableDelayOnExitFreForTest(true);
        FirstRunActivity.setObserverForTest(mTestObserver);

        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mContext = mInstrumentation.getTargetContext();
        for (Class clazz : mSupportedActivities) {
            ActivityMonitor monitor = new ActivityMonitor(clazz.getName(), null, false);
            mMonitorMap.put(clazz, monitor);
            mInstrumentation.addMonitor(monitor);
        }
    }

    @After
    public void tearDown() {
        // Tear down the last activity first, otherwise the other cleanup, in particular skipped by
        // policy pref, might trigger an assert in activity initialization because of the statics
        // we reset below. Run it on UI so there are no threading issues.
        if (mLastActivity != null) {
            ThreadUtils.runOnUiThreadBlocking(() -> mLastActivity.finish());
        }
        // Finish the rest of the running activities.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    for (Activity runningActivity : ApplicationStatus.getRunningActivities()) {
                        runningActivity.finish();
                    }
                });

        FirstRunStatus.setFirstRunSkippedByPolicy(false);
        AccountManagerFacadeProvider.resetInstanceForTests();
    }

    private ActivityMonitor getMonitor(Class activityClass) {
        Assert.assertTrue(mSupportedActivities.contains(activityClass));
        return mMonitorMap.get(activityClass);
    }

    private FirstRunActivity launchFirstRunActivity() {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(TEST_URL));
        intent.setPackage(mContext.getPackageName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);

        // Because the AsyncInitializationActivity notices that the FRE hasn't been run yet, it
        // redirects to it.  Once the user closes the FRE, the user should be kicked back into the
        // startup flow where they were interrupted.
        return waitForFirstRunActivity();
    }

    private <T extends Activity> T waitForActivity(Class<T> activityClass) {
        Assert.assertTrue(mSupportedActivities.contains(activityClass));
        ActivityMonitor monitor = getMonitor(activityClass);
        mLastActivity = mInstrumentation.waitForMonitorWithTimeout(monitor, ACTIVITY_WAIT_LONG_MS);
        Assert.assertNotNull("Could not find " + activityClass.getName(), mLastActivity);
        return (T) mLastActivity;
    }

    private void setHasAppRestrictionForMock() {
        doCallback((Callback<Boolean> callback) -> callback.onResult(true))
                .when(mMockAppRestrictionInfo)
                .getHasAppRestriction(any());
        FirstRunAppRestrictionInfo.setInitializedInstanceForTest(mMockAppRestrictionInfo);
    }

    private void skipTosDialogViaPolicy() {
        setHasAppRestrictionForMock();
        Bundle restrictions = new Bundle();
        AbstractAppRestrictionsProvider.setTestRestrictions(restrictions);

        FakeEnterpriseInfo fakeEnterpriseInfo = new FakeEnterpriseInfo();
        fakeEnterpriseInfo.initialize(new EnterpriseInfo.OwnedState(true, false));
        EnterpriseInfo.setInstanceForTest(fakeEnterpriseInfo);
    }

    private void enableCloudManagementViaPolicy() {
        setHasAppRestrictionForMock();
        Bundle restrictions = new Bundle();
        restrictions.putString("CloudManagementEnrollmentToken", TEST_ENROLLMENT_TOKEN);
        AbstractAppRestrictionsProvider.setTestRestrictions(restrictions);
    }

    private void launchCustomTabs(String url) {
        mContext.startActivity(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(mContext, url));
    }

    private void launchViewIntent(String url) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        intent.setPackage(mContext.getPackageName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }

    private void launchMainIntent() {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setPackage(mContext.getPackageName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }

    private void clickThroughFirstRun(
            FirstRunActivity firstRunActivity, FirstRunPagesTestCase testCase) throws Exception {
        // Start FRE.
        FirstRunNavigationHelper navigationHelper = new FirstRunNavigationHelper(firstRunActivity);
        navigationHelper.ensurePagesCreationSucceeded().continueWithoutAnAccount();

        if (testCase.searchPromoType() == SearchEnginePromoType.DONT_SHOW) {
            navigationHelper.ensureDefaultSearchEnginePromoNotCurrentPage();
        } else {
            navigationHelper.selectDefaultSearchEngine();
        }

        if (testCase.showSigninPromo()
                && !ChromeFeatureList.isEnabled(
                        ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
            navigationHelper.skipSigninPromo();
        } else {
            navigationHelper.ensureSigninPromoNotCurrentPage();
        }
    }

    private void verifyUrlEquals(String expected, Uri actual) {
        Assert.assertEquals(
                "Expected " + expected + " did not match actual " + actual,
                Uri.parse(expected),
                actual);
    }

    private FirstRunActivity waitForFirstRunActivity() {
        return (FirstRunActivity) waitForActivity(FirstRunActivity.class);
    }

    /**
     * When launching a second Chrome, the new FRE should replace the old FRE. In order to know when
     * the second FirstRunActivity is ready, use object inequality with old one.
     *
     * @param previousFreActivity The previous activity.
     */
    private FirstRunActivity waitForDifferentFirstRunActivity(
            FirstRunActivity previousFreActivity) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    for (Activity runningActivity : ApplicationStatus.getRunningActivities()) {
                        @ActivityState
                        int state = ApplicationStatus.getStateForActivity(runningActivity);
                        if (runningActivity.getClass() == FirstRunActivity.class
                                && runningActivity != previousFreActivity
                                && (state == ActivityState.STARTED
                                        || state == ActivityState.RESUMED)) {
                            mLastActivity = runningActivity;
                            return true;
                        }
                    }
                    return false;
                },
                "Did not find a different FirstRunActivity from " + previousFreActivity,
                /* maxTimeoutMs= */ ACTIVITY_WAIT_LONG_MS,
                /* checkIntervalMs= */ CriteriaHelper.DEFAULT_POLLING_INTERVAL);

        CriteriaHelper.pollInstrumentationThread(
                previousFreActivity::isFinishing,
                "The original FirstRunActivity should be finished, instead "
                        + ApplicationStatus.getStateForActivity(previousFreActivity));
        return (FirstRunActivity) mLastActivity;
    }

    private <T extends ChromeActivity> Uri waitAndGetUriFromChromeActivity(Class<T> activityClass) {
        ChromeActivity chromeActivity = waitForActivity(activityClass);
        return chromeActivity.getIntent().getData();
    }

    private ScopedObserverData getObserverData(FirstRunActivity freActivity) {
        return mTestObserver.getScopedObserverData(freActivity);
    }

    private void blockOnFlowIsKnown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertNull("mAccountsPromise is already initialized!", mAccountsPromise);
                    mAccountsPromise = new Promise<>();
                    // getCoreAccountInfos() is called by AccountTrackerService.seedAccounts();
                    // TODO(crbug.com/40228999): Remove when account manager facade initiates
                    //  seeding.
                    Mockito.when(mAccountManagerFacade.getCoreAccountInfos())
                            .thenReturn(new Promise<>());
                });
        Mockito.when(mAccountManagerFacade.getCoreAccountInfos()).thenReturn(mAccountsPromise);
        AccountManagerFacadeProvider.setInstanceForTests(mAccountManagerFacade);
    }

    private void unblockOnFlowIsKnown() {
        Mockito.verify(mAccountManagerFacade, atLeastOnce()).getCoreAccountInfos();
        ThreadUtils.runOnUiThreadBlocking(() -> mAccountsPromise.fulfill(Collections.emptyList()));
    }

    @Test
    @MediumTest
    public void startPartnerCustomizationDuringFRE() {
        launchFirstRunActivity();
        CriteriaHelper.pollInstrumentationThread(
                () -> PartnerBrowserCustomizations.getInstance().isInitialized());
    }

    @Test
    @MediumTest
    public void startPartnerCustomizationFromMainIntent() {
        launchMainIntent();
        CriteriaHelper.pollInstrumentationThread(
                () -> PartnerBrowserCustomizations.getInstance().isInitialized());
    }

    @Test
    @SmallTest
    public void testHelpPageSkipsFirstRun() {
        // Fire an Intent to load a generic URL.
        CustomTabActivity.showInfoPage(mContext, TEST_URL);

        // The original activity should be started because it's a "help page".
        waitForActivity(CustomTabActivity.class);
        Assert.assertFalse(mLastActivity.isFinishing());

        // First run should be skipped for this Activity.
        Assert.assertEquals(0, getMonitor(FirstRunActivity.class).getHits());
    }

    @Test
    @SmallTest
    public void testAbortFirstRun() throws Exception {
        launchViewIntent(TEST_URL);
        Activity chromeLauncherActivity = waitForActivity(ChromeLauncherActivity.class);

        // Because the ChromeLauncherActivity notices that the FRE hasn't been run yet, it
        // redirects to it.
        FirstRunActivity firstRunActivity = waitForFirstRunActivity();

        // Once the user closes the FRE, the user should be kicked back into the
        // startup flow where they were interrupted.
        ScopedObserverData scopedObserverData = getObserverData(firstRunActivity);
        Assert.assertEquals(0, scopedObserverData.abortFirstRunExperienceCallback.getCallCount());
        ThreadUtils.runOnUiThreadBlocking(mLastActivity::onBackPressed);
        scopedObserverData.abortFirstRunExperienceCallback.waitForCallback(
                "FirstRunActivity didn't abort", 0);

        CriteriaHelper.pollInstrumentationThread(() -> mLastActivity.isFinishing());

        // ChromeLauncherActivity should finish if FRE was aborted.
        CriteriaHelper.pollInstrumentationThread(chromeLauncherActivity::isFinishing);
    }

    // TODO(crbug.com/40785454): Add test cases for the new Welcome screen that includes the
    // Sign-in promo once the sign-in components can be disabled by policy.

    // TODO(crbug.com/40794359): Add test cases for ToS page disabled by policy after the
    // user accepted ToS and aborted first run.

    // TODO(crbug.com/346755013): Add tests that check for the history sync screen when the UNO flag
    // is
    // enabled.

    @Test
    @MediumTest
    public void testFirstRunPages_NoCctPolicy_AbsenceOfPromos() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase());
    }

    @Test
    @MediumTest
    public void testFirstRunPages_NoCctPolicy_SearchPromo() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase().withSearchPromo());
    }

    @Test
    @MediumTest
    public void testFirstRunPages_NoCctPolicy_SearchPromo_SigninPromo() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase().withSearchPromo().withSigninPromo());
    }

    @Test
    @MediumTest
    public void testFirstRunPages_NoCctPolicy_SigninPromo() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase().withSigninPromo());
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testFirstRunPages_NoCctPolicy_OnBackPressed() throws Exception {
        initializePreferences(new FirstRunPagesTestCase().withSearchPromo().withSigninPromo());

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one, go back until initial page, and
        // then complete first run.
        new FirstRunNavigationHelper(firstRunActivity)
                .ensurePagesCreationSucceeded()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine()
                .ensureSigninPromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureDefaultSearchEnginePromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureTermsOfServiceIsCurrentPage()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine()
                .skipSigninPromo();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testFirstRunPages_WithCctPolicy_OnBackPressed() throws Exception {
        initializePreferences(
                new FirstRunPagesTestCase()
                        .withCctTosDisabled()
                        .withSearchPromo()
                        .withSigninPromo());

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one, go back until initial page, and
        // then complete first run.
        new FirstRunNavigationHelper(firstRunActivity)
                .ensurePagesCreationSucceeded()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine()
                .ensureSigninPromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureDefaultSearchEnginePromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureTermsOfServiceIsCurrentPage()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine()
                .skipSigninPromo();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    public void testSigninFirstRunPages_WithCctPolicy_AbsenceOfPromos() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase().withCctTosDisabled());
    }

    @Test
    @MediumTest
    public void testSigninFirstRunPages_WithCctPolicy_SearchPromo() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase().withCctTosDisabled().withSearchPromo());
    }

    @Test
    @MediumTest
    public void testSigninFirstRunPages_WithCctPolicy_SearchPromo_SigninPromo() throws Exception {
        runFirstRunPagesTest(
                new FirstRunPagesTestCase()
                        .withCctTosDisabled()
                        .withSearchPromo()
                        .withSigninPromo());
    }

    @Test
    @MediumTest
    public void testSigninFirstRunPages_WithCctPolicy_SigninPromo() throws Exception {
        runFirstRunPagesTest(new FirstRunPagesTestCase().withCctTosDisabled().withSigninPromo());
    }

    private void runFirstRunPagesTest(FirstRunPagesTestCase testCase) throws Exception {
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();
        clickThroughFirstRun(firstRunActivity, testCase);

        // FRE should be completed now, which will kick the user back into the interrupted flow.
        // In this case, the user gets sent to the ChromeTabbedActivity after a View Intent is
        // processed by ChromeLauncherActivity.
        getObserverData(firstRunActivity)
                .updateCachedEngineCallback
                .waitForCallback("Failed to alert search widgets that an update is necessary", 0);
        waitForActivity(ChromeTabbedActivity.class);
    }

    private void initializePreferences(FirstRunPagesTestCase testCase) {
        if (testCase.cctTosDisabled()) skipTosDialogViaPolicy();

        FirstRunFlowSequencer.setDelegateFactoryForTesting(
                (profileProvider) ->
                        new TestFirstRunFlowSequencerDelegate(testCase, profileProvider));

        setUpLocaleManagerDelegate(testCase.searchPromoType());
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testFirstRunPages_ProgressHistogramRecordedOnlyOnce() throws Exception {
        HistogramWatcher histograms =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                "MobileFre.Progress.ViewIntent",
                                MobileFreProgress.STARTED,
                                MobileFreProgress.WELCOME_SHOWN,
                                MobileFreProgress.SYNC_CONSENT_SHOWN,
                                MobileFreProgress.SYNC_CONSENT_DISMISSED,
                                MobileFreProgress.DEFAULT_SEARCH_ENGINE_SHOWN)
                        .build();
        initializePreferences(new FirstRunPagesTestCase().withSearchPromo().withSigninPromo());

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one, go back until initial page, and
        // then complete first run.
        new FirstRunNavigationHelper(firstRunActivity)
                .ensurePagesCreationSucceeded()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine()
                .ensureSigninPromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureDefaultSearchEnginePromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureTermsOfServiceIsCurrentPage()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine()
                .skipSigninPromo();

        waitForActivity(ChromeTabbedActivity.class);

        histograms.assertExpected();
    }

    @Test
    @MediumTest
    public void testFirstRunPages_ProgressHistogramRecording_NoPromos() throws Exception {
        HistogramWatcher histograms =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                "MobileFre.Progress.ViewIntent",
                                MobileFreProgress.STARTED,
                                MobileFreProgress.WELCOME_SHOWN)
                        .build();

        initializePreferences(new FirstRunPagesTestCase());

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        new FirstRunNavigationHelper(firstRunActivity)
                .ensurePagesCreationSucceeded()
                .continueWithoutAnAccount();

        waitForActivity(ChromeTabbedActivity.class);

        histograms.assertExpected();
    }

    @Test
    @MediumTest
    @DisabledTest(message = "https://crbug.com/1221647")
    public void testExitFirstRunWithPolicy() throws Exception {
        initializePreferences(new FirstRunPagesTestCase().withCctTosDisabled());

        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(mContext, TEST_URL);
        mContext.startActivity(intent);

        FirstRunActivity freActivity = waitForFirstRunActivity();
        CriteriaHelper.pollUiThread(
                () -> freActivity.getSupportFragmentManager().getFragments().size() > 0);
        // Make sure native is initialized so that the subsequent transition is not blocked.
        CriteriaHelper.pollUiThread(
                (() -> freActivity.getNativeInitializationPromise().isFulfilled()),
                "native never initialized.");

        waitForActivity(CustomTabActivity.class);
        Assert.assertFalse(
                "Usage and crash reporting pref was set to true after skip",
                PrivacyPreferencesManagerImpl.getInstance().isUsageAndCrashReportingPermitted());
        Assert.assertTrue(
                "FRE should be skipped for CCT.", FirstRunStatus.isFirstRunSkippedByPolicy());
    }

    @Test
    @MediumTest
    @DisabledTest(message = "issuetracker.google.com/360931705")
    public void testFirstRunSkippedSharedPreferenceRefresh() throws Exception {
        // Set that the first run was previous skipped by policy in shared preference, then
        // refreshing shared preference should cause its value to become false, since there's no
        // policy set in this test case.
        FirstRunStatus.setFirstRunSkippedByPolicy(true);

        Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                        mContext, ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
        mContext.startActivity(intent);
        CustomTabActivity activity = waitForActivity(CustomTabActivity.class);
        CriteriaHelper.pollUiThreadLongTimeout(
                "Native init never completed", activity::didFinishNativeInitialization);

        // DeferredStartupHandler could not finish with CriteriaHelper#DEFAULT_MAX_TIME_TO_POLL.
        // Use longer timeout here to avoid flakiness. See https://crbug.com/1157611.
        CriteriaHelper.pollUiThread(activity::deferredStartupPostedForTesting);
        Assert.assertTrue(
                "Deferred startup never completed",
                DeferredStartupHandler.waitForDeferredStartupCompleteForTesting(
                        ScalableTimeout.scaleTimeout(ACTIVITY_WAIT_LONG_MS)));

        // FirstRun status should be refreshed by TosDialogBehaviorSharedPrefInvalidator in deferred
        // start up task.
        CriteriaHelper.pollUiThread(() -> !FirstRunStatus.isFirstRunSkippedByPolicy());
    }

    @Test
    @MediumTest
    public void testSkipTosPage() throws TimeoutException {
        // Test case that verifies when the ToS Page was previously accepted, launching the FRE
        // should transition to the next page.
        FirstRunStatus.setSkipWelcomePage(true);

        FirstRunActivity freActivity = launchFirstRunActivity();
        CriteriaHelper.pollUiThread(
                () -> freActivity.getSupportFragmentManager().getFragments().size() > 0);

        getObserverData(freActivity)
                .jumpToPageCallback
                .waitForCallback("Welcome page should be skipped.", 0);
    }

    @Test
    @MediumTest
    // TODO(crbug.com/40142602): Change this test case when policy can handle cases when ToS
    // is accepted in Browser App.
    public void testSkipTosPage_WithCctPolicy() throws Exception {
        skipTosDialogViaPolicy();
        FirstRunStatus.setSkipWelcomePage(true);

        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(mContext, TEST_URL);
        mContext.startActivity(intent);

        FirstRunActivity freActivity = waitForFirstRunActivity();
        CriteriaHelper.pollUiThread(
                () -> freActivity.getSupportFragmentManager().getFragments().size() > 0);

        // A page skip should happen, while we are still staying at FRE.
        getObserverData(freActivity)
                .jumpToPageCallback
                .waitForCallback("Welcome page should be skipped.", 0);
        Assert.assertFalse(
                "FRE should not be skipped for CCT.", FirstRunStatus.isFirstRunSkippedByPolicy());
        Assert.assertFalse(
                "FreActivity should still be alive.", freActivity.isActivityFinishingOrDestroyed());
    }

    @Test
    @MediumTest
    public void testFastDestroy() {
        // Inspired by crbug.com/1119548, where onDestroy() before triggerLayoutInflation() caused
        // a crash.
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(mContext, TEST_URL);
        mContext.startActivity(intent);
    }

    @Test
    @MediumTest
    public void testMultipleFresCustomIntoView() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        launchCustomTabs(TEST_URL);
        FirstRunActivity firstFreActivity = waitForFirstRunActivity();

        launchViewIntent(FOO_URL);
        FirstRunActivity secondFreActivity = waitForDifferentFirstRunActivity(firstFreActivity);

        clickThroughFirstRun(secondFreActivity, testCase);
        verifyUrlEquals(FOO_URL, waitAndGetUriFromChromeActivity(ChromeTabbedActivity.class));
    }

    @Test
    @MediumTest
    public void testMultipleFresViewIntoCustom() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        launchViewIntent(TEST_URL);
        FirstRunActivity firstFreActivity = waitForFirstRunActivity();

        launchCustomTabs(FOO_URL);
        FirstRunActivity secondFreActivity = waitForDifferentFirstRunActivity(firstFreActivity);

        clickThroughFirstRun(secondFreActivity, testCase);
        verifyUrlEquals(FOO_URL, waitAndGetUriFromChromeActivity(CustomTabActivity.class));
    }

    @Test
    @MediumTest
    public void testMultipleFresBothView() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        launchViewIntent(TEST_URL);
        FirstRunActivity firstFreActivity = waitForFirstRunActivity();

        launchViewIntent(FOO_URL);
        FirstRunActivity secondFreActivity = waitForDifferentFirstRunActivity(firstFreActivity);

        clickThroughFirstRun(secondFreActivity, testCase);
        verifyUrlEquals(FOO_URL, waitAndGetUriFromChromeActivity(ChromeTabbedActivity.class));
    }

    @Test
    @MediumTest
    public void testMultipleFresBackButton() throws Exception {
        launchViewIntent(TEST_URL);
        FirstRunActivity firstFreActivity = waitForFirstRunActivity();

        launchViewIntent(TEST_URL);
        FirstRunActivity secondFreActivity = waitForDifferentFirstRunActivity(firstFreActivity);

        ScopedObserverData secondFreData = getObserverData(secondFreActivity);
        Assert.assertEquals(
                "Second FRE should not have aborted before back button is pressed.",
                0,
                secondFreData.abortFirstRunExperienceCallback.getCallCount());

        Assert.assertTrue(
                "FirstRunActivity should intercept back press",
                secondFreActivity.getOnBackPressedDispatcher().hasEnabledCallbacks());
        ThreadUtils.runOnUiThreadBlocking(
                secondFreActivity.getOnBackPressedDispatcher()::onBackPressed);
        secondFreData.abortFirstRunExperienceCallback.waitForCallback(
                "Second FirstRunActivity didn't abort", 0);
        CriteriaHelper.pollInstrumentationThread(
                secondFreActivity::isFinishing, "Second FRE should be finishing now.");
    }

    @Test
    @MediumTest
    public void testNativeInitBeforeFragment() throws Exception {
        FirstRunPagesTestCase testCase = new FirstRunPagesTestCase();
        initializePreferences(testCase);

        // Inspired by https://crbug.com/1207683 where a notification was dropped because native
        // initialized before the first fragment was attached to the activity.
        blockOnFlowIsKnown();

        launchViewIntent(TEST_URL);
        FirstRunActivity firstRunActivity = waitForFirstRunActivity();
        CriteriaHelper.pollUiThread(
                (() -> firstRunActivity.getNativeInitializationPromise().isFulfilled()),
                "native never initialized.");

        unblockOnFlowIsKnown();
        clickThroughFirstRun(firstRunActivity, testCase);
        verifyUrlEquals(TEST_URL, waitAndGetUriFromChromeActivity(ChromeTabbedActivity.class));
    }

    @Test
    @MediumTest
    public void testSigninFirstRunPageShownBeforeChildStatusFetch() throws Exception {
        // ChildAccountStatusSupplier uses AppRestrictions to quickly detect non-supervised cases,
        // so pretend there are AppRestrictions set by FamilyLink.
        setHasAppRestrictionForMock();
        blockOnFlowIsKnown();
        initializePreferences(new FirstRunPagesTestCase());

        FirstRunActivity firstRunActivity = launchFirstRunActivity();
        new FirstRunNavigationHelper(firstRunActivity).ensureTermsOfServiceIsCurrentPage();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ProgressBar progressBar =
                            ((SigninFirstRunFragment)
                                            firstRunActivity.getCurrentFragmentForTesting())
                                    .getView()
                                    .findViewById(R.id.fre_native_and_policy_load_progress_spinner);
                    // Replace the progress bar with a placeholder to allow other checks. Currently
                    // the progress bar cannot be stopped otherwise due to some espresso issues
                    // (crbug/1115067).
                    progressBar.setIndeterminateDrawable(
                            new ColorDrawable(
                                    SemanticColorUtils.getDefaultBgColor(firstRunActivity)));
                });

        onView(withId(R.id.fre_logo)).check(matches(isDisplayed()));
        onView(withId(R.id.fre_native_and_policy_load_progress_spinner))
                .check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    public void testSigninFirstRunLoadPointHistograms() throws Exception {
        var histograms =
                HistogramWatcher.newBuilder()
                        .expectAnyRecord("MobileFre.FromLaunch.ChildStatusAvailable")
                        .expectAnyRecord("MobileFre.FromLaunch.PoliciesLoaded")
                        .build();
        initializePreferences(new FirstRunPagesTestCase());

        FirstRunActivity firstRunActivity = launchFirstRunActivity();
        new FirstRunNavigationHelper(firstRunActivity)
                .ensurePagesCreationSucceeded()
                .ensureTermsOfServiceIsCurrentPage();

        histograms.assertExpected("Child status or policies fetch time not recorded");
    }

    @Test
    @MediumTest
    public void testNativeInitBeforeFragmentSkip() throws Exception {
        FirstRunPagesTestCase testCase = new FirstRunPagesTestCase();
        initializePreferences(testCase);
        skipTosDialogViaPolicy();
        blockOnFlowIsKnown();

        launchCustomTabs(TEST_URL);
        FirstRunActivity firstRunActivity = waitForFirstRunActivity();
        CriteriaHelper.pollUiThread(
                (() -> firstRunActivity.getNativeInitializationPromise().isFulfilled()),
                "native never initialized.");

        unblockOnFlowIsKnown();
        clickThroughFirstRun(firstRunActivity, testCase);
        verifyUrlEquals(TEST_URL, waitAndGetUriFromChromeActivity(CustomTabActivity.class));
    }

    @Test
    @MediumTest
    public void testCloudManagementDoesNotBlockFirstRun() throws Exception {
        // Ensures FRE is not blocked if cloud management is enabled.
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);
        enableCloudManagementViaPolicy();

        launchViewIntent(TEST_URL);
        FirstRunActivity firstRunActivity = waitForFirstRunActivity();
        clickThroughFirstRun(firstRunActivity, testCase);
        verifyUrlEquals(TEST_URL, waitAndGetUriFromChromeActivity(ChromeTabbedActivity.class));
    }

    private void setUpLocaleManagerDelegate(@SearchEnginePromoType final int searchPromoType) {
        // Force the LocaleManager into a specific state.
        LocaleManagerDelegate mockDelegate =
                new LocaleManagerDelegate() {
                    @Override
                    public int getSearchEnginePromoShowType() {
                        return searchPromoType;
                    }

                    @Override
                    public List<TemplateUrl> getSearchEnginesForPromoDialog(int promoType) {
                        return TemplateUrlServiceFactory.getForProfile(
                                        ProfileManager.getLastUsedRegularProfile())
                                .getTemplateUrls();
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> LocaleManager.getInstance().setDelegateForTest(mockDelegate));
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testPrefsUpdated_allPagesAlreadyShown() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one.
        FirstRunNavigationHelper navigationHelper =
                new FirstRunNavigationHelper(firstRunActivity)
                        .ensurePagesCreationSucceeded()
                        .continueWithoutAnAccount()
                        .selectDefaultSearchEngine()
                        .ensureSigninPromoIsCurrentPage();

        // Change preferences to disable all promos.
        testCase.setSearchPromoType(SearchEnginePromoType.DONT_SHOW);
        testCase.setSigninPromo(false);

        // Go back should skip all the promo pages and reach the terms of service page. Accepting
        // terms of service completes first run.
        navigationHelper
                .goBackToPreviousPage()
                .ensureTermsOfServiceIsCurrentPage()
                .continueWithoutAnAccount();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    public void testPrefsUpdated_noPagesShown() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Show terms of services.
        FirstRunNavigationHelper navigationHelper =
                new FirstRunNavigationHelper(firstRunActivity)
                        .ensurePagesCreationSucceeded()
                        .ensureTermsOfServiceIsCurrentPage();

        // Change preferences before any promo page is shown.
        testCase.setSearchPromoType(SearchEnginePromoType.DONT_SHOW);
        testCase.setSigninPromo(false);

        // Accepting terms of services should complete first run, since all the promos are disabled.
        navigationHelper
                .continueWithoutAnAccount()
                .ensureDefaultSearchEnginePromoNotCurrentPage()
                .ensureSigninPromoNotCurrentPage();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testPrefsUpdated_searchEnginePromoDisableAfterPromoShown() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one.
        FirstRunNavigationHelper navigationHelper =
                new FirstRunNavigationHelper(firstRunActivity)
                        .ensurePagesCreationSucceeded()
                        .continueWithoutAnAccount()
                        .selectDefaultSearchEngine()
                        .ensureSigninPromoIsCurrentPage();

        // Disable search engine prompt after the next page is shown.
        testCase.setSearchPromoType(SearchEnginePromoType.DONT_SHOW);
        setUpLocaleManagerDelegate(SearchEnginePromoType.DONT_SHOW);

        // Go back until initial page, and then complete first run. The search engine prompt
        // shouldn't be shown again in either direction.
        navigationHelper
                .goBackToPreviousPage()
                .ensureDefaultSearchEnginePromoNotCurrentPage()
                .continueWithoutAnAccount()
                .ensureDefaultSearchEnginePromoNotCurrentPage()
                .skipSigninPromo();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testPrefsUpdated_searchEnginePromoDisableWhilePromoShown() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go over first run prompts and stop at the search engine page.
        FirstRunNavigationHelper navigationHelper =
                new FirstRunNavigationHelper(firstRunActivity)
                        .ensurePagesCreationSucceeded()
                        .continueWithoutAnAccount()
                        .ensureDefaultSearchEnginePromoIsCurrentPage();

        // Disable search engine prompt while it's shown. This will not hide the page.
        testCase.setSearchPromoType(SearchEnginePromoType.DONT_SHOW);
        setUpLocaleManagerDelegate(SearchEnginePromoType.DONT_SHOW);

        // Pass the search engine prompt, and move to the last page without skipping it.
        // Go back until initial page, and then complete first run. The search engine prompt
        // shouldn't be shown again in either direction.
        navigationHelper
                .selectDefaultSearchEngine()
                .ensureSigninPromoIsCurrentPage()
                .goBackToPreviousPage()
                .ensureDefaultSearchEnginePromoNotCurrentPage()
                .continueWithoutAnAccount()
                .ensureDefaultSearchEnginePromoNotCurrentPage()
                .skipSigninPromo();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testPrefsUpdated_signinPromoPromoDisableAfterPromoShown() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one.
        FirstRunNavigationHelper navigationHelper =
                new FirstRunNavigationHelper(firstRunActivity)
                        .ensurePagesCreationSucceeded()
                        .continueWithoutAnAccount()
                        .selectDefaultSearchEngine()
                        .ensureSigninPromoIsCurrentPage();

        // Disable sign-in prompt while it's shown. This will not hide the page.
        testCase.setSigninPromo(false);

        // Go back until initial page, and then complete first run. The sign-in prompt shouldn't be
        // shown again.
        navigationHelper
                .goBackToPreviousPage()
                .ensureDefaultSearchEnginePromoIsCurrentPage()
                .goBackToPreviousPage()
                .continueWithoutAnAccount()
                .selectDefaultSearchEngine();

        waitForActivity(ChromeTabbedActivity.class);
    }

    @Test
    @MediumTest
    // TODO(crbug.com/346755013): Add a corresponding test for the case where the flag is enabled.
    @Features.DisableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
    public void testPrefsUpdated_signinPromoPromoDisableWhilePromoShown() throws Exception {
        FirstRunPagesTestCase testCase = FirstRunPagesTestCase.createWithShowAllPromos();
        initializePreferences(testCase);

        FirstRunActivity firstRunActivity = launchFirstRunActivity();

        // Go until the last page without skipping the last one.
        FirstRunNavigationHelper navigationHelper =
                new FirstRunNavigationHelper(firstRunActivity)
                        .ensurePagesCreationSucceeded()
                        .continueWithoutAnAccount()
                        .selectDefaultSearchEngine()
                        .ensureSigninPromoIsCurrentPage();

        // Disable sign-in prompt while it's shown. This will not hide the page.
        testCase.setSearchPromoType(SearchEnginePromoType.DONT_SHOW);

        // User should be able to interact with sign-in promo page and complete first run.
        navigationHelper.ensureSigninPromoIsCurrentPage().skipSigninPromo();

        waitForActivity(ChromeTabbedActivity.class);
    }

    private void clickButton(final Activity activity, final int id, final String message) {
        CriteriaHelper.pollUiThread(
                () -> {
                    View view = activity.findViewById(id);
                    Criteria.checkThat(view, Matchers.notNullValue());
                    Criteria.checkThat(view.getVisibility(), Matchers.is(View.VISIBLE));
                    Criteria.checkThat(view.isEnabled(), Matchers.is(true));
                });

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    Button button = activity.findViewById(id);
                    Assert.assertNotNull(message, button);
                    button.performClick();
                });
    }

    /** Configuration for tests that depend on showing First Run pages. */
    static class FirstRunPagesTestCase {
        private boolean mCctTosDisabled;
        private @SearchEnginePromoType int mSearchPromoType = SearchEnginePromoType.DONT_SHOW;
        private boolean mShowSigninPromo;

        boolean cctTosDisabled() {
            return mCctTosDisabled;
        }

        @SearchEnginePromoType
        int searchPromoType() {
            return mSearchPromoType;
        }

        boolean showSearchPromo() {
            return mSearchPromoType == SearchEnginePromoType.SHOW_NEW
                    || mSearchPromoType == SearchEnginePromoType.SHOW_EXISTING;
        }

        boolean showSigninPromo() {
            return mShowSigninPromo;
        }

        FirstRunPagesTestCase setCctTosDisabled() {
            mCctTosDisabled = true;
            return this;
        }

        FirstRunPagesTestCase setSearchPromoType(@SearchEnginePromoType int searchPromoType) {
            mSearchPromoType = searchPromoType;
            return this;
        }

        FirstRunPagesTestCase setSigninPromo(boolean showSigninPromo) {
            mShowSigninPromo = showSigninPromo;
            return this;
        }

        FirstRunPagesTestCase withCctTosDisabled() {
            return setCctTosDisabled();
        }

        FirstRunPagesTestCase withSearchPromo() {
            return setSearchPromoType(SearchEnginePromoType.SHOW_EXISTING);
        }

        FirstRunPagesTestCase withSigninPromo() {
            // TODO(crbug.com/346755013): Rename this and similar methods to "withSyncPromo".
            return setSigninPromo(true);
        }

        static FirstRunPagesTestCase createWithShowAllPromos() {
            return new FirstRunPagesTestCase().withSearchPromo().withSigninPromo();
        }
    }

    /**
     * Performs basic navigation operations on First Run pages, such as checking if a given promo is
     * current shown, moving to the next page, or going back to the previous page.
     */
    class FirstRunNavigationHelper {
        private final FirstRunActivity mFirstRunActivity;
        private final ScopedObserverData mScopedObserverData;

        protected FirstRunNavigationHelper(FirstRunActivity firstRunActivity) {
            mFirstRunActivity = firstRunActivity;
            mScopedObserverData = getObserverData(mFirstRunActivity);
        }

        protected FirstRunNavigationHelper ensurePagesCreationSucceeded() throws Exception {
            mScopedObserverData.createPostNativeAndPoliciesPageSequenceCallback.waitForCallback(
                    "Failed to finalize the flow and create subsequent pages", 0);
            Assert.assertEquals(
                    "Search engine name should not have been set yet",
                    0,
                    mScopedObserverData.updateCachedEngineCallback.getCallCount());

            return this;
        }

        protected FirstRunNavigationHelper ensureTermsOfServiceIsCurrentPage() {
            return waitForCurrentFragmentToMatch(
                    "Terms of Service should be the current page",
                    Matchers.instanceOf(SigninFirstRunFragment.class));
        }

        protected FirstRunNavigationHelper ensureDefaultSearchEnginePromoIsCurrentPage() {
            return waitForCurrentFragmentToMatch(
                    "Search engine promo should be the current page",
                    Matchers.instanceOf(DefaultSearchEngineFirstRunFragment.class));
        }

        protected FirstRunNavigationHelper ensureDefaultSearchEnginePromoNotCurrentPage() {
            return waitForCurrentFragmentToMatch(
                    "Search engine promo shouldn't be the current page",
                    Matchers.not(Matchers.instanceOf(DefaultSearchEngineFirstRunFragment.class)));
        }

        protected FirstRunNavigationHelper ensureSigninPromoIsCurrentPage() {
            if (ChromeFeatureList.isEnabled(
                    ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
                ensureTermsOfServiceIsCurrentPage();
                return this;
            }
            return waitForCurrentFragmentToMatch(
                    "Sign-in promo should be the current page",
                    Matchers.instanceOf(SyncConsentFirstRunFragment.class));
        }

        protected FirstRunNavigationHelper ensureSigninPromoNotCurrentPage() {
            return waitForCurrentFragmentToMatch(
                    "Sign-in promo shouldn't be the current page",
                    Matchers.not(Matchers.instanceOf(SyncConsentFirstRunFragment.class)));
        }

        // TODO(b/346755013): Rename this method once we add integration tests for the case where an
        // account exists on the device.
        protected FirstRunNavigationHelper continueWithoutAnAccount() throws Exception {
            ensureTermsOfServiceIsCurrentPage();

            int jumpCallCount = mScopedObserverData.jumpToPageCallback.getCallCount();
            int acceptCallCount = mScopedObserverData.acceptTermsOfServiceCallback.getCallCount();

            clickButton(mFirstRunActivity, R.id.signin_fre_continue_button, "Failed to accept ToS");
            mScopedObserverData.jumpToPageCallback.waitForCallback(
                    "Failed to try moving to the next screen", jumpCallCount);
            mScopedObserverData.acceptTermsOfServiceCallback.waitForCallback(
                    "Failed to accept the ToS", acceptCallCount);

            return this;
        }

        protected FirstRunNavigationHelper selectDefaultSearchEngine() throws Exception {
            ensureDefaultSearchEnginePromoIsCurrentPage();

            int jumpCallCount = mScopedObserverData.jumpToPageCallback.getCallCount();
            DefaultSearchEngineDialogHelperUtils.clickOnFirstEngine(
                    mFirstRunActivity.findViewById(android.R.id.content));
            mScopedObserverData.jumpToPageCallback.waitForCallback(
                    "Failed trying to move past the search engine fragment", jumpCallCount);

            return this;
        }

        protected FirstRunNavigationHelper skipSigninPromo() throws Exception {
            if (ChromeFeatureList.isEnabled(
                    ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
                ensureTermsOfServiceIsCurrentPage();
            } else {
                ensureSigninPromoIsCurrentPage();
            }

            int jumpCallCount = mScopedObserverData.jumpToPageCallback.getCallCount();
            if (ChromeFeatureList.isEnabled(
                    ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
                clickButton(
                        mFirstRunActivity,
                        R.id.signin_fre_dismiss_button,
                        "Failed to skip signing-in");
            } else {
                clickButton(mFirstRunActivity, R.id.button_secondary, "Failed to skip signing-in");
            }
            mScopedObserverData.jumpToPageCallback.waitForCallback(
                    "Failed trying to move past the sign in fragment", jumpCallCount);

            return this;
        }

        protected FirstRunNavigationHelper goBackToPreviousPage() throws Exception {
            int jumpCallCount = mScopedObserverData.jumpToPageCallback.getCallCount();
            ThreadUtils.runOnUiThreadBlocking(
                    mFirstRunActivity.getOnBackPressedDispatcher()::onBackPressed);
            mScopedObserverData.jumpToPageCallback.waitForCallback(
                    "Failed go back to previous page", jumpCallCount);

            return this;
        }

        protected FirstRunNavigationHelper waitForCurrentFragmentToMatch(
                String failureReason, Matcher<Object> matcher) {
            CriteriaHelper.pollUiThread(
                    () -> matcher.matches(mFirstRunActivity.getCurrentFragmentForTesting()),
                    failureReason);
            return this;
        }
    }

    /**
     * Overrides the default {@link FirstRunFlowSequencer}'s delegate to make decisions on
     * showing/skipping promo pages based on the current {@link FirstRunPagesTestCase}.
     */
    private static class TestFirstRunFlowSequencerDelegate
            extends FirstRunFlowSequencer.FirstRunFlowSequencerDelegate {
        private FirstRunPagesTestCase mTestCase;

        public TestFirstRunFlowSequencerDelegate(
                FirstRunPagesTestCase testCase, OneshotSupplier<ProfileProvider> profileProvider) {
            super(profileProvider);
            mTestCase = testCase;
        }

        @Override
        public boolean shouldShowSyncConsentPage(boolean isChild) {
            return mTestCase.showSigninPromo()
                    && !ChromeFeatureList.isEnabled(
                            ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS);
        }

        @Override
        public boolean shouldShowHistorySyncOptIn(boolean isChild) {
            // TODO(b/346755013): Update this method to correctly determine whether to show History
            // sync or not, depending on the test case.
            return false;
        }

        @Override
        public boolean shouldShowSearchEnginePage() {
            return mTestCase.showSearchPromo();
        }
    }
}