[TabGroupModelFilter] Add mergeTabsToGroup API
This CL adds an API mergeTabsToGroup to TabGroupModelFilter. This API
allows users to create group from existed tabs or groups.
BUG=963692
(cherry picked from commit 9772e48e079107ab4931dae5cdafe1c82813c57a)
Change-Id: I04d2610e169d4c68041b9ad542038f384e9cf40e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1632838
Commit-Queue: Mei Liang <meiliang@chromium.org>
Reviewed-by: Yusuf Ozuysal <yusufo@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#665351}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1656711
Cr-Commit-Position: refs/branch-heads/3809@{#270}
Cr-Branched-From: d82dec1a818f378c464ba307ddd9c92133eac355-refs/heads/master@{#665002}
diff --git a/chrome/android/chrome_junit_test_java_sources.gni b/chrome/android/chrome_junit_test_java_sources.gni
index aa7afa4..aae8b87 100644
--- a/chrome/android/chrome_junit_test_java_sources.gni
+++ b/chrome/android/chrome_junit_test_java_sources.gni
@@ -184,6 +184,7 @@
"junit/src/org/chromium/chrome/browser/tabstate/TabStateUnitTest.java",
"junit/src/org/chromium/chrome/browser/tasks/EngagementTimeUtilTest.java",
"junit/src/org/chromium/chrome/browser/tasks/JourneyManagerTest.java",
+ "junit/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilterUnitTest.java",
"junit/src/org/chromium/chrome/browser/toolbar/ToolbarSecurityIconTest.java",
"junit/src/org/chromium/chrome/browser/usage_stats/EventTrackerTest.java",
"junit/src/org/chromium/chrome/browser/usage_stats/PageViewObserverTest.java",
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
index 7e17b3e..7951ff3 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
@@ -340,6 +340,9 @@
instanceof TabGroupModelFilter) {
mTabGroupObserver = new TabGroupModelFilter.Observer() {
@Override
+ public void didMergeTabToGroup(Tab movedTab, int selectedTabIdInGroup) {}
+
+ @Override
public void didMoveWithinGroup(
Tab movedTab, int tabModelOldIndex, int tabModelNewIndex) {
int curPosition = mModel.indexFromId(movedTab.getId());
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/EmptyTabModelFilter.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/EmptyTabModelFilter.java
index f2066924..3ed6e46 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/EmptyTabModelFilter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/EmptyTabModelFilter.java
@@ -28,6 +28,9 @@
@Override
protected void reorder() {}
+ @Override
+ protected void resetFilterStateInternal() {}
+
// TabList implementation.
@Override
public boolean isIncognito() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelFilter.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelFilter.java
index 16823987..cfacc71 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelFilter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelFilter.java
@@ -110,6 +110,25 @@
*/
protected abstract void reorder();
+ /**
+ * Concrete class requires to define what to clean up.
+ */
+ protected abstract void resetFilterStateInternal();
+
+ /**
+ * Calls {@code resetFilterStateInternal} method to clean up filter internal data, and resets
+ * the internal data based on the current {@link TabModel}.
+ */
+ protected void resetFilterState() {
+ resetFilterStateInternal();
+
+ TabModel tabModel = getTabModel();
+ for (int i = 0; i < tabModel.getCount(); i++) {
+ Tab tab = tabModel.getTabAt(i);
+ addTab(tab);
+ }
+ }
+
// TODO(crbug.com/948518): This is a band-aid fix for not crashing when undo the last closed
// tab, should remove later.
/**
@@ -161,7 +180,6 @@
@Override
public void didMoveTab(Tab tab, int newIndex, int curIndex) {
- reorder();
for (TabModelObserver observer : mFilteredObservers) {
observer.didMoveTab(tab, newIndex, curIndex);
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilter.java b/chrome/android/java/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilter.java
index bec2314..860b12a4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilter.java
@@ -48,6 +48,15 @@
*/
public interface Observer {
/**
+ * This method is called after a tab is moved to form a group or moved into an existed
+ * group.
+ * @param movedTab The {@link Tab} which has been moved. If a group is merged to a tab or
+ * another group, this is the last tab of the merged group.
+ * @param selectedTabIdInGroup The id of the selected {@link Tab} in group.
+ */
+ void didMergeTabToGroup(Tab movedTab, int selectedTabIdInGroup);
+
+ /**
* This method is called after a group is moved.
*
* @param movedTab The tab which has been moved. This is the last tab within the group.
@@ -139,6 +148,7 @@
// The number of groups with at least 2 tabs.
private int mActualGroupCount;
private Tab mAbsentSelectedTab;
+ private boolean mShouldRecordUma = true;
public TabGroupModelFilter(TabModel tabModel) {
super(tabModel);
@@ -223,6 +233,71 @@
}
}
+ /**
+ * This method merges the source group that contains the {@code sourceTabId} to the destination
+ * group that contains the {@code destinationTabId}. This method only operates if two groups are
+ * in the same {@code TabModel}.
+ *
+ * @param sourceTabId The id of the {@link Tab} to get the source group.
+ * @param destinationTabId The id of a {@link Tab} to get the destination group.
+ */
+ public void mergeTabsToGroup(int sourceTabId, int destinationTabId) {
+ Tab sourceTab = TabModelUtils.getTabById(getTabModel(), sourceTabId);
+ Tab destinationTab = TabModelUtils.getTabById(getTabModel(), destinationTabId);
+
+ assert sourceTab != null && destinationTab != null
+ && sourceTab.isIncognito()
+ == destinationTab.isIncognito()
+ : "Attempting to merge groups from different model";
+
+ int destinationGroupId = destinationTab.getRootId();
+ List<Tab> tabsToMerge = getRelatedTabList(sourceTabId);
+ int sourceTabIndexInTabModel =
+ TabModelUtils.getTabIndexById(getTabModel(), sourceTab.getId());
+ int destinationIndexInTabModel = getTabModelDestinationIndex(destinationTab);
+ boolean isMovingBackward = sourceTabIndexInTabModel < destinationIndexInTabModel;
+
+ if (!needToUpdateTabModel(tabsToMerge, destinationIndexInTabModel)) {
+ for (int i = 0; i < tabsToMerge.size(); i++) {
+ Tab tab = tabsToMerge.get(i);
+ tab.setRootId(destinationGroupId);
+ }
+ resetFilterState();
+
+ Tab lastMergedTab = tabsToMerge.get(tabsToMerge.size() - 1);
+ TabGroup group = mGroupIdToGroupMap.get(lastMergedTab.getRootId());
+ for (Observer observer : mGroupFilterObserver) {
+ observer.didMergeTabToGroup(
+ tabsToMerge.get(tabsToMerge.size() - 1), group.getLastShownTabId());
+ }
+ } else {
+ for (int i = 0; i < tabsToMerge.size(); i++) {
+ Tab tab = tabsToMerge.get(i);
+ tab.setRootId(destinationGroupId);
+ getTabModel().moveTab(tab.getId(),
+ isMovingBackward ? destinationIndexInTabModel
+ : destinationIndexInTabModel++);
+ }
+ }
+ }
+
+ private int getTabModelDestinationIndex(Tab destinationTab) {
+ List<Integer> destinationGroupedTabIds =
+ mGroupIdToGroupMap.get(destinationTab.getRootId()).getTabIdList();
+ int destinationTabIndex = TabModelUtils.getTabIndexById(
+ getTabModel(), destinationGroupedTabIds.get(destinationGroupedTabIds.size() - 1));
+
+ return destinationTabIndex + 1;
+ }
+
+ private boolean needToUpdateTabModel(List<Tab> tabsToMerge, int destinationIndexInTabModel) {
+ assert tabsToMerge.size() > 0;
+
+ int firstTabIndexInTabModel =
+ TabModelUtils.getTabIndexById(getTabModel(), tabsToMerge.get(0).getId());
+ return firstTabIndexInTabModel != destinationIndexInTabModel;
+ }
+
// TODO(crbug.com/951608): follow up with sessions count histogram for TabGroups.
private int updateAndGetSessionsCount(int groupId) {
ThreadUtils.assertOnBackgroundThread();
@@ -276,7 +351,8 @@
if (mGroupIdToGroupMap.containsKey(groupId)) {
if (mGroupIdToGroupMap.get(groupId).size() == 1) {
mActualGroupCount++;
- if (tab.getLaunchType() == TabLaunchType.FROM_LONGPRESS_BACKGROUND) {
+ if (mShouldRecordUma
+ && tab.getLaunchType() == TabLaunchType.FROM_LONGPRESS_BACKGROUND) {
RecordUserAction.record("TabGroup.Created.OpenInNewTab");
}
}
@@ -377,24 +453,77 @@
}
@Override
+ protected void resetFilterStateInternal() {
+ mGroupIdToGroupIndexMap.clear();
+ mGroupIdToGroupMap.clear();
+ mActualGroupCount = 0;
+ }
+
+ @Override
+ protected void resetFilterState() {
+ mShouldRecordUma = false;
+ super.resetFilterState();
+
+ TabModel tabModel = getTabModel();
+ selectTab(tabModel.getTabAt(tabModel.index()));
+ mShouldRecordUma = true;
+ }
+
+ @Override
protected boolean shouldNotifyObserversOnSetIndex() {
return mAbsentSelectedTab == null;
}
@Override
public void didMoveTab(Tab tab, int newIndex, int curIndex) {
- super.didMoveTab(tab, newIndex, curIndex);
+ // Need to cache the flag before resetting the internal data map.
+ boolean isMergeTabToGroup = isMergeTabToGroup(tab);
+ int groupIdBeforeMove = getGroupIdBeforeMove(tab, isMergeTabToGroup);
+ assert groupIdBeforeMove != TabGroup.INVALID_GROUP_ID;
+ TabGroup groupBeforeMove = mGroupIdToGroupMap.get(groupIdBeforeMove);
- if (isMoveWithinGroup(tab, curIndex, newIndex)) {
+ if (isMergeTabToGroup) {
+ resetFilterState();
+ if (groupBeforeMove != null && groupBeforeMove.size() != 1) return;
+
+ TabGroup group = mGroupIdToGroupMap.get(tab.getRootId());
for (Observer observer : mGroupFilterObserver) {
- observer.didMoveWithinGroup(tab, curIndex, newIndex);
+ observer.didMergeTabToGroup(tab, group.getLastShownTabId());
}
} else {
- if (!hasFinishedMovingGroup(tab, newIndex)) return;
- for (Observer observer : mGroupFilterObserver) {
- observer.didMoveTabGroup(tab, curIndex, newIndex);
+ reorder();
+
+ if (isMoveWithinGroup(tab, curIndex, newIndex)) {
+ for (Observer observer : mGroupFilterObserver) {
+ observer.didMoveWithinGroup(tab, curIndex, newIndex);
+ }
+ } else {
+ if (!hasFinishedMovingGroup(tab, newIndex)) return;
+ for (Observer observer : mGroupFilterObserver) {
+ observer.didMoveTabGroup(tab, curIndex, newIndex);
+ }
}
}
+
+ super.didMoveTab(tab, newIndex, curIndex);
+ }
+
+ private boolean isMergeTabToGroup(Tab tab) {
+ TabGroup tabGroup = mGroupIdToGroupMap.get(tab.getRootId());
+ return !tabGroup.contains(tab.getId());
+ }
+
+ private int getGroupIdBeforeMove(Tab tabToMove, boolean isMoveToDifferentGroup) {
+ if (!isMoveToDifferentGroup) return tabToMove.getRootId();
+
+ Set<Integer> groupIdSet = mGroupIdToGroupMap.keySet();
+ for (Integer groupIdKey : groupIdSet) {
+ if (mGroupIdToGroupMap.get(groupIdKey).contains(tabToMove.getId())) {
+ return groupIdKey;
+ }
+ }
+
+ return TabGroup.INVALID_GROUP_ID;
}
private boolean isMoveWithinGroup(
@@ -409,8 +538,13 @@
private boolean hasFinishedMovingGroup(Tab movedTab, int newIndexInTabModel) {
TabGroup tabGroup = mGroupIdToGroupMap.get(movedTab.getRootId());
- int offsetIndex = Math.abs(newIndexInTabModel - tabGroup.size() + 1);
- return tabGroup.contains(getTabModel().getTabAt(offsetIndex).getId());
+ int offsetIndex = newIndexInTabModel - tabGroup.size() + 1;
+ if (offsetIndex < 0) return false;
+
+ for (int i = newIndexInTabModel; i >= offsetIndex; i--) {
+ if (getTabModel().getTabAt(i).getRootId() != movedTab.getRootId()) return false;
+ }
+ return true;
}
// TabList implementation.
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilterUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilterUnitTest.java
new file mode 100644
index 0000000..631f40e
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tasks/tabgroup/TabGroupModelFilterUnitTest.java
@@ -0,0 +1,344 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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.tasks.tabgroup;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tabmodel.TabLaunchType;
+import org.chromium.chrome.browser.tabmodel.TabModel;
+import org.chromium.chrome.browser.tabmodel.TabModelObserver;
+import org.chromium.chrome.browser.tabmodel.TabModelUtils;
+import org.chromium.testing.local.LocalRobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@link TabGroupModelFilter}.
+ */
+@RunWith(LocalRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TabGroupModelFilterUnitTest {
+ private static final int TAB1_ID = 456;
+ private static final int TAB2_ID = 789;
+ private static final int TAB3_ID = 123;
+ private static final int TAB4_ID = 147;
+ private static final int TAB5_ID = 258;
+ private static final int TAB6_ID = 369;
+ private static final int TAB1_ROOT_ID = TAB1_ID;
+ private static final int TAB2_ROOT_ID = TAB2_ID;
+ private static final int TAB3_ROOT_ID = TAB2_ID;
+ private static final int TAB4_ROOT_ID = TAB4_ID;
+ private static final int TAB5_ROOT_ID = TAB5_ID;
+ private static final int TAB6_ROOT_ID = TAB5_ID;
+ private static final int POSITION1 = 0;
+ private static final int POSITION2 = 1;
+ private static final int POSITION3 = 2;
+ private static final int POSITION4 = 3;
+ private static final int POSITION5 = 4;
+ private static final int POSITION6 = 5;
+
+ @Mock
+ TabModel mTabModel;
+
+ @Mock
+ TabGroupModelFilter.Observer mTabGroupModelFilterObserver;
+
+ @Captor
+ ArgumentCaptor<TabModelObserver> mTabModelObserverCaptor;
+
+ private Tab mTab1;
+ private Tab mTab2;
+ private Tab mTab3;
+ private Tab mTab4;
+ private Tab mTab5;
+ private Tab mTab6;
+ private List<Tab> mTabs = new ArrayList<>();
+
+ private TabGroupModelFilter mTabGroupModelFilter;
+
+ private Tab prepareTab(int tabId, int rootId) {
+ Tab tab = mock(Tab.class);
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ int newRootId = invocation.getArgument(0);
+ doReturn(newRootId).when(tab).getRootId();
+ return null;
+ }
+ }).when(tab).setRootId(anyInt());
+
+ doReturn(tabId).when(tab).getId();
+ tab.setRootId(rootId);
+
+ return tab;
+ }
+
+ private void setRootId(Tab tab, int rootId) {
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ int newRootId = invocation.getArgument(0);
+ doReturn(newRootId).when(tab).getRootId();
+ return null;
+ }
+ }).when(tab).setRootId(rootId);
+ }
+
+ private void setUpTab() {
+ mTab1 = prepareTab(TAB1_ID, TAB1_ROOT_ID);
+ mTab2 = prepareTab(TAB2_ID, TAB2_ROOT_ID);
+ mTab3 = prepareTab(TAB3_ID, TAB3_ROOT_ID);
+ mTab4 = prepareTab(TAB4_ID, TAB4_ROOT_ID);
+ mTab5 = prepareTab(TAB5_ID, TAB5_ROOT_ID);
+ mTab6 = prepareTab(TAB6_ID, TAB6_ROOT_ID);
+ }
+
+ private void setUpTabModel() {
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Tab tab = invocation.getArgument(0);
+ mTabs.add(tab);
+ return null;
+ }
+ }).when(mTabModel).addTab(any(Tab.class), anyInt(), anyInt());
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ int movedTabId = invocation.getArgument(0);
+ int newIndex = invocation.getArgument(1);
+
+ int oldIndex = TabModelUtils.getTabIndexById(mTabModel, movedTabId);
+ Tab tab = TabModelUtils.getTabById(mTabModel, movedTabId);
+
+ mTabs.remove(tab);
+ if (oldIndex < newIndex) --newIndex;
+ mTabs.add(newIndex, tab);
+ mTabModelObserverCaptor.getValue().didMoveTab(tab, newIndex, oldIndex);
+ return null;
+ }
+ }).when(mTabModel).moveTab(anyInt(), anyInt());
+
+ doAnswer(new Answer() {
+ @Override
+ public Tab answer(InvocationOnMock invocation) throws Throwable {
+ int index = invocation.getArgument(0);
+ return mTabs.get(index);
+ }
+ }).when(mTabModel).getTabAt(anyInt());
+
+ doAnswer(new Answer() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ Tab tab = invocation.getArgument(0);
+ return mTabs.indexOf(tab);
+ }
+ }).when(mTabModel).indexOf(any(Tab.class));
+
+ doAnswer(new Answer() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ return mTabs.size();
+ }
+ }).when(mTabModel).getCount();
+
+ doReturn(0).when(mTabModel).index();
+ }
+
+ @Before
+ public void setUp() {
+ RecordUserAction.setDisabledForTests(true);
+ RecordHistogram.setDisabledForTests(true);
+
+ MockitoAnnotations.initMocks(this);
+
+ setUpTab();
+ setUpTabModel();
+
+ doNothing().when(mTabModel).addObserver(mTabModelObserverCaptor.capture());
+
+ mTabGroupModelFilter = new TabGroupModelFilter(mTabModel);
+ mTabGroupModelFilter.addTabGroupObserver(mTabGroupModelFilterObserver);
+
+ mTabModel.addTab(mTab1, -1, TabLaunchType.FROM_CHROME_UI);
+ mTabModelObserverCaptor.getValue().didAddTab(mTab1, TabLaunchType.FROM_CHROME_UI);
+
+ mTabModel.addTab(mTab2, -1, TabLaunchType.FROM_CHROME_UI);
+ mTabModelObserverCaptor.getValue().didAddTab(mTab2, TabLaunchType.FROM_CHROME_UI);
+
+ mTabModel.addTab(mTab3, -1, TabLaunchType.FROM_CHROME_UI);
+ mTabModelObserverCaptor.getValue().didAddTab(mTab3, TabLaunchType.FROM_CHROME_UI);
+
+ mTabModel.addTab(mTab4, -1, TabLaunchType.FROM_CHROME_UI);
+ mTabModelObserverCaptor.getValue().didAddTab(mTab4, TabLaunchType.FROM_CHROME_UI);
+
+ mTabModel.addTab(mTab5, -1, TabLaunchType.FROM_CHROME_UI);
+ mTabModelObserverCaptor.getValue().didAddTab(mTab5, TabLaunchType.FROM_CHROME_UI);
+
+ mTabModel.addTab(mTab6, -1, TabLaunchType.FROM_CHROME_UI);
+ mTabModelObserverCaptor.getValue().didAddTab(mTab6, TabLaunchType.FROM_CHROME_UI);
+ }
+
+ @After
+ public void tearDown() {
+ RecordUserAction.setDisabledForTests(false);
+ RecordHistogram.setDisabledForTests(false);
+ }
+
+ @Test
+ public void mergeTabToGroup_No_Update_TabModel() {
+ List<Tab> expectedGroup = new ArrayList<>(Arrays.asList(mTab2, mTab3, mTab4));
+
+ mTabGroupModelFilter.mergeTabsToGroup(mTab4.getId(), mTab2.getId());
+
+ verify(mTabModel, never()).moveTab(anyInt(), anyInt());
+ assertArrayEquals(mTabGroupModelFilter.getRelatedTabList(mTab4.getId()).toArray(),
+ expectedGroup.toArray());
+ }
+
+ @Test
+ public void mergeTabToGroup_Update_TabModel() {
+ mTabGroupModelFilter.mergeTabsToGroup(mTab5.getId(), mTab2.getId());
+ verify(mTabModel).moveTab(mTab5.getId(), POSITION3 + 1);
+ }
+
+ @Test
+ public void mergeOneTabToTab_Forward() {
+ List<Tab> expectedGroup = new ArrayList<>(Arrays.asList(mTab1, mTab4));
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab1, mTab4, mTab2, mTab3, mTab5, mTab6));
+ int startIndex = POSITION1;
+
+ mTabGroupModelFilter.mergeTabsToGroup(mTab4.getId(), mTab1.getId());
+
+ verify(mTabModel).moveTab(mTab4.getId(), ++startIndex);
+ verify(mTabGroupModelFilterObserver).didMergeTabToGroup(mTab4, mTab1.getId());
+ assertArrayEquals(mTabGroupModelFilter.getRelatedTabList(mTab4.getId()).toArray(),
+ expectedGroup.toArray());
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+
+ @Test
+ public void mergeGroupToTab_Forward() {
+ List<Tab> expectedGroup = new ArrayList<>(Arrays.asList(mTab1, mTab5, mTab6));
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab1, mTab5, mTab6, mTab2, mTab3, mTab4));
+ int startIndex = POSITION1;
+
+ mTabGroupModelFilter.mergeTabsToGroup(mTab5.getId(), mTab1.getId());
+
+ verify(mTabModel).moveTab(mTab5.getId(), ++startIndex);
+ verify(mTabModel).moveTab(mTab6.getId(), ++startIndex);
+ verify(mTabGroupModelFilterObserver).didMergeTabToGroup(mTab6, mTab1.getId());
+ assertArrayEquals(mTabGroupModelFilter.getRelatedTabList(mTab5.getId()).toArray(),
+ expectedGroup.toArray());
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+
+ @Test
+ public void mergeGroupToGroup_Forward() {
+ List<Tab> expectedGroup = new ArrayList<>(Arrays.asList(mTab2, mTab3, mTab5, mTab6));
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab1, mTab2, mTab3, mTab5, mTab6, mTab4));
+ int startIndex = POSITION3;
+
+ mTabGroupModelFilter.mergeTabsToGroup(mTab5.getId(), mTab2.getId());
+
+ verify(mTabModel).moveTab(mTab5.getId(), ++startIndex);
+ verify(mTabModel).moveTab(mTab6.getId(), ++startIndex);
+ verify(mTabGroupModelFilterObserver).didMergeTabToGroup(mTab6, mTab2.getId());
+ assertArrayEquals(mTabGroupModelFilter.getRelatedTabList(mTab5.getId()).toArray(),
+ expectedGroup.toArray());
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+
+ @Test
+ public void mergeOneTabToTab_Backward() {
+ List<Tab> expectedGroup = new ArrayList<>(Arrays.asList(mTab4, mTab1));
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab2, mTab3, mTab4, mTab1, mTab5, mTab6));
+ int startIndex = POSITION4;
+
+ mTabGroupModelFilter.mergeTabsToGroup(mTab1.getId(), mTab4.getId());
+
+ verify(mTabModel).moveTab(mTab1.getId(), startIndex + 1);
+ verify(mTabGroupModelFilterObserver).didMergeTabToGroup(mTab1, mTab4.getId());
+ assertArrayEquals(mTabGroupModelFilter.getRelatedTabList(mTab1.getId()).toArray(),
+ expectedGroup.toArray());
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+
+ @Test
+ public void mergeGroupToTab_Backward() {
+ List<Tab> expectedGroup = new ArrayList<>(Arrays.asList(mTab4, mTab2, mTab3));
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab1, mTab4, mTab2, mTab3, mTab5, mTab6));
+ int startIndex = POSITION4;
+
+ mTabGroupModelFilter.mergeTabsToGroup(mTab2.getId(), mTab4.getId());
+
+ verify(mTabModel).moveTab(mTab2.getId(), startIndex + 1);
+ verify(mTabModel).moveTab(mTab3.getId(), startIndex + 1);
+ verify(mTabGroupModelFilterObserver).didMergeTabToGroup(mTab3, mTab4.getId());
+ assertArrayEquals(mTabGroupModelFilter.getRelatedTabList(mTab2.getId()).toArray(),
+ expectedGroup.toArray());
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+
+ @Test
+ public void moveGroup_Backward() {
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab1, mTab4, mTab2, mTab3, mTab5, mTab6));
+ int startIndex = POSITION4;
+
+ mTabGroupModelFilter.moveRelatedTabs(mTab2.getId(), startIndex + 1);
+
+ verify(mTabModel).moveTab(mTab2.getId(), startIndex + 1);
+ verify(mTabModel).moveTab(mTab3.getId(), startIndex + 1);
+ verify(mTabGroupModelFilterObserver).didMoveTabGroup(mTab3, POSITION3 - 1, startIndex);
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+
+ @Test
+ public void moveGroup_Forward() {
+ List<Tab> expectedTabModel =
+ new ArrayList<>(Arrays.asList(mTab1, mTab2, mTab3, mTab5, mTab6, mTab4));
+ int startIndex = POSITION3;
+
+ mTabGroupModelFilter.moveRelatedTabs(mTab5.getId(), startIndex + 1);
+
+ verify(mTabModel).moveTab(mTab5.getId(), startIndex + 1);
+ verify(mTabModel).moveTab(mTab6.getId(), startIndex + 2);
+ verify(mTabGroupModelFilterObserver).didMoveTabGroup(mTab6, POSITION6, startIndex + 2);
+ assertArrayEquals(mTabs.toArray(), expectedTabModel.toArray());
+ }
+}