chromium/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/PendingTabClosureManagerTest.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.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.spy;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.Tab;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/** Unit tests for {@link PendingTabClosureManager}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class PendingTabClosureManagerTest {
    private PendingTabClosureManager mPendingTabClosureManager;

    private class FakeTabModel extends EmptyTabModel {
        private LinkedList<Tab> mTabs = new LinkedList<Tab>();
        private int mIndex = TabModel.INVALID_TAB_INDEX;

        public FakeTabModel() {}

        public void setTabs(Tab[] tabs) {
            mTabs = new LinkedList<Tab>(Arrays.asList(tabs));
        }

        public void clear() {
            mTabs.clear();
        }

        public void insertUndoneTabClosureAt(Tab tab, int insertIndex) {
            if (mIndex >= insertIndex) mIndex++;
            mTabs.add(insertIndex, tab);

            if (mIndex == INVALID_TAB_INDEX) {
                mIndex = insertIndex;
            }
        }

        @Override
        public int getCount() {
            return mTabs.size();
        }

        @Override
        public Tab getTabAt(int position) {
            return mTabs.get(position);
        }

        @Override
        public boolean isIncognito() {
            return false;
        }

        @Override
        public int indexOf(Tab tab) {
            return mTabs.indexOf(tab);
        }

        @Override
        public int index() {
            return mIndex;
        }

        @Override
        public boolean supportsPendingClosures() {
            return true;
        }
    }

    private class PendingClosureDelegate
            implements PendingTabClosureManager.PendingTabClosureDelegate {
        @Override
        public void insertUndoneTabClosureAt(Tab tab, int index) {
            mTabModel.insertUndoneTabClosureAt(tab, index);
        }

        @Override
        public void finalizeClosure(Tab tab) {}

        @Override
        public void notifyAllTabsClosureUndone() {}

        @Override
        public void notifyOnFinishingMultipleTabClosure(List<Tab> tabs) {}
    }

    FakeTabModel mTabModel;
    @Mock PendingClosureDelegate mDelegate;
    @Mock Profile mProfile;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mTabModel = new FakeTabModel();
        mDelegate = spy(new PendingClosureDelegate());
        mPendingTabClosureManager = new PendingTabClosureManager(mTabModel, mDelegate);
    }

    @After
    public void tearDown() {
        mPendingTabClosureManager.destroy();
    }

    private void setupRewoundState(PendingTabClosureManager manager, Tab[] tabs) {
        manager.commitAllTabClosures();
        mTabModel.setTabs(tabs);
        manager.resetState();

        // Simulate initiating closing all tabs in the tab model.
        mTabModel.clear();
    }

    private void checkRewoundState(
            PendingTabClosureManager manager, Tab[] tabs, boolean ignoreClosing) {
        TabList rewoundList = manager.getRewoundList();
        Assert.assertEquals("Tab count not matching", tabs.length, rewoundList.getCount());
        for (int i = 0; i < tabs.length; i++) {
            if (!ignoreClosing) {
                Assert.assertTrue(manager.isClosurePending(tabs[i].getId()));
            }
            Assert.assertEquals(
                    "Tab at index " + Integer.toString(i) + " doesn't match.",
                    tabs[i],
                    rewoundList.getTabAt(i));
        }
    }

    /** Test that committing a single pending tab closure works. */
    @Test
    public void testCommitSingleTabEvent() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab[] tabList = new Tab[] {tab0};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(tabList));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.commitTabClosure(tab0.getId());
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Arrays.asList(tabList)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab0));
        checkRewoundState(mPendingTabClosureManager, new Tab[] {}, false);
    }

    /** Test that cancelling a single pending tab closure works. */
    @Test
    public void testCancelSingleTabEvent() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab[] tabList = new Tab[] {tab0};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(tabList));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.cancelTabClosure(tab0.getId());
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab0), eq(0));
        // Still in rewound state as the tab continues to exist.
        checkRewoundState(mPendingTabClosureManager, tabList, true);
    }

    /**
     * Test that committing a pending multiple tab closure works and is deferred until all tabs
     * commit.
     */
    @Test
    public void testCommitMultipleTabEvent() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab[] tabList = new Tab[] {tab1, tab0};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(tabList));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.commitTabClosure(tab0.getId());
        // No commits actually occur until later.
        checkRewoundState(mPendingTabClosureManager, tabList, false);
        mPendingTabClosureManager.commitTabClosure(tab1.getId());
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Arrays.asList(tabList)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab1));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab0));
        checkRewoundState(mPendingTabClosureManager, new Tab[] {}, false);
    }

    /** Test that cancelling a pending multiple tab closure works and happens immediately. */
    @Test
    public void testCancelMultipleTabEvent() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab[] tabList = new Tab[] {tab1, tab0};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(tabList));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.cancelTabClosure(tab0.getId());
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab0), eq(0));
        checkRewoundState(mPendingTabClosureManager, tabList, true);

        mPendingTabClosureManager.cancelTabClosure(tab1.getId());
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab1), eq(0));
        checkRewoundState(mPendingTabClosureManager, tabList, true);
    }

    @Test
    public void testPartialCommitThenCancel() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab[] tabList = new Tab[] {tab1, tab0};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(tabList));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.commitTabClosure(tab0.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.cancelTabClosure(tab1.getId());
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab1), eq(0));
        delegateInOrder.verify(mDelegate).notifyOnFinishingMultipleTabClosure(eq(List.of(tab0)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab0));
        checkRewoundState(mPendingTabClosureManager, new Tab[] {tab1}, false);
    }

    @Test
    public void testPartialCancelThenCommit() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab[] tabList = new Tab[] {tab1, tab0};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(tabList));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.cancelTabClosure(tab0.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, true);
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab0), eq(0));

        mPendingTabClosureManager.commitTabClosure(tab1.getId());
        delegateInOrder.verify(mDelegate).notifyOnFinishingMultipleTabClosure(eq(List.of(tab1)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab1));
        checkRewoundState(mPendingTabClosureManager, new Tab[] {tab0}, false);
    }

    @Test
    public void testCommitAndCancelMultipleEventsOutOfOrder() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab tab2 = new MockTab(2, mProfile);
        Tab tab3 = new MockTab(3, mProfile);
        Tab tab4 = new MockTab(4, mProfile);
        Tab[] tabList = new Tab[] {tab0, tab1, tab2, tab3, tab4};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Collections.singletonList(tab0));
        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(new Tab[] {tab2, tab4}));
        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(new Tab[] {tab1, tab3}));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.cancelTabClosure(tab3.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, true);
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab3), eq(0));

        mPendingTabClosureManager.commitTabClosure(tab2.getId());
        // No commits actually occur until all tabs are committed.
        checkRewoundState(mPendingTabClosureManager, tabList, true);

        tabList = new Tab[] {tab0, tab1, tab3};
        mPendingTabClosureManager.commitTabClosure(tab4.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, true);
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Arrays.asList(new Tab[] {tab2, tab4})));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab2));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab4));

        mPendingTabClosureManager.cancelTabClosure(tab1.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, true);
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab1), eq(0));

        mPendingTabClosureManager.commitTabClosure(tab0.getId());
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Collections.singletonList(tab0)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab0));
        checkRewoundState(mPendingTabClosureManager, new Tab[] {tab1, tab3}, true);
    }

    /**
     * Test that {@link PendingTabClosureManager#commitAllTabClosures()} commits all tabs queued
     * except for tabs that have been undone already.
     */
    @Test
    public void testCommitAllClosures() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab tab2 = new MockTab(2, mProfile);
        Tab tab3 = new MockTab(3, mProfile);
        Tab tab4 = new MockTab(4, mProfile);
        Tab tab5 = new MockTab(5, mProfile);
        Tab[] tabList = new Tab[] {tab0, tab1, tab2, tab3, tab4, tab5};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Collections.singletonList(tab0));
        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(new Tab[] {tab1, tab4}));
        mPendingTabClosureManager.addTabClosureEvent(Collections.singletonList(tab2));
        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(new Tab[] {tab3, tab5}));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.commitTabClosure(tab1.getId());
        // No commits actually occur until all tabs are committed.
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        tabList = new Tab[] {tab0, tab1, tab3, tab4, tab5};
        // Fully close tab 2.
        mPendingTabClosureManager.commitTabClosure(tab2.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, false);
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Collections.singletonList(tab2)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab2));

        // Restore tab 5.
        mPendingTabClosureManager.cancelTabClosure(tab5.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, true);
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab5), eq(0));

        mPendingTabClosureManager.commitAllTabClosures();
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Collections.singletonList(tab0)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab0));
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Arrays.asList(new Tab[] {tab1, tab4})));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab1));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab4));
        delegateInOrder
                .verify(mDelegate)
                .notifyOnFinishingMultipleTabClosure(eq(Collections.singletonList(tab3)));
        delegateInOrder.verify(mDelegate).finalizeClosure(eq(tab3));
        checkRewoundState(mPendingTabClosureManager, new Tab[] {tab5}, true);
    }

    /**
     * Test that {@link PendingTabClosureManager#openMostRecentlyClosedEntry()} undoes all tabs in
     * the most recent event even if some are ready to commit.
     */
    @Test
    public void testOpenMostRecentlyClosedWithCommit() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab tab2 = new MockTab(2, mProfile);
        Tab tab3 = new MockTab(3, mProfile);
        Tab[] tabList = new Tab[] {tab0, tab1, tab2, tab3};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Collections.singletonList(tab0));
        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(new Tab[] {tab1, tab2, tab3}));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        mPendingTabClosureManager.commitTabClosure(tab1.getId());
        // No commits actually occur until all tabs are committed.
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        // Restore entry as a whole.
        mPendingTabClosureManager.openMostRecentlyClosedEntry();
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab1), eq(0));
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab2), eq(1));
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab3), eq(2));
        checkRewoundState(mPendingTabClosureManager, tabList, true);
    }

    /**
     * Test that {@link PendingTabClosureManager#openMostRecentlyClosedEntry()} undoes all tabs in
     * the most recent event except those already undone..
     */
    @Test
    public void testOpenMostRecentlyClosedWithClose() {
        InOrder delegateInOrder = inOrder(mDelegate);
        Tab tab0 = new MockTab(0, mProfile);
        Tab tab1 = new MockTab(1, mProfile);
        Tab tab2 = new MockTab(2, mProfile);
        Tab tab3 = new MockTab(3, mProfile);
        Tab[] tabList = new Tab[] {tab0, tab1, tab2, tab3};
        setupRewoundState(mPendingTabClosureManager, tabList);

        mPendingTabClosureManager.addTabClosureEvent(Collections.singletonList(tab0));
        mPendingTabClosureManager.addTabClosureEvent(Arrays.asList(new Tab[] {tab1, tab2, tab3}));
        checkRewoundState(mPendingTabClosureManager, tabList, false);

        // Restore tab 2.
        mPendingTabClosureManager.cancelTabClosure(tab2.getId());
        checkRewoundState(mPendingTabClosureManager, tabList, true);
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab2), eq(0));

        // Restore entry as a whole.
        mPendingTabClosureManager.openMostRecentlyClosedEntry();
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab1), eq(0));
        delegateInOrder.verify(mDelegate).insertUndoneTabClosureAt(eq(tab3), eq(2));
        checkRewoundState(mPendingTabClosureManager, tabList, true);
    }
}