chromium/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/UndoTabModelUnitTest.java

// Copyright 2022 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.tabmodel;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import androidx.test.filters.SmallTest;

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

import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.homepage.HomepageManager;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.TabContentManager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;

/**
 * Unit tests for undo and restoring of tabs in a {@link TabModel}. For additional tests that are
 * impossible or difficult to implement as unit test see {@link UndoTabModelTest}.
 */
@RunWith(BaseRobolectricTestRunner.class)
public class UndoTabModelUnitTest {
    private static final long FAKE_NATIVE_ADDRESS = 123L;
    private static final Tab[] sEmptyList = new Tab[] {};

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    /** Disable native calls from {@link TabModelJniBridge}. */
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private TabModelJniBridge.Natives mTabModelJniBridge;

    /** Required to be non-null for {@link TabModelJniBridge}. */
    @Mock private Profile mProfile;

    @Mock private Profile mIncognitoProfile;

    /** Required to simulate tab thumbnail deletion. */
    @Mock private TabContentManager mTabContentManager;

    /** Required to handle some tab lookup actions. */
    @Mock private TabModelDelegate mTabModelDelegate;

    /** Required to handle some actions and initialize {@link TabModelOrderControllerImpl}. */
    @Mock private TabModelSelector mTabModelSelector;

    @Mock private TabModelFilterProvider mTabModelFilterProvider;
    @Mock private TabModelFilter mTabModelFilter;

    @Mock private Callback<Tab> mTabSupplierObserver;

    private int mNextTabId;

    @Before
    public void setUp() {
        // Disable HomepageManager#shouldCloseAppWithZeroTabs() for TabModelImpl#closeAllTabs().
        HomepageManager.getInstance().setPrefHomepageEnabled(false);

        when(mIncognitoProfile.isOffTheRecord()).thenReturn(true);

        PriceTrackingFeatures.setPriceTrackingEnabledForTesting(false);

        mJniMocker.mock(TabModelJniBridgeJni.TEST_HOOKS, mTabModelJniBridge);
        when(mTabModelJniBridge.init(any(), any(), anyInt(), anyBoolean()))
                .thenReturn(FAKE_NATIVE_ADDRESS);

        when(mTabModelDelegate.isReparentingInProgress()).thenReturn(false);

        when(mTabModelSelector.getTabModelFilterProvider()).thenReturn(mTabModelFilterProvider);
        when(mTabModelFilterProvider.getTabModelFilter(false)).thenReturn(mTabModelFilter);
        when(mTabModelFilterProvider.getTabModelFilter(true)).thenReturn(mTabModelFilter);
        when(mTabModelFilter.getValidPosition(any(), anyInt()))
                .thenAnswer(i -> i.getArguments()[1]);

        mNextTabId = 0;
    }

    /** Create a {@link TabModel} to use for the test. */
    private TabModel createTabModel(boolean isIncognito) {
        AsyncTabParamsManager realAsyncTabParamsManager =
                AsyncTabParamsManagerFactory.createAsyncTabParamsManager();
        TabModelOrderControllerImpl orderController =
                new TabModelOrderControllerImpl(mTabModelSelector);
        TabModel tabModel;
        final boolean supportUndo = !isIncognito;
        if (isIncognito) {
            // TODO(crbug.com/40222755): Consider using an incognito tab model.
            tabModel =
                    new TabModelImpl(
                            mIncognitoProfile,
                            ActivityType.TABBED,
                            /* regularTabCreator= */ null,
                            /* incognitoTabCreator= */ null,
                            orderController,
                            mTabContentManager,
                            () -> NextTabPolicy.HIERARCHICAL,
                            realAsyncTabParamsManager,
                            mTabModelDelegate,
                            supportUndo,
                            /* trackInNativeModelList= */ true);
            when(mTabModelSelector.getModel(true)).thenReturn(tabModel);
        } else {
            tabModel =
                    new TabModelImpl(
                            mProfile,
                            ActivityType.TABBED,
                            /* regularTabCreator= */ null,
                            /* incognitoTabCreator= */ null,
                            orderController,
                            mTabContentManager,
                            () -> NextTabPolicy.HIERARCHICAL,
                            realAsyncTabParamsManager,
                            mTabModelDelegate,
                            supportUndo,
                            /* trackInNativeModelList= */ true);
            when(mTabModelSelector.getModel(false)).thenReturn(tabModel);
        }
        // Assume the model is the current and active model.
        tabModel.setActive(true);
        when(mTabModelSelector.getCurrentModel()).thenReturn(tabModel);
        when(mTabModelDelegate.getCurrentModel()).thenReturn(tabModel);
        // Avoid NPE in TabModelImpl#findTabInAllTabModels() by assuming two duplicate models exist
        // as it doesn't matter for that method.
        when(mTabModelDelegate.getModel(anyBoolean())).thenReturn(tabModel);
        return tabModel;
    }

    /** Check {@code model} contains the correct tab lists and has the right {@code selectedTab}. */
    private void checkState(
            final TabModel model,
            final Tab[] tabsList,
            final Tab selectedTab,
            final Tab[] closingTabs,
            final Tab[] fullTabsList,
            final Tab fullSelectedTab) {
        // Keeping these checks on the test thread so the stacks are useful for identifying
        // failures.

        // Check the selected tab.
        assertEquals("Wrong selected tab", selectedTab, TabModelUtils.getCurrentTab(model));

        // Check the list of tabs.
        assertEquals("Incorrect number of tabs", tabsList.length, model.getCount());
        for (int i = 0; i < tabsList.length; i++) {
            assertEquals("Unexpected tab at " + i, tabsList[i].getId(), model.getTabAt(i).getId());
        }

        // Check the list of tabs we expect to be closing.
        for (int i = 0; i < closingTabs.length; i++) {
            int id = closingTabs[i].getId();
            assertTrue("Tab " + id + " not in closing list", model.isClosurePending(id));
        }

        TabList fullModel = model.getComprehensiveModel();

        // Check the comprehensive selected tab.
        assertEquals("Wrong selected tab", fullSelectedTab, TabModelUtils.getCurrentTab(fullModel));

        // Check the comprehensive list of tabs.
        assertEquals("Incorrect number of tabs", fullTabsList.length, fullModel.getCount());
        for (int i = 0; i < fullModel.getCount(); i++) {
            int id = fullModel.getTabAt(i).getId();
            assertEquals("Unexpected tab at " + i, fullTabsList[i].getId(), id);
        }
    }

    private void createTab(final TabModel model, boolean isIncognito) {
        final int launchType = TabLaunchType.FROM_CHROME_UI;
        MockTab tab =
                MockTab.createAndInitialize(
                        mNextTabId++, isIncognito ? mIncognitoProfile : mProfile, launchType);
        tab.setIsInitialized(true);
        model.addTab(tab, -1, launchType, TabCreationState.LIVE_IN_FOREGROUND);
    }

    private void selectTab(final TabModel model, final Tab tab) {
        model.setIndex(model.indexOf(tab), TabSelectionType.FROM_USER);
    }

    private void closeTab(final TabModel model, final Tab tab, final boolean undoable)
            throws TimeoutException {
        // Check preconditions.
        assertFalse(tab.isClosing());
        assertTrue(tab.isInitialized());
        assertFalse(model.isClosurePending(tab.getId()));
        assertNotNull(model.getTabById(tab.getId()));

        final CallbackHelper didReceivePendingClosureHelper = new CallbackHelper();
        model.addObserver(
                new TabModelObserver() {
                    @Override
                    public void tabPendingClosure(Tab tab) {
                        didReceivePendingClosureHelper.notifyCalled();
                    }
                });

        // Take action.
        model.closeTabs(TabClosureParams.closeTab(tab).allowUndo(undoable).build());

        boolean didMakePending = undoable && model.supportsPendingClosures();

        // Make sure the TabModel throws a tabPendingClosure callback if necessary.
        if (didMakePending) didReceivePendingClosureHelper.waitForCallback(0);

        // Check post conditions
        assertEquals(didMakePending, model.isClosurePending(tab.getId()));
        assertNull(model.getTabById(tab.getId()));
        assertTrue(tab.isClosing());
        assertEquals(didMakePending, tab.isInitialized());
    }

    private void closeMultipleTabsInternal(
            final TabModel model, final Runnable closeRunnable, final boolean undoable)
            throws TimeoutException {
        final CallbackHelper didReceivePendingClosureHelper = new CallbackHelper();
        model.addObserver(
                new TabModelObserver() {
                    @Override
                    public void multipleTabsPendingClosure(List<Tab> tabs, boolean isAllTabs) {
                        didReceivePendingClosureHelper.notifyCalled();
                    }
                });
        closeRunnable.run();

        boolean didMakePending = undoable && model.supportsPendingClosures();

        // Make sure the TabModel throws a tabPendingClosure callback if necessary.
        if (didMakePending) didReceivePendingClosureHelper.waitForCallback(0);
    }

    private void closeMultipleTabs(
            final TabModel model, final List<Tab> tabs, final boolean undoable)
            throws TimeoutException {
        closeMultipleTabsInternal(
                model,
                () -> model.closeTabs(TabClosureParams.closeTabs(tabs).allowUndo(undoable).build()),
                undoable);
    }

    private void closeAllTabs(final TabModel model) throws TimeoutException {
        closeMultipleTabsInternal(
                model, () -> model.closeTabs(TabClosureParams.closeAllTabs().build()), true);
    }

    private void cancelTabClosure(final TabModel model, final Tab tab) throws TimeoutException {
        // Check preconditions.
        assertTrue(tab.isClosing());
        assertTrue(tab.isInitialized());
        assertTrue(model.isClosurePending(tab.getId()));
        assertNull(model.getTabById(tab.getId()));

        final CallbackHelper didReceiveClosureCancelledHelper = new CallbackHelper();
        model.addObserver(
                new TabModelObserver() {
                    @Override
                    public void tabClosureUndone(Tab tab) {
                        didReceiveClosureCancelledHelper.notifyCalled();
                    }
                });

        // Take action.
        model.cancelTabClosure(tab.getId());

        // Make sure the TabModel throws a tabClosureUndone.
        didReceiveClosureCancelledHelper.waitForCallback(0);

        // Check post conditions.
        assertFalse(model.isClosurePending(tab.getId()));
        assertNotNull(model.getTabById(tab.getId()));
        assertFalse(tab.isClosing());
        assertTrue(tab.isInitialized());
    }

    private void cancelAllTabClosures(final TabModel model, final Tab[] expectedToClose)
            throws TimeoutException {
        final CallbackHelper tabClosureUndoneHelper = new CallbackHelper();
        final CallbackHelper allTabClosureCancellationCompletedHelper = new CallbackHelper();

        for (int i = 0; i < expectedToClose.length; i++) {
            Tab tab = expectedToClose[i];
            assertTrue(tab.isClosing());
            assertTrue(tab.isInitialized());
            assertTrue(model.isClosurePending(tab.getId()));
            assertNull(model.getTabById(tab.getId()));

            // Make sure that this TabModel throws the right events.
            model.addObserver(
                    new TabModelObserver() {
                        @Override
                        public void tabClosureUndone(Tab currentTab) {
                            tabClosureUndoneHelper.notifyCalled();
                        }

                        @Override
                        public void allTabsClosureUndone() {
                            allTabClosureCancellationCompletedHelper.notifyCalled();
                        }
                    });
        }

        for (int i = 0; i < expectedToClose.length; i++) {
            Tab tab = expectedToClose[i];
            model.cancelTabClosure(tab.getId());
        }
        model.notifyAllTabsClosureUndone();

        tabClosureUndoneHelper.waitForCallback(0, expectedToClose.length);
        allTabClosureCancellationCompletedHelper.waitForCallback(0, 1);

        for (int i = 0; i < expectedToClose.length; i++) {
            final Tab tab = expectedToClose[i];
            assertFalse(model.isClosurePending(tab.getId()));
            assertNotNull(model.getTabById(tab.getId()));
            assertFalse(tab.isClosing());
            assertTrue(tab.isInitialized());
        }
    }

    private void commitTabClosure(final TabModel model, final Tab tab) throws TimeoutException {
        // Check preconditions.
        assertTrue(tab.isClosing());
        assertTrue(tab.isInitialized());
        assertTrue(model.isClosurePending(tab.getId()));
        assertNull(model.getTabById(tab.getId()));

        final CallbackHelper didReceiveClosureCommittedHelper = new CallbackHelper();
        model.addObserver(
                new TabModelObserver() {
                    @Override
                    public void tabClosureCommitted(Tab tab) {
                        didReceiveClosureCommittedHelper.notifyCalled();
                    }
                });

        // Take action.
        model.commitTabClosure(tab.getId());

        // Make sure the TabModel throws a tabClosureCommitted.
        didReceiveClosureCommittedHelper.waitForCallback(0);

        // Check post conditions
        assertFalse(model.isClosurePending(tab.getId()));
        assertNull(model.getTabById(tab.getId()));
        assertTrue(tab.isClosing());
        assertFalse(tab.isInitialized());
    }

    private void commitAllTabClosures(final TabModel model, Tab[] expectedToClose)
            throws TimeoutException {
        final CallbackHelper tabClosureCommittedHelper = new CallbackHelper();

        for (int i = 0; i < expectedToClose.length; i++) {
            Tab tab = expectedToClose[i];
            assertTrue(tab.isClosing());
            assertTrue(tab.isInitialized());
            assertTrue(model.isClosurePending(tab.getId()));

            // Make sure that this TabModel throws the right events.
            model.addObserver(
                    new TabModelObserver() {
                        @Override
                        public void tabClosureCommitted(Tab currentTab) {
                            tabClosureCommittedHelper.notifyCalled();
                        }
                    });
        }

        model.commitAllTabClosures();

        tabClosureCommittedHelper.waitForCallback(0, expectedToClose.length);
        for (int i = 0; i < expectedToClose.length; i++) {
            final Tab tab = expectedToClose[i];
            assertTrue(tab.isClosing());
            assertFalse(tab.isInitialized());
            assertFalse(model.isClosurePending(tab.getId()));
        }
    }

    /**
     * Test undo with a single tab with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0s ]             -                 [ 0s ]
     * 2.  CloseTab(0, allow undo)    -                  [ 0 ]             [ 0s ]
     * 3.  CancelClose(0)             [ 0s ]             -                 [ 0s ]
     * 4.  CloseTab(0, allow undo)    -                  [ 0 ]             [ 0s ]
     * 5.  CommitClose(0)             -                  -                 -
     * 6.  CreateTab(0)               [ 0s ]             -                 [ 0s ]
     * 7.  CloseTab(0, allow undo)    -                  [ 0 ]             [ 0s ]
     * 8.  CommitAllClose             -                  -                 -
     * 9.  CreateTab(0)               [ 0s ]             -                 [ 0s ]
     * 10. CloseTab(0, disallow undo) -                  -                 -
     *
     */
    @Test
    @SmallTest
    public void testSingleTab() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);

        Tab[] fullList = new Tab[] {tab0};

        // 1.
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);

        // 2.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0}, fullList, tab0);

        // 3.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);

        // 4.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0}, fullList, tab0);

        // 5.
        commitTabClosure(model, tab0);
        fullList = sEmptyList;
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);

        // 6.
        createTab(model, isIncognito);
        tab0 = model.getTabAt(0);
        fullList = new Tab[] {tab0};
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);

        // 7.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0}, fullList, tab0);

        // 8.
        commitAllTabClosures(model, new Tab[] {tab0});
        fullList = sEmptyList;
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);

        // 9.
        createTab(model, isIncognito);
        tab0 = model.getTabAt(0);
        fullList = new Tab[] {tab0};
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);

        // 10.
        closeTab(model, tab0, false);
        fullList = sEmptyList;
        checkState(model, sEmptyList, null, sEmptyList, fullList, null);
        assertTrue(tab0.isClosing());
        assertFalse(tab0.isInitialized());
    }

    /**
     * Test undo with two tabs with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1s ]           -                 [ 0 1s ]
     * 2.  CloseTab(0, allow undo)    [ 1s ]             [ 0 ]             [ 0 1s ]
     * 3.  CancelClose(0)             [ 0 1s ]           -                 [ 0 1s ]
     * 4.  CloseTab(0, allow undo)    [ 1s ]             [ 0 ]             [ 0 1s ]
     * 5.  CloseTab(1, allow undo)    -                  [ 1 0 ]           [ 0s 1 ]
     * 6.  CancelClose(1)             [ 1s ]             [ 0 ]             [ 0 1s ]
     * 7.  CancelClose(0)             [ 0 1s ]           -                 [ 0 1s ]
     * 8.  CloseTab(1, allow undo)    [ 0s ]             [ 1 ]             [ 0s 1 ]
     * 9.  CloseTab(0, allow undo)    -                  [ 0 1 ]           [ 0s 1 ]
     * 10. CancelClose(1)             [ 1s ]             [ 0 ]             [ 0 1s ]
     * 11. CancelClose(0)             [ 0 1s ]           -                 [ 0 1s ]
     * 12. CloseTab(1, allow undo)    [ 0s ]             [ 1 ]             [ 0s 1 ]
     * 13. CloseTab(0, allow undo)    -                  [ 0 1 ]           [ 0s 1 ]
     * 14. CancelClose(0)             [ 0s ]             [ 1 ]             [ 0s 1 ]
     * 15. CloseTab(0, allow undo)    -                  [ 0 1 ]           [ 0s 1 ]
     * 16. CancelClose(0)             [ 0s ]             [ 1 ]             [ 0s 1 ]
     * 17. CancelClose(1)             [ 0s 1 ]           -                 [ 0s 1 ]
     * 18. CloseTab(0, disallow undo) [ 1s ]             -                 [ 1s ]
     * 19. CreateTab(0)               [ 1 0s ]           -                 [ 1 0s ]
     * 20. CloseTab(0, allow undo)    [ 1s ]             [ 0 ]             [ 1s 0 ]
     * 21. CommitClose(0)             [ 1s ]             -                 [ 1s ]
     * 22. CreateTab(0)               [ 1 0s ]           -                 [ 1 0s ]
     * 23. CloseTab(0, allow undo)    [ 1s ]             [ 0 ]             [ 1s 0 ]
     * 24. CloseTab(1, allow undo)    -                  [ 1 0 ]           [ 1s 0 ]
     * 25. CommitAllClose             -                  -                 -
     *
     */
    @Test
    @SmallTest
    public void testTwoTabs() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);

        Tab[] fullList = new Tab[] {tab0, tab1};

        // 1.
        checkState(model, new Tab[] {tab0, tab1}, tab1, sEmptyList, fullList, tab1);

        // 2.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);

        // 3.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1}, tab1, sEmptyList, fullList, tab1);

        // 4.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);

        // 5.
        closeTab(model, tab1, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0, tab1}, fullList, tab0);

        // 6.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);

        // 7.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1}, tab1, sEmptyList, fullList, tab1);

        // 8.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1}, fullList, tab0);

        // 9.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0, tab1}, fullList, tab0);

        // 10.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);

        // 11.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1}, tab1, sEmptyList, fullList, tab1);

        // 12.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1}, fullList, tab0);

        // 13.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0, tab1}, fullList, tab0);

        // 14.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1}, fullList, tab0);

        // 15.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0, tab1}, fullList, tab0);

        // 16.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1}, fullList, tab0);

        // 17.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab0, tab1}, tab0, sEmptyList, fullList, tab0);

        // 18.
        closeTab(model, tab0, false);
        fullList = new Tab[] {tab1};
        checkState(model, new Tab[] {tab1}, tab1, sEmptyList, fullList, tab1);

        // 19.
        createTab(model, isIncognito);
        tab0 = model.getTabAt(1);
        fullList = new Tab[] {tab1, tab0};
        checkState(model, new Tab[] {tab1, tab0}, tab0, sEmptyList, fullList, tab0);

        // 20.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);

        // 21.
        commitTabClosure(model, tab0);
        fullList = new Tab[] {tab1};
        checkState(model, new Tab[] {tab1}, tab1, sEmptyList, fullList, tab1);

        // 22.
        createTab(model, isIncognito);
        tab0 = model.getTabAt(1);
        fullList = new Tab[] {tab1, tab0};
        checkState(model, new Tab[] {tab1, tab0}, tab0, sEmptyList, fullList, tab0);

        // 23.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);

        // 24.
        closeTab(model, tab1, true);
        checkState(model, sEmptyList, null, new Tab[] {tab1, tab0}, fullList, tab1);

        // 25.
        commitAllTabClosures(model, new Tab[] {tab1, tab0});
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
    }

    /**
     * Test restoring in the same order of closing with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(0, allow undo)    [ 1 2 3s ]         [ 0 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(1, allow undo)    [ 2 3s ]           [ 1 0 ]           [ 0 1 2 3s ]
     * 4.  CloseTab(2, allow undo)    [ 3s ]             [ 2 1 0 ]         [ 0 1 2 3s ]
     * 5.  CloseTab(3, allow undo)    -                  [ 3 2 1 0 ]       [ 0s 1 2 3 ]
     * 6.  CancelClose(3)             [ 3s ]             [ 2 1 0 ]         [ 0 1 2 3s ]
     * 7.  CancelClose(2)             [ 2 3s ]           [ 1 0 ]           [ 0 1 2 3s ]
     * 8.  CancelClose(1)             [ 1 2 3s ]         [ 0 ]             [ 0 1 2 3s ]
     * 9.  CancelClose(0)             [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 10. SelectTab(3)               [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 11. CloseTab(3, allow undo)    [ 0 1 2s ]         [ 3 ]             [ 0 1 2s 3 ]
     * 12. CloseTab(2, allow undo)    [ 0 1s ]           [ 2 3 ]           [ 0 1s 2 3 ]
     * 13. CloseTab(1, allow undo)    [ 0s ]             [ 1 2 3 ]         [ 0s 1 2 3 ]
     * 14. CloseTab(0, allow undo)    -                  [ 0 1 2 3 ]       [ 0s 1 2 3 ]
     * 15. CancelClose(0)             [ 0s ]             [ 1 2 3 ]         [ 0s 1 2 3 ]
     * 16. CancelClose(1)             [ 0s 1 ]           [ 2 3 ]           [ 0s 1 2 3 ]
     * 17. CancelClose(2)             [ 0s 1 2 ]         [ 3 ]             [ 0s 1 2 3 ]
     * 18. CancelClose(3)             [ 0s 1 2 3 ]       -                 [ 0s 1 2 3 ]
     * 19. CloseTab(2, allow undo)    [ 0s 1 3 ]         [ 2 ]             [ 0s 1 2 3 ]
     * 20. CloseTab(0, allow undo)    [ 1s 3 ]           [ 0 2 ]           [ 0 1s 2 3 ]
     * 21. CloseTab(3, allow undo)    [ 1s ]             [ 3 0 2 ]         [ 0 1s 2 3 ]
     * 22. CancelClose(3)             [ 1s 3 ]           [ 0 2 ]           [ 0 1s 2 3 ]
     * 23. CancelClose(0)             [ 0 1s 3 ]         [ 2 ]             [ 0 1s 2 3 ]
     * 24. CancelClose(2)             [ 0 1s 2 3 ]       -                 [ 0 1s 2 3 ]
     *
     */
    @Test
    @SmallTest
    public void testInOrderRestore() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        final Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1, tab2, tab3}, tab3, new Tab[] {tab0}, fullList, tab3);

        // 3.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab2, tab3}, tab3, new Tab[] {tab1, tab0}, fullList, tab3);

        // 4.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab2, tab1, tab0}, fullList, tab3);

        // 5.
        closeTab(model, tab3, true);
        checkState(model, sEmptyList, null, new Tab[] {tab3, tab2, tab1, tab0}, fullList, tab0);

        // 6.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab2, tab1, tab0}, fullList, tab3);

        // 7.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab2, tab3}, tab3, new Tab[] {tab1, tab0}, fullList, tab3);

        // 8.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1, tab2, tab3}, tab3, new Tab[] {tab0}, fullList, tab3);

        // 9.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 10.
        selectTab(model, tab3);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 11.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab2, new Tab[] {tab3}, fullList, tab2);

        // 12.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab1}, tab1, new Tab[] {tab2, tab3}, fullList, tab1);

        // 13.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1, tab2, tab3}, fullList, tab0);

        // 14.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0, tab1, tab2, tab3}, fullList, tab0);

        // 15.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1, tab2, tab3}, fullList, tab0);

        // 16.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab0, tab1}, tab0, new Tab[] {tab2, tab3}, fullList, tab0);

        // 17.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab0, new Tab[] {tab3}, fullList, tab0);

        // 18.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab0, sEmptyList, fullList, tab0);

        // 19.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab1, tab3}, tab0, new Tab[] {tab2}, fullList, tab0);

        // 20.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1, tab3}, tab1, new Tab[] {tab0, tab2}, fullList, tab1);

        // 21.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab3, tab0, tab2}, fullList, tab1);

        // 22.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab1, tab3}, tab1, new Tab[] {tab0, tab2}, fullList, tab1);

        // 23.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1, tab3}, tab1, new Tab[] {tab2}, fullList, tab1);

        // 24.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab1, sEmptyList, fullList, tab1);
    }

    /**
     * Test restoring in the reverse of closing with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(0, allow undo)    [ 1 2 3s ]         [ 0 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(1, allow undo)    [ 2 3s ]           [ 1 0 ]           [ 0 1 2 3s ]
     * 4.  CloseTab(2, allow undo)    [ 3s ]             [ 2 1 0 ]         [ 0 1 2 3s ]
     * 5.  CloseTab(3, allow undo)    -                  [ 3 2 1 0 ]       [ 0s 1 2 3 ]
     * 6.  CancelClose(0)             [ 0s ]             [ 3 2 1 ]         [ 0s 1 2 3 ]
     * 7.  CancelClose(1)             [ 0s 1 ]           [ 3 2 ]           [ 0s 1 2 3 ]
     * 8.  CancelClose(2)             [ 0s 1 2 ]         [ 3 ]             [ 0s 1 2 3 ]
     * 9.  CancelClose(3)             [ 0s 1 2 3 ]       -                 [ 0s 1 2 3 ]
     * 10. CloseTab(3, allow undo)    [ 0s 1 2 ]         [ 3 ]             [ 0s 1 2 3 ]
     * 11. CloseTab(2, allow undo)    [ 0s 1 ]           [ 2 3 ]           [ 0s 1 2 3 ]
     * 12. CloseTab(1, allow undo)    [ 0s ]             [ 1 2 3 ]         [ 0s 1 2 3 ]
     * 13. CloseTab(0, allow undo)    -                  [ 0 1 2 3 ]       [ 0s 1 2 3 ]
     * 14. CancelClose(3)             [ 3s ]             [ 0 1 2 ]         [ 0 1 2 3s ]
     * 15. CancelClose(2)             [ 2 3s ]           [ 0 1 ]           [ 0 1 2 3s ]
     * 16. CancelClose(1)             [ 1 2 3s ]         [ 0 ]             [ 0 1 2 3s ]
     * 17. CancelClose(0)             [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 18. SelectTab(3)               [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 19. CloseTab(2, allow undo)    [ 0 1 3s ]         [ 2 ]             [ 0 1 2 3s ]
     * 20. CloseTab(0, allow undo)    [ 1 3s ]           [ 0 2 ]           [ 0 1 2 3s ]
     * 21. CloseTab(3, allow undo)    [ 1s ]             [ 3 0 2 ]         [ 0 1s 2 3 ]
     * 22. CancelClose(2)             [ 1s 2 ]           [ 3 0 ]           [ 0 1s 2 3 ]
     * 23. CancelClose(0)             [ 0 1s 2 ]         [ 3 ]             [ 0 1s 2 3 ]
     * 24. CancelClose(3)             [ 0 1s 2 3 ]       -                 [ 0 1s 2 3 ]
     *
     */
    @Test
    @SmallTest
    public void testReverseOrderRestore() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1, tab2, tab3}, tab3, new Tab[] {tab0}, fullList, tab3);

        // 3.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab2, tab3}, tab3, new Tab[] {tab1, tab0}, fullList, tab3);

        // 4.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab2, tab1, tab0}, fullList, tab3);

        // 5.
        closeTab(model, tab3, true);
        checkState(model, sEmptyList, null, new Tab[] {tab3, tab2, tab1, tab0}, fullList, tab0);

        // 6.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab3, tab2, tab1}, fullList, tab0);

        // 7.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab0, tab1}, tab0, new Tab[] {tab3, tab2}, fullList, tab0);

        // 8.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab0, new Tab[] {tab3}, fullList, tab0);

        // 9.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab0, sEmptyList, fullList, tab0);

        // 10.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab0, new Tab[] {tab3}, fullList, tab0);

        // 11.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab1}, tab0, new Tab[] {tab2, tab3}, fullList, tab0);

        // 12.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1, tab2, tab3}, fullList, tab0);

        // 13.
        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0, tab1, tab2, tab3}, fullList, tab0);

        // 14.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab0, tab1, tab2}, fullList, tab3);

        // 15.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab2, tab3}, tab3, new Tab[] {tab0, tab1}, fullList, tab3);

        // 16.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1, tab2, tab3}, tab3, new Tab[] {tab0}, fullList, tab3);

        // 17.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 18.
        selectTab(model, tab3);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 19.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab1, tab3}, tab3, new Tab[] {tab2}, fullList, tab3);

        // 20.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1, tab3}, tab3, new Tab[] {tab0, tab2}, fullList, tab3);

        // 21.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab3, tab0, tab2}, fullList, tab1);

        // 22.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab1, tab2}, tab1, new Tab[] {tab3, tab0}, fullList, tab1);

        // 23.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab1, new Tab[] {tab3}, fullList, tab1);

        // 24.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab1, sEmptyList, fullList, tab1);
    }

    /**
     * Test restoring out of order with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(0, allow undo)    [ 1 2 3s ]         [ 0 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(1, allow undo)    [ 2 3s ]           [ 1 0 ]           [ 0 1 2 3s ]
     * 4.  CloseTab(2, allow undo)    [ 3s ]             [ 2 1 0 ]         [ 0 1 2 3s ]
     * 5.  CloseTab(3, allow undo)    -                  [ 3 2 1 0 ]       [ 0s 1 2 3 ]
     * 6.  CancelClose(2)             [ 2s ]             [ 3 1 0 ]         [ 0 1 2s 3 ]
     * 7.  CancelClose(1)             [ 1 2s ]           [ 3 0 ]           [ 0 1 2s 3 ]
     * 8.  CancelClose(3)             [ 1 2s 3 ]         [ 0 ]             [ 0 1 2s 3 ]
     * 9.  CancelClose(0)             [ 0 1 2s 3 ]       -                 [ 0 1 2s 3 ]
     * 10. CloseTab(1, allow undo)    [ 0 2s 3 ]         [ 1 ]             [ 0 1 2s 3 ]
     * 11. CancelClose(1)             [ 0 1 2s 3 ]       -                 [ 0 1 2s 3 ]
     * 12. CloseTab(3, disallow undo) [ 0 1 2s ]         -                 [ 0 1 2s ]
     * 13. CloseTab(1, allow undo)    [ 0 2s ]           [ 1 ]             [ 0 1 2s ]
     * 14. CloseTab(0, allow undo)    [ 2s ]             [ 0 1 ]           [ 0 1 2s ]
     * 15. CommitClose(0)             [ 2s ]             [ 1 ]             [ 1 2s ]
     * 16. CancelClose(1)             [ 1 2s ]           -                 [ 1 2s ]
     * 17. CloseTab(2, disallow undo) [ 1s ]             -                 [ 1s ]
     *
     */
    @Test
    @SmallTest
    public void testOutOfOrder1() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1, tab2, tab3}, tab3, new Tab[] {tab0}, fullList, tab3);

        // 3.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab2, tab3}, tab3, new Tab[] {tab1, tab0}, fullList, tab3);

        // 4.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab2, tab1, tab0}, fullList, tab3);

        // 5.
        closeTab(model, tab3, true);
        checkState(model, sEmptyList, null, new Tab[] {tab3, tab2, tab1, tab0}, fullList, tab0);

        // 6.
        cancelTabClosure(model, tab2);
        checkState(model, new Tab[] {tab2}, tab2, new Tab[] {tab3, tab1, tab0}, fullList, tab2);

        // 7.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1, tab2}, tab2, new Tab[] {tab3, tab0}, fullList, tab2);

        // 8.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab1, tab2, tab3}, tab2, new Tab[] {tab0}, fullList, tab2);

        // 9.
        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab2, sEmptyList, fullList, tab2);

        // 10.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab2, new Tab[] {tab1}, fullList, tab2);

        // 11.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab2, sEmptyList, fullList, tab2);

        // 12.
        closeTab(model, tab3, false);
        fullList = new Tab[] {tab0, tab1, tab2};
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab2, sEmptyList, fullList, tab2);

        // 13.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2}, tab2, new Tab[] {tab1}, fullList, tab2);

        // 14.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab2}, tab2, new Tab[] {tab0, tab1}, fullList, tab2);

        // 15.
        commitTabClosure(model, tab0);
        fullList = new Tab[] {tab1, tab2};
        checkState(model, new Tab[] {tab2}, tab2, new Tab[] {tab1}, fullList, tab2);

        // 16.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1, tab2}, tab2, sEmptyList, fullList, tab2);

        // 17.
        closeTab(model, tab2, false);
        fullList = new Tab[] {tab1};
        checkState(model, new Tab[] {tab1}, tab1, sEmptyList, fullList, tab1);
    }

    /**
     * Test restoring out of order with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(1, allow undo)    [ 0 2 3s ]         [ 1 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(3, allow undo)    [ 0 2s ]           [ 3 1 ]           [ 0 1 2s 3 ]
     * 4.  CancelClose(1)             [ 0 1 2s ]         [ 3 ]             [ 0 1 2s 3 ]
     * 5.  CloseTab(2, allow undo)    [ 0 1s ]           [ 2 3 ]           [ 0 1s 2 3 ]
     * 6.  CloseTab(0, allow undo)    [ 1s ]             [ 0 2 3 ]         [ 0 1s 2 3 ]
     * 7.  CommitClose(0)             [ 1s ]             [ 2 3 ]           [ 1s 2 3 ]
     * 8.  CancelClose(3)             [ 1s 3 ]           [ 2 ]             [ 1s 2 3 ]
     * 9.  CloseTab(1, allow undo)    [ 3s ]             [ 1 2 ]           [ 1 2 3s ]
     * 10. CommitClose(2)             [ 3s ]             [ 1 ]             [ 1 3s ]
     * 11. CancelClose(1)             [ 1 3s ]           -                 [ 1 3s ]
     * 12. CloseTab(3, allow undo)    [ 1s ]             [ 3 ]             [ 1s 3 ]
     * 13. CloseTab(1, allow undo)    -                  [ 1 3 ]           [ 1s 3 ]
     * 14. CommitAll                  -                  -                 -
     *
     */
    @Test
    @SmallTest
    public void testOutOfOrder2() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab3, new Tab[] {tab1}, fullList, tab3);

        // 3.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab0, tab2}, tab2, new Tab[] {tab3, tab1}, fullList, tab2);

        // 4.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab0, tab1, tab2}, tab2, new Tab[] {tab3}, fullList, tab2);

        // 5.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab1}, tab1, new Tab[] {tab2, tab3}, fullList, tab1);

        // 6.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0, tab2, tab3}, fullList, tab1);

        // 7.
        commitTabClosure(model, tab0);
        fullList = new Tab[] {tab1, tab2, tab3};
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab2, tab3}, fullList, tab1);

        // 8.
        cancelTabClosure(model, tab3);
        checkState(model, new Tab[] {tab1, tab3}, tab1, new Tab[] {tab2}, fullList, tab1);

        // 9.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab1, tab2}, fullList, tab3);

        // 10.
        commitTabClosure(model, tab2);
        fullList = new Tab[] {tab1, tab3};
        checkState(model, new Tab[] {tab3}, tab3, new Tab[] {tab1}, fullList, tab3);

        // 11.
        cancelTabClosure(model, tab1);
        checkState(model, new Tab[] {tab1, tab3}, tab3, sEmptyList, fullList, tab3);

        // 12.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab3}, fullList, tab1);

        // 13.
        closeTab(model, tab1, true);
        checkState(model, sEmptyList, null, new Tab[] {tab1, tab3}, fullList, tab1);

        // 14.
        commitAllTabClosures(model, new Tab[] {tab1, tab3});
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
    }

    /**
     * Test undo {@link TabModel#closeAllTabs()} with the following actions/expected states:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0  1 2 3s ]
     * 2.  CloseTab(1, allow undo)    [ 0 2 3s ]         [ 1 ]             [ 0  1 2 3s ]
     * 3.  CloseTab(2, allow undo)    [ 0 3s ]           [ 2 1 ]           [ 0  1 2 3s ]
     * 4.  CloseAll                   -                  [ 0 3 2 1 ]       [ 0s 1 2 3  ]
     * 5.  CancelAllClose             [ 0 1 2 3s ]       -                 [ 0  1 2 3s ]
     * 6.  CloseAll                   -                  [ 0 1 2 3 ]       [ 0s 1 2 3  ]
     * 7.  CommitAllClose             -                  -                 -
     * 8.  CreateTab(0)               [ 0s ]             -                 [ 0s ]
     * 9.  CloseAll                   -                  [ 0 ]             [ 0s ]
     *
     */
    @Test
    @SmallTest
    public void testCloseAll() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, fullList, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab3, new Tab[] {tab1}, fullList, tab3);

        // 3.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab3}, tab3, new Tab[] {tab1, tab2}, fullList, tab3);

        // 4.
        closeAllTabs(model);
        checkState(model, sEmptyList, null, fullList, fullList, tab0);

        // 5.
        cancelAllTabClosures(model, fullList);
        checkState(model, fullList, tab0, sEmptyList, fullList, tab0);

        // 6.
        closeAllTabs(model);
        checkState(model, sEmptyList, null, fullList, fullList, tab0);

        // 7.
        commitAllTabClosures(model, fullList);
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
        assertTrue(tab0.isClosing());
        assertTrue(tab1.isClosing());
        assertTrue(tab2.isClosing());
        assertTrue(tab3.isClosing());
        assertFalse(tab0.isInitialized());
        assertFalse(tab1.isInitialized());
        assertFalse(tab2.isInitialized());
        assertFalse(tab3.isInitialized());

        // 8.
        createTab(model, isIncognito);
        tab0 = model.getTabAt(0);
        fullList = new Tab[] {tab0};
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);

        // 9.
        closeAllTabs(model);
        checkState(model, sEmptyList, null, fullList, fullList, tab0);
        assertTrue(tab0.isClosing());
        assertTrue(tab0.isInitialized());
    }

    /**
     * Test {@link TabModel#closeTab(Tab)} when not allowing a close commits all pending
     * closes:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(1, allow undo)    [ 0 2 3s ]         [ 1 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(2, allow undo)    [ 0 3s ]           [ 2 1 ]           [ 0 1 2 3s ]
     * 4.  CloseTab(3, disallow undo) [ 0s ]             -                 [ 0s ]
     *
     *
     */
    @Test
    @SmallTest
    public void testCloseTab() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 3.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab3}, tab3, sEmptyList, fullList, tab3);

        // 4.
        closeTab(model, tab3, false);
        fullList = new Tab[] {tab0};
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);
        assertTrue(tab1.isClosing());
        assertTrue(tab2.isClosing());
        assertFalse(tab1.isInitialized());
        assertFalse(tab2.isInitialized());
    }

    /**
     * Test {@link TabModel#moveTab(int, int)} commits all pending closes:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(1, allow undo)    [ 0 2 3s ]         [ 1 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(2, allow undo)    [ 0 3s ]           [ 2 1 ]           [ 0 1 2 3s ]
     * 4.  MoveTab(0, 2)              [ 3s 0 ]           -                 [ 3s 0 ]
     *
     */
    @Test
    @SmallTest
    public void testMoveTab() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 3.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab3}, tab3, sEmptyList, fullList, tab3);

        // 4.
        model.moveTab(tab0.getId(), 2);
        fullList = new Tab[] {tab3, tab0};
        checkState(model, new Tab[] {tab3, tab0}, tab3, sEmptyList, fullList, tab3);
        assertTrue(tab1.isClosing());
        assertTrue(tab2.isClosing());
        assertFalse(tab1.isInitialized());
        assertFalse(tab1.isInitialized());
    }

    /**
     * Test adding a {@link Tab} to a {@link TabModel} commits all pending closes:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(1, allow undo)    [ 0 2 3s ]         [ 1 ]             [ 0 1 2 3s ]
     * 3.  CloseTab(2, allow undo)    [ 0 3s ]           [ 2 1 ]           [ 0 1 2 3s ]
     * 4.  CreateTab(4)               [ 0 3 4s ]         -                 [ 0 3 4s ]
     * 5.  CloseTab(0, allow undo)    [ 3 4s ]           [ 0 ]             [ 0 3 4s ]
     * 6.  CloseTab(3, allow undo)    [ 4s ]             [ 3 0 ]           [ 0 3 4s ]
     * 7.  CloseTab(4, allow undo)    -                  [ 4 3 0 ]         [ 0s 3 4 ]
     * 8.  CreateTab(5)               [ 5s ]             -                 [ 5s ]
     */
    @Test
    @SmallTest
    public void testAddTab() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 2.
        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab3, sEmptyList, fullList, tab3);

        // 3.
        closeTab(model, tab2, true);
        checkState(model, new Tab[] {tab0, tab3}, tab3, sEmptyList, fullList, tab3);

        // 4.
        createTab(model, isIncognito);
        Tab tab4 = model.getTabAt(2);
        fullList = new Tab[] {tab0, tab3, tab4};
        checkState(model, new Tab[] {tab0, tab3, tab4}, tab4, sEmptyList, fullList, tab4);
        assertTrue(tab1.isClosing());
        assertTrue(tab2.isClosing());
        assertFalse(tab1.isInitialized());
        assertFalse(tab2.isInitialized());

        // 5.
        closeTab(model, tab0, true);
        checkState(model, new Tab[] {tab3, tab4}, tab4, new Tab[] {tab0}, fullList, tab4);

        // 6.
        closeTab(model, tab3, true);
        checkState(model, new Tab[] {tab4}, tab4, new Tab[] {tab3, tab0}, fullList, tab4);

        // 7.
        closeTab(model, tab4, true);
        checkState(model, sEmptyList, null, new Tab[] {tab4, tab3, tab0}, fullList, tab0);

        // 8.
        createTab(model, isIncognito);
        Tab tab5 = model.getTabAt(0);
        fullList = new Tab[] {tab5};
        checkState(model, new Tab[] {tab5}, tab5, sEmptyList, fullList, tab5);
        assertTrue(tab0.isClosing());
        assertTrue(tab3.isClosing());
        assertTrue(tab4.isClosing());
        assertFalse(tab0.isInitialized());
        assertFalse(tab3.isInitialized());
        assertFalse(tab4.isInitialized());
    }

    /**
     * Test a {@link TabModel} where undo is not supported:
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3s ]       -                 [ 0 1 2 3s ]
     * 2.  CloseTab(1, allow undo)    [ 0 2 3s ]         -                 [ 0 2 3s ]
     * 3.  CloseAll                   -                  -                 -
     */
    @Test
    @SmallTest
    public void testUndoNotSupported() throws TimeoutException {
        final boolean isIncognito = true;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3};

        // 1.
        checkState(model, new Tab[] {tab0, tab1, tab2, tab3}, tab3, sEmptyList, fullList, tab3);
        assertFalse(model.supportsPendingClosures());

        // 2.
        closeTab(model, tab1, true);
        fullList = new Tab[] {tab0, tab2, tab3};
        checkState(model, new Tab[] {tab0, tab2, tab3}, tab3, sEmptyList, fullList, tab3);
        assertTrue(tab1.isClosing());
        assertFalse(tab1.isInitialized());

        // 3.
        closeAllTabs(model);
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
        assertTrue(tab0.isClosing());
        assertTrue(tab2.isClosing());
        assertTrue(tab3.isClosing());
        assertFalse(tab0.isInitialized());
        assertFalse(tab2.isInitialized());
        assertFalse(tab3.isInitialized());
    }

    /**
     * Test a {@link TabModel} where undo is not supported and
     * {@link TabModelObserver#onFinishingMultipleTabClosure()} is called.
     *     Action                     Model List         Close List        Comprehensive List
     * 1.  Initial State              [ 0 1 2 3 4s ]     -                 [ 0 1 2 3 4s ]
     * 2.  CloseTab(1)                [ 0 2 3 4s ]       -                 [ 0 2 3 4s ]
     * 3.  CloseMultipleTabs(2, 4)    [ 0 3s ]           -                 [ 0 3s ]
     * 4.  CloseAll                   -                  -                 -
     */
    @Test
    @SmallTest
    public void testUndoNotSupportedOnFinishingMultipleTabClosure() throws TimeoutException {
        final boolean isIncognito = true;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab tab2 = model.getTabAt(2);
        Tab tab3 = model.getTabAt(3);
        Tab tab4 = model.getTabAt(4);

        Tab[] fullList = new Tab[] {tab0, tab1, tab2, tab3, tab4};

        // 1.
        checkState(model, fullList, tab4, sEmptyList, fullList, tab4);
        assertFalse(model.supportsPendingClosures());

        final ArrayList<Tab> lastClosedTabs = new ArrayList<Tab>();
        model.addObserver(
                new TabModelObserver() {
                    @Override
                    public void onFinishingMultipleTabClosure(List<Tab> tabs, boolean canRestore) {
                        lastClosedTabs.clear();
                        lastClosedTabs.addAll(tabs);
                    }
                });

        // 2.
        closeTab(model, tab1, true);
        fullList = new Tab[] {tab0, tab2, tab3, tab4};
        checkState(model, fullList, tab4, sEmptyList, fullList, tab4);
        assertTrue(tab1.isClosing());
        assertFalse(tab1.isInitialized());
        assertArrayEquals(new Tab[] {tab1}, lastClosedTabs.toArray(new Tab[0]));

        // 3.
        closeMultipleTabs(model, Arrays.asList(new Tab[] {tab2, tab4}), true);
        fullList = new Tab[] {tab0, tab3};
        checkState(model, fullList, tab0, sEmptyList, fullList, tab0);
        assertTrue(tab2.isClosing());
        assertTrue(tab4.isClosing());
        assertFalse(tab2.isInitialized());
        assertFalse(tab4.isInitialized());
        assertArrayEquals(new Tab[] {tab2, tab4}, lastClosedTabs.toArray(new Tab[0]));

        // 4.
        closeAllTabs(model);
        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
        assertTrue(tab0.isClosing());
        assertTrue(tab3.isClosing());
        assertFalse(tab0.isInitialized());
        assertFalse(tab3.isInitialized());
        assertArrayEquals(new Tab[] {tab0, tab3}, lastClosedTabs.toArray(new Tab[0]));
    }

    /** Test opening recently closed tabs using the rewound list in Java. */
    @Test
    @SmallTest
    public void testOpenRecentlyClosedTab() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        createTab(model, isIncognito);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab tab1 = model.getTabAt(1);
        Tab[] allTabs = new Tab[] {tab0, tab1};

        closeTab(model, tab1, true);
        checkState(model, new Tab[] {tab0}, tab0, new Tab[] {tab1}, allTabs, tab0);

        // Ensure tab recovery, and reuse of {@link Tab} objects in Java.
        model.openMostRecentlyClosedEntry();
        checkState(model, allTabs, tab0, sEmptyList, allTabs, tab0);
    }

    @Test
    @SmallTest
    public void testActiveModelCloseAndUndoForTabSupplier() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        assertEquals(0, model.getTabCountSupplier().get().intValue());
        createTab(model, isIncognito);
        model.getCurrentTabSupplier().addObserver(mTabSupplierObserver);
        ShadowLooper.runUiThreadTasks();

        Tab tab0 = model.getTabAt(0);
        Tab[] fullList = new Tab[] {tab0};

        assertEquals(tab0, model.getCurrentTabSupplier().get());
        verify(mTabSupplierObserver).onResult(eq(tab0));
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);
        assertEquals(1, model.getTabCountSupplier().get().intValue());

        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0}, fullList, tab0);
        assertNull(model.getCurrentTabSupplier().get());
        verify(mTabSupplierObserver).onResult(isNull());
        assertEquals(0, model.getTabCountSupplier().get().intValue());

        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);
        assertEquals(tab0, model.getCurrentTabSupplier().get());
        verify(mTabSupplierObserver, times(2)).onResult(eq(tab0));
        assertEquals(1, model.getTabCountSupplier().get().intValue());
    }

    @Test
    @SmallTest
    public void testInactiveModelCloseAndUndoForTabSupplier() throws TimeoutException {
        final boolean isIncognito = false;
        final TabModel model = createTabModel(isIncognito);
        assertEquals(0, model.getTabCountSupplier().get().intValue());
        model.getCurrentTabSupplier().addObserver(mTabSupplierObserver);
        model.setActive(false);
        createTab(model, isIncognito);

        Tab tab0 = model.getTabAt(0);
        Tab[] fullList = new Tab[] {tab0};

        assertEquals(tab0, model.getCurrentTabSupplier().get());
        verify(mTabSupplierObserver).onResult(eq(tab0));
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);
        assertEquals(1, model.getTabCountSupplier().get().intValue());

        closeTab(model, tab0, true);
        checkState(model, sEmptyList, null, new Tab[] {tab0}, fullList, tab0);
        assertNull(model.getCurrentTabSupplier().get());
        verify(mTabSupplierObserver).onResult(isNull());
        assertEquals(0, model.getTabCountSupplier().get().intValue());

        cancelTabClosure(model, tab0);
        checkState(model, new Tab[] {tab0}, tab0, sEmptyList, fullList, tab0);
        assertEquals(tab0, model.getCurrentTabSupplier().get());
        verify(mTabSupplierObserver, times(2)).onResult(eq(tab0));
        assertEquals(1, model.getTabCountSupplier().get().intValue());
    }
}