chromium/chrome/android/junit/src/org/chromium/chrome/browser/tab/tab_restore/HistoricalTabModelObserverUnitTest.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.tab.tab_restore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.content.Context;

import androidx.annotation.Nullable;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.ContextUtils;
import org.chromium.base.Token;
import org.chromium.base.supplier.LazyOneshotSupplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
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.tabmodel.TabModel;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.components.tab_groups.TabGroupColorId;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** Unit tests for {@link HistoricalTabModelObserver}. */
@RunWith(BaseRobolectricTestRunner.class)
@EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
public class HistoricalTabModelObserverUnitTest {
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    private Context mContext;
    @Mock private TabGroupModelFilter mTabGroupModelFilter;
    @Mock private TabModel mTabModel;
    @Mock private Profile mProfile;
    @Mock private HistoricalTabSaver mHistoricalTabSaver;

    private HistoricalTabModelObserver mObserver;

    @Before
    public void setUp() {
        when(mTabGroupModelFilter.isTabGroupHiding(any())).thenReturn(false);
        when(mTabGroupModelFilter.isTabInTabGroup(any())).thenReturn(false);
        when(mTabGroupModelFilter.getRelatedTabCountForRootId(anyInt())).thenReturn(-1);
        when(mTabGroupModelFilter.getTabModel()).thenReturn(mTabModel);
        when(mTabModel.getComprehensiveModel()).thenReturn(mTabModel);

        mObserver = new HistoricalTabModelObserver(mTabGroupModelFilter, mHistoricalTabSaver);
        verify(mTabGroupModelFilter).addObserver(mObserver);

        mContext = spy(ContextUtils.getApplicationContext());
        ContextUtils.initApplicationContextForTests(mContext);
    }

    @After
    public void tearDown() {
        mObserver.destroy();
        verify(mTabGroupModelFilter).removeObserver(mObserver);
    }

    @Test
    public void testEmpty() {
        mObserver.onFinishingMultipleTabClosure(new ArrayList<>(), /* canRestore= */ true);

        verifyNoMoreInteractions(mHistoricalTabSaver);
    }

    @Test
    public void testMultipleTabs_NotRestorable() {
        MockTab mockTab = createMockTab(0);

        mObserver.onFinishingMultipleTabClosure(
                Collections.singletonList(mockTab), /* canRestore= */ false);

        verifyNoMoreInteractions(mHistoricalTabSaver);
    }

    @Test
    public void testSingleTab() {
        MockTab mockTab = createMockTab(0);

        mObserver.onFinishingMultipleTabClosure(
                Collections.singletonList(mockTab), /* canRestore= */ true);

        verify(mHistoricalTabSaver).createHistoricalTab(eq(mockTab));
    }

    @Test
    public void testTabGroupWithSingleTab_NotUndoable() {
        int rootId = 123;
        MockTab mockTab = createMockTab(rootId);
        Token tabGroupId = new Token(1L, 2L);
        String title = "bar";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab});

        mObserver.onFinishingMultipleTabClosure(
                Collections.singletonList(mockTab), /* canRestore= */ true);

        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();

        assertEquals(1, entries.size());
        HistoricalEntry group = entries.get(0);
        assertFalse(group.isSingleTab());
        assertEquals(1, group.getTabs().size());
        assertEquals(rootId, group.getRootId());
        assertEquals(tabGroupId, group.getTabGroupId());
        assertEquals(title, group.getGroupTitle());
        assertEquals(color, group.getGroupColor());
        assertEquals(mockTab, group.getTabs().get(0));
    }

    @Test
    public void testTabGroupWithSingleTab_Undoable() {
        int rootId = 123;
        MockTab mockTab = createMockTab(rootId);
        Token tabGroupId = new Token(1L, 2L);
        String title = "bar";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab});
        when(mTabGroupModelFilter.getRelatedTabCountForRootId(rootId)).thenReturn(1);
        when(mTabGroupModelFilter.tabGroupExistsForRootId(rootId)).thenReturn(false);
        when(mTabGroupModelFilter.isTabInTabGroup(mockTab)).thenReturn(false);

        mObserver.onFinishingMultipleTabClosure(
                Collections.singletonList(mockTab), /* canRestore= */ true);

        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();

        assertEquals(1, entries.size());
        HistoricalEntry group = entries.get(0);
        assertFalse(group.isSingleTab());
        assertEquals(1, group.getTabs().size());
        assertEquals(rootId, group.getRootId());
        assertEquals(tabGroupId, group.getTabGroupId());
        assertEquals(title, group.getGroupTitle());
        assertEquals(color, group.getGroupColor());
        assertEquals(mockTab, group.getTabs().get(0));
    }

    @Test
    public void testMultipleTabs() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);
        MockTab mockTab2 = createMockTab(2);

        Tab[] tabList = new Tab[] {mockTab0, mockTab1, mockTab2};
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();
        assertEquals(tabList.length, entries.size());
        for (int i = 0; i < tabList.length; i++) {
            HistoricalEntry entry = entries.get(i);
            assertEquals(1, entry.getTabs().size());
            assertEquals(tabList[i], entry.getTabs().get(0));
        }
    }

    @Test
    public void testSubsetOfGroup() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);
        MockTab mockTab2 = createMockTab(2);

        final String title = "foo";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        MockTab[] tabList = new MockTab[] {mockTab0, mockTab1, mockTab2};
        Token tabGroupId = new Token(1L, 243L);
        createGroup(tabGroupId, title, color, tabList);

        List<Tab> closingTabList = List.of(mockTab1, mockTab2);
        mObserver.onFinishingMultipleTabClosure(closingTabList, /* canRestore= */ true);

        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();

        assertEquals(2, entries.size());
        HistoricalEntry entryTab1 = entries.get(0);
        HistoricalEntry entryTab2 = entries.get(1);
        assertTrue(entryTab1.isSingleTab());
        assertTrue(entryTab2.isSingleTab());
        assertEquals(mockTab1, entryTab1.getTabs().get(0));
        assertEquals(mockTab2, entryTab2.getTabs().get(0));
    }

    @Test
    public void testSingleGroup() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);
        MockTab mockTab2 = createMockTab(2);

        final String title = "foo";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        MockTab[] tabList = new MockTab[] {mockTab0, mockTab1, mockTab2};
        Token tabGroupId = new Token(1L, 243L);
        final int rootId = createGroup(tabGroupId, title, color, tabList);

        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        // HistoricalTabModelObserver relies on HistoricalTabSaver to simplify to a single group
        // entry.
        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();

        assertEquals(1, entries.size());
        HistoricalEntry group = entries.get(0);
        assertFalse(group.isSingleTab());
        assertEquals(tabList.length, group.getTabs().size());
        assertEquals(rootId, group.getRootId());
        assertEquals(tabGroupId, group.getTabGroupId());
        assertEquals(title, group.getGroupTitle());
        assertEquals(color, group.getGroupColor());
        for (int i = 0; i < tabList.length; i++) {
            assertEquals(tabList[i], group.getTabs().get(i));
        }
    }

    @Test
    public void testSingleTabInGroup() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);

        final String title = "foo";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        Token tabGroupId = new Token(3L, 4L);
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab1});

        MockTab[] tabList = new MockTab[] {mockTab0, mockTab1};
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        // HistoricalTabModelObserver relies on HistoricalTabSaver to simplify to a single tab
        // entry.
        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();
        assertEquals(2, entries.size());

        HistoricalEntry tab0 = entries.get(0);
        assertEquals(1, tab0.getTabs().size());
        assertEquals(mockTab0, tab0.getTabs().get(0));

        HistoricalEntry tab1 = entries.get(1);
        assertEquals(1, tab1.getTabs().size());
        assertEquals(tabGroupId, tab1.getTabGroupId());
        assertEquals(mockTab1, tab1.getTabs().get(0));
    }

    @Test
    public void testTabGroupHiding() {
        MockTab mockTab0 = createMockTab(0);

        final String title = "foo";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        Token tabGroupId = new Token(3L, 4L);
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab0});
        when(mTabGroupModelFilter.isTabGroupHiding(tabGroupId)).thenReturn(true);
        when(mTabGroupModelFilter.getLazyAllTabGroupIdsInComprehensiveModel(any()))
                .thenReturn(LazyOneshotSupplier.fromValue(new HashSet<>()));

        MockTab[] tabList = new MockTab[] {mockTab0};
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        // HistoricalTabModelObserver relies on HistoricalTabSaver to simplify to a single tab
        // entry.
        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();
        assertEquals(0, entries.size());
    }

    @Test
    public void testTabGroupHidingTwoPhases_SingleTabs() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);

        final String title = "foo";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        Token tabGroupId = new Token(3L, 4L);
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab0, mockTab1});
        when(mTabGroupModelFilter.isTabGroupHiding(tabGroupId)).thenReturn(true);
        when(mTabGroupModelFilter.getLazyAllTabGroupIdsInComprehensiveModel(any()))
                .thenReturn(LazyOneshotSupplier.fromValue(Set.of(tabGroupId)));

        MockTab[] tabList = new MockTab[] {mockTab0};
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        // Close tab 0 first, even though the group is hiding there are still other tabs in the
        // comprehensive model for the group so treat as a separate closure.
        verify(mHistoricalTabSaver).createHistoricalTab(eq(mockTab0));

        // Close tab 1, since it is part of the last event "closing" the tab group and it is hiding
        // the group so no entry should be created.
        tabList = new MockTab[] {mockTab1};
        createGroup(tabGroupId, title, color, tabList);
        when(mTabGroupModelFilter.getLazyAllTabGroupIdsInComprehensiveModel(any()))
                .thenReturn(LazyOneshotSupplier.fromValue(new HashSet<>()));
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), true);

        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();
        assertEquals(0, entries.size());
    }

    @Test
    public void testTabGroupHidingTwoPhases_SingleTabThenTwo() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);
        MockTab mockTab2 = createMockTab(2);

        final String title = "foo";
        @TabGroupColorId int color = TabGroupColorId.GREY;
        Token tabGroupId = new Token(3L, 4L);
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab0, mockTab1, mockTab2});
        when(mTabGroupModelFilter.isTabGroupHiding(tabGroupId)).thenReturn(true);
        when(mTabGroupModelFilter.getLazyAllTabGroupIdsInComprehensiveModel(any()))
                .thenReturn(LazyOneshotSupplier.fromValue(Set.of(tabGroupId)));

        MockTab[] tabList = new MockTab[] {mockTab0};
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        // Close tab 0 first, even though the group is hiding there are still other tabs in the
        // comprehensive model for the group so treat as a separate closure.
        verify(mHistoricalTabSaver).createHistoricalTab(eq(mockTab0));

        // Close tab 1, and tab 2, since it is part of the last event "closing" the tab group and it
        // is hiding the group so no entry should be created.
        tabList = new MockTab[] {mockTab1, mockTab2};
        createGroup(tabGroupId, title, color, new MockTab[] {mockTab1, mockTab2});
        when(mTabGroupModelFilter.getLazyAllTabGroupIdsInComprehensiveModel(any()))
                .thenReturn(LazyOneshotSupplier.fromValue(new HashSet<>()));
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), true);

        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();
        assertEquals(0, entries.size());
    }

    @Test
    public void testMultipleTabsAndGroups() {
        MockTab mockTab0 = createMockTab(0);
        MockTab mockTab1 = createMockTab(1);
        MockTab mockTab2 = createMockTab(2);
        MockTab mockTab3 = createMockTab(3);
        MockTab mockTab4 = createMockTab(4);
        MockTab mockTab5 = createMockTab(5);

        final String groupTitle1 = "foo";
        @TabGroupColorId int groupColor1 = TabGroupColorId.GREY;
        MockTab[] groupTabs1 = new MockTab[] {mockTab3, mockTab5};
        Token tabGroupId1 = new Token(3L, 4L);
        final int rootId1 = createGroup(tabGroupId1, groupTitle1, groupColor1, groupTabs1);

        final String groupTitle2 = "Bar";
        @TabGroupColorId int groupColor2 = TabGroupColorId.BLUE;
        MockTab[] groupTabs2 = new MockTab[] {mockTab1, mockTab2};
        Token tabGroupId2 = new Token(3L, 4L);
        final int rootId2 = createGroup(tabGroupId2, groupTitle2, groupColor2, groupTabs2);

        Tab[] tabList = new Tab[] {mockTab0, mockTab2, mockTab3, mockTab4, mockTab1, mockTab5};
        mObserver.onFinishingMultipleTabClosure(Arrays.asList(tabList), /* canRestore= */ true);

        // HistoricalTabModelObserver relies on HistoricalTabSaver to simplify to a single group
        // entry.
        ArgumentCaptor<List<HistoricalEntry>> arg = ArgumentCaptor.forClass((Class) List.class);
        verify(mHistoricalTabSaver).createHistoricalBulkClosure(arg.capture());
        List<HistoricalEntry> entries = arg.getValue();
        assertEquals(4, entries.size());

        // First tab in list is ungrouped mockTab0.
        HistoricalEntry historicalTab0 = entries.get(0);
        assertEquals(1, historicalTab0.getTabs().size());
        assertEquals(mockTab0, historicalTab0.getTabs().get(0));

        // Second tab in list is mockTab2 for tabGroup2. This grabs mockTab1 to this position.
        HistoricalEntry historicalGroup2 = entries.get(1);
        assertFalse(historicalGroup2.isSingleTab());
        assertEquals(2, historicalGroup2.getTabs().size());
        assertEquals(rootId2, historicalGroup2.getRootId());
        assertEquals(tabGroupId2, historicalGroup2.getTabGroupId());
        assertEquals(groupTitle2, historicalGroup2.getGroupTitle());
        assertEquals(groupColor2, historicalGroup2.getGroupColor());
        assertEquals(mockTab2, historicalGroup2.getTabs().get(0));
        assertEquals(mockTab1, historicalGroup2.getTabs().get(1));

        // Third tab in list is mockTab3 for tabGroup1. This grabs mockTab5 to this position.
        HistoricalEntry historicalGroup1 = entries.get(2);
        assertFalse(historicalGroup1.isSingleTab());
        assertEquals(2, historicalGroup1.getTabs().size());
        assertEquals(rootId1, historicalGroup1.getRootId());
        assertEquals(tabGroupId1, historicalGroup1.getTabGroupId());
        assertEquals(groupTitle1, historicalGroup1.getGroupTitle());
        assertEquals(groupColor1, historicalGroup1.getGroupColor());
        assertEquals(mockTab3, historicalGroup1.getTabs().get(0));
        assertEquals(mockTab5, historicalGroup1.getTabs().get(1));

        // Fourth tab in list is ungrouped mockTab4.
        HistoricalEntry historicalTab4 = entries.get(3);
        assertEquals(1, historicalTab4.getTabs().size());
        assertEquals(mockTab4, historicalTab4.getTabs().get(0));
    }

    private MockTab createMockTab(int id) {
        MockTab mockTab = new MockTab(id, mProfile);
        mockTab.setRootId(id);
        return mockTab;
    }

    /**
     * Creates a group.
     *
     * @param tabGroupId The tab group id.
     * @param title Group title.
     * @param color Group color.
     * @param tabList List of tabs in group.
     * @return ID of the group.
     */
    private int createGroup(
            @Nullable Token tabGroupId,
            @Nullable String title,
            @TabGroupColorId int color,
            MockTab[] tabList) {
        assert tabList.length != 0;

        final int rootId = tabList[0].getId();
        when(mTabGroupModelFilter.getTabGroupTitle(rootId)).thenReturn(title);
        when(mTabGroupModelFilter.getTabGroupColorWithFallback(rootId)).thenReturn(color);
        when(mTabGroupModelFilter.getRelatedTabCountForRootId(rootId)).thenReturn(tabList.length);
        when(mTabGroupModelFilter.tabGroupExistsForRootId(rootId)).thenReturn(true);
        for (MockTab tab : tabList) {
            tab.setRootId(rootId);
            tab.setTabGroupId(tabGroupId);
            when(mTabGroupModelFilter.isTabInTabGroup(tab)).thenReturn(true);
        }
        return rootId;
    }
}