// Copyright 2021 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.app.tabmodel;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.content.res.Resources;
import android.os.Looper;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.android.util.concurrent.PausedExecutorService;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.task.PostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.homepage.HomepageManager;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.ntp.RecentlyClosedBridge;
import org.chromium.chrome.browser.ntp.RecentlyClosedBridgeJni;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
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.TabState;
import org.chromium.chrome.browser.tab.TabStateExtractor;
import org.chromium.chrome.browser.tab.TabTestUtils;
import org.chromium.chrome.browser.tab.WebContentsState;
import org.chromium.chrome.browser.tab.state.PersistedTabData;
import org.chromium.chrome.browser.tab.state.PersistedTabDataJni;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.ChromeTabCreator;
import org.chromium.chrome.browser.tabmodel.MismatchedIndicesHandler;
import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelJniBridge;
import org.chromium.chrome.browser.tabmodel.TabModelJniBridgeJni;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabModelSelectorMetadata;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabPersistentStoreObserver;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
/** Tests for TabPersistentStore reacting to events from TabModel and Tab. */
@RunWith(BaseRobolectricTestRunner.class)
@LooperMode(Mode.PAUSED)
@DisableFeatures({
ChromeFeatureList.ANDROID_TAB_DECLUTTER,
ChromeFeatureList.ANDROID_TAB_DECLUTTER_RESCUE_KILLSWITCH
})
public class TabPersistentStoreIntegrationTest {
@Rule public JniMocker jniMocker = new JniMocker();
private static final int TAB_ID = 42;
private static final WebContentsState WEB_CONTENTS_STATE =
new WebContentsState(ByteBuffer.allocateDirect(100));
private TabbedModeTabModelOrchestrator mOrchestrator;
private TabModelSelector mTabModelSelector;
private TabPersistentStore mTabPersistentStore;
@Mock private ChromeTabbedActivity mChromeActivity;
@Mock private TabCreatorManager mTabCreatorManager;
@Mock private ChromeTabCreator mChromeTabCreator;
@Mock private NextTabPolicySupplier mNextTabPolicySupplier;
@Mock private MismatchedIndicesHandler mMismatchedIndicesHandler;
@Mock private TabContentManager mTabContentManager;
@Mock private Profile mProfile;
@Mock private ProfileProvider mProfileProvider;
@Mock private TabModelJniBridge.Natives mTabModelJniBridgeJni;
@Mock private RecentlyClosedBridge.Natives mRecentlyClosedBridgeJni;
@Mock private Resources mResources;
@Mock private PersistedTabData.Natives mPersistedTabDataJni;
@Mock private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
private PausedExecutorService mExecutor = new PausedExecutorService();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
PostTask.setPrenativeThreadPoolExecutorForTesting(mExecutor);
// Create TabPersistentStore and TabModelSelectorImpl through orchestrator like
// ChromeActivity does.
when(mChromeActivity.isInMultiWindowMode()).thenReturn(false);
when(mChromeActivity.getResources()).thenReturn(mResources);
when(mResources.getInteger(org.chromium.ui.R.integer.min_screen_width_bucket))
.thenReturn(1);
when(mTabCreatorManager.getTabCreator(anyBoolean())).thenReturn(mChromeTabCreator);
// Pretend native was loaded, creating TabModelImpls.
OneshotSupplierImpl<ProfileProvider> profileProviderSupplier = new OneshotSupplierImpl<>();
profileProviderSupplier.set(mProfileProvider);
when(mProfileProvider.getOriginalProfile()).thenReturn(mProfile);
when(mProfile.getOriginalProfile()).thenReturn(mProfile);
PriceTrackingFeatures.setPriceTrackingEnabledForTesting(false);
mOrchestrator =
new TabbedModeTabModelOrchestrator(
/* tabMergingEnabled= */ true, mActivityLifecycleDispatcher);
mOrchestrator.createTabModels(
mChromeActivity,
profileProviderSupplier,
mTabCreatorManager,
mNextTabPolicySupplier,
mMismatchedIndicesHandler,
0);
mTabModelSelector = mOrchestrator.getTabModelSelector();
mTabPersistentStore = mOrchestrator.getTabPersistentStore();
jniMocker.mock(TabModelJniBridgeJni.TEST_HOOKS, mTabModelJniBridgeJni);
jniMocker.mock(RecentlyClosedBridgeJni.TEST_HOOKS, mRecentlyClosedBridgeJni);
jniMocker.mock(PersistedTabDataJni.TEST_HOOKS, mPersistedTabDataJni);
TabTestUtils.mockTabJni(jniMocker);
mOrchestrator.onNativeLibraryReady(mTabContentManager);
}
@After
public void tearDown() {
// TabbedModeTabModelOrchestrator gets a new TabModelSelector from TabWindowManagerSingleton
// for every test case, so TabWindowManagerSingleton has to be reset to avoid running out of
// assignment slots.
TabWindowManagerSingleton.resetTabModelSelectorFactoryForTesting();
TabStateExtractor.resetTabStatesForTesting();
}
@Test
@SmallTest
@Feature({"TabPersistentStore"})
public void testOpenAndCloseTabCreatesAndDeletesFile_tabState() {
doTestOpenAndCloseTabCreatesAndDeletesFile();
}
private void doTestOpenAndCloseTabCreatesAndDeletesFile() {
// Setup the test: Create a tab
TabModel tabModel = mTabModelSelector.getModel(false);
MockTab tab = MockTab.createAndInitialize(TAB_ID, mProfile, TabLaunchType.FROM_CHROME_UI);
// Ordinarily, TabState comes from native, so setup a stub in TabStateExtractor.
TabState tabState = new TabState();
tabState.contentsState = WEB_CONTENTS_STATE;
TabStateExtractor.setTabStateForTesting(TAB_ID, tabState);
tabModel.addTab(tab, 0, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
File tabStateFile = mTabPersistentStore.getTabStateFile(TAB_ID, false);
assertFalse(tabStateFile.exists());
// Step to test: Load stops
tab.broadcastOnLoadStopped(false);
runAllAsyncTasks();
// Verify tab state was created
assertTrue(tabStateFile.exists());
// Close the tab
tabModel.closeTabs(TabClosureParams.closeTab(tab).build());
runAllAsyncTasks();
// Step to test: Commit tab closure
tabModel.commitTabClosure(TAB_ID);
runAllAsyncTasks();
// Verify the file was deleted
assertFalse(tabStateFile.exists());
}
@Test
@SmallTest
@Feature({"TabPersistentStore"})
public void testUndoTabClosurePersistsState_tabState() {
doTestUndoTabClosurePersistsState();
}
private void doTestUndoTabClosurePersistsState() {
AtomicInteger timesMetadataSaved = new AtomicInteger();
observeOnMetadataSavedAsynchronously(timesMetadataSaved);
// Setup the test: Create a tab and close it
TabModel tabModel = mTabModelSelector.getModel(false);
Tab tab = MockTab.createAndInitialize(TAB_ID, mProfile, TabLaunchType.FROM_CHROME_UI);
tabModel.addTab(tab, 0, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
tabModel.closeTabs(TabClosureParams.closeTab(tab).build());
runAllAsyncTasks();
int timesMetadataSavedBefore = timesMetadataSaved.intValue();
// Step to test: Cancel tab closure
tabModel.cancelTabClosure(TAB_ID);
runAllAsyncTasks();
// Verify that metadata was saved
assertEquals(timesMetadataSavedBefore + 1, timesMetadataSaved.intValue());
}
@Test
@SmallTest
@Feature({"TabPersistentStore"})
public void testCloseTabPersistsState() {
AtomicInteger timesMetadataSaved = new AtomicInteger();
observeOnMetadataSavedAsynchronously(timesMetadataSaved);
// Setup the test: Create a tab and close it.
TabModel tabModel = mTabModelSelector.getModel(false);
Tab tab = MockTab.createAndInitialize(1, mProfile, TabLaunchType.FROM_CHROME_UI);
tabModel.addTab(tab, 0, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
int timesMetadataSavedBefore = timesMetadataSaved.intValue();
// Step to test: Close tab.
tabModel.closeTabs(TabClosureParams.closeTab(tab).build());
runAllAsyncTasks();
// Step to test: Commit tab closure.
tabModel.commitTabClosure(1);
runAllAsyncTasks();
// Verify that metadata was saved.
assertEquals(timesMetadataSavedBefore + 1, timesMetadataSaved.intValue());
}
@Test
@SmallTest
@Feature({"TabPersistentStore"})
public void testCloseAllTabsPersistsState() {
HomepageManager homepageManager = Mockito.mock(HomepageManager.class);
when(homepageManager.shouldCloseAppWithZeroTabs()).thenReturn(false);
HomepageManager.setInstanceForTesting(homepageManager);
AtomicInteger timesMetadataSaved = new AtomicInteger();
observeOnMetadataSavedAsynchronously(timesMetadataSaved);
// Setup the test: Create three tabs and close them all.
TabModel tabModel = mTabModelSelector.getModel(false);
Tab tab1 = MockTab.createAndInitialize(1, mProfile, TabLaunchType.FROM_CHROME_UI);
tabModel.addTab(tab1, 0, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
Tab tab2 = MockTab.createAndInitialize(2, mProfile, TabLaunchType.FROM_CHROME_UI);
tabModel.addTab(tab2, 1, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
Tab tab3 = MockTab.createAndInitialize(3, mProfile, TabLaunchType.FROM_CHROME_UI);
tabModel.addTab(tab3, 2, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
int timesMetadataSavedBefore = timesMetadataSaved.intValue();
// Step to test: Close all tabs.
tabModel.closeTabs(TabClosureParams.closeAllTabs().build());
runAllAsyncTasks();
// Step to test: Commit tabs closure.
tabModel.commitAllTabClosures();
runAllAsyncTasks();
// Verify that metadata was saved.
assertEquals(timesMetadataSavedBefore + 1, timesMetadataSaved.intValue());
}
private void runAllAsyncTasks() {
// Run AsyncTasks
mExecutor.runAll();
// Wait for onPostExecute() of the AsyncTasks to run on the UI Thread.
shadowOf(Looper.getMainLooper()).idle();
}
private void observeOnMetadataSavedAsynchronously(AtomicInteger timesMetadataSaved) {
TabPersistentStoreObserver observer =
new TabPersistentStoreObserver() {
@Override
public void onMetadataSavedAsynchronously(
TabModelSelectorMetadata modelSelectorMetadata) {
timesMetadataSaved.incrementAndGet();
}
};
mTabPersistentStore.addObserver(observer);
}
}