chromium/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/ArchivedTabsMessageServiceUnitTest.java

// Copyright 2024 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.tasks.tab_management;

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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsCardViewProperties.ARCHIVE_TIME_DELTA_DAYS;
import static org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsCardViewProperties.CLICK_HANDLER;
import static org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsCardViewProperties.NUMBER_OF_ARCHIVED_TABS;

import android.app.Activity;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.app.tabmodel.ArchivedTabModelOrchestrator;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.tab.TabArchiveSettings;
import org.chromium.chrome.browser.tab_ui.OnTabSelectingListener;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tasks.tab_management.MessageService.MessageType;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.PropertyModel;

/** Tests for ArchivedTabsMessageService. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ArchivedTabsMessageServiceUnitTest {
    private static final int TIME_DELTA_DAYS = 10;

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

    @Mock private ArchivedTabModelOrchestrator mArchivedTabModelOrchestrator;
    @Mock private TabArchiveSettings mTabArchiveSettings;
    @Mock private TabModel mArchivedTabModel;
    @Mock private MessageService.MessageObserver mMessageObserver;
    @Mock private ArchivedTabsDialogCoordinator mArchivedTabsDialogCoordinator;
    @Mock private BrowserControlsStateProvider mBrowserControlsStateProvider;
    @Mock private TabContentManager mTabContentManager;
    @Mock private SnackbarManager mSnackbarManager;
    @Mock private TabCreator mRegularTabCreator;
    @Mock private BackPressManager mBackPressManager;
    @Mock private OnTabSelectingListener mOnTabSelectingListener;
    @Mock private ModalDialogManager mModalDialogManager;
    @Mock private Tracker mTracker;
    @Mock private Runnable mAppendMessageRunnable;
    @Mock private TabListCoordinator mTabListCoordinator;
    @Captor private ArgumentCaptor<TabArchiveSettings.Observer> mTabArchiveSettingsObserver;

    private Activity mActivity;
    private ViewGroup mRootView;
    private ArchivedTabsMessageService mArchivedTabsMessageService;
    private ObservableSupplierImpl<Integer> mTabCountSupplier = new ObservableSupplierImpl<>();
    private ObservableSupplierImpl<TabListCoordinator> mTabListCoordinatorSupplier =
            new ObservableSupplierImpl<>();

    @Before
    public void setUp() throws Exception {
        mActivity = Robolectric.buildActivity(Activity.class).setup().get();
        mRootView = new FrameLayout(mActivity);

        doReturn(TIME_DELTA_DAYS).when(mTabArchiveSettings).getArchiveTimeDeltaDays();
        doReturn(mTabCountSupplier).when(mArchivedTabModel).getTabCountSupplier();
        mTabListCoordinatorSupplier.set(mTabListCoordinator);
    }

    private void createArchivedTabsMessageService() {
        mArchivedTabsMessageService =
                new ArchivedTabsMessageService(
                        mActivity,
                        mArchivedTabModelOrchestrator,
                        mBrowserControlsStateProvider,
                        mTabContentManager,
                        TabListMode.GRID,
                        mRootView,
                        mSnackbarManager,
                        mRegularTabCreator,
                        mBackPressManager,
                        mModalDialogManager,
                        mTracker,
                        mAppendMessageRunnable,
                        mTabListCoordinatorSupplier);
        mArchivedTabsMessageService.setArchivedTabsDialogCoordiantorForTesting(
                mArchivedTabsDialogCoordinator);
        mArchivedTabsMessageService.addObserver(mMessageObserver);
        mArchivedTabsMessageService.setOnTabSelectingListener(mOnTabSelectingListener);

        // When the service is created, this getter will return null. Only set up the mock right
        // before onTabModelCreated is called when initialization is nearly over.
        doReturn(mTabArchiveSettings).when(mArchivedTabModelOrchestrator).getTabArchiveSettings();

        mArchivedTabsMessageService
                .getArchivedTabModelOrchestratorObserverForTesting()
                .onTabModelCreated(mArchivedTabModel);
        verify(mTabArchiveSettings).addObserver(mTabArchiveSettingsObserver.capture());
    }

    @Test
    public void testTabAddedThenRemoved() {
        createArchivedTabsMessageService();
        PropertyModel customCardPropertyModel =
                mArchivedTabsMessageService.getCustomCardModelForTesting();

        doReturn(1).when(mArchivedTabModel).getCount();
        mTabCountSupplier.set(1);
        assertEquals(1, customCardPropertyModel.get(NUMBER_OF_ARCHIVED_TABS));
        assertEquals(10, customCardPropertyModel.get(ARCHIVE_TIME_DELTA_DAYS));
        verify(mMessageObserver, times(1))
                .messageReady(eq(MessageType.ARCHIVED_TABS_MESSAGE), any());

        doReturn(0).when(mArchivedTabModel).getCount();
        mTabCountSupplier.set(0);
        assertEquals(0, customCardPropertyModel.get(NUMBER_OF_ARCHIVED_TABS));
        assertEquals(10, customCardPropertyModel.get(ARCHIVE_TIME_DELTA_DAYS));
        verify(mMessageObserver, times(1)).messageInvalidate(MessageType.ARCHIVED_TABS_MESSAGE);
        verify(mAppendMessageRunnable).run();
    }

    @Test
    public void testSendDuplicateMessage() {
        createArchivedTabsMessageService();
        PropertyModel customCardPropertyModel =
                mArchivedTabsMessageService.getCustomCardModelForTesting();

        doReturn(12).when(mArchivedTabModel).getCount();
        mTabCountSupplier.set(12);
        assertEquals(12, customCardPropertyModel.get(NUMBER_OF_ARCHIVED_TABS));
        assertEquals(10, customCardPropertyModel.get(ARCHIVE_TIME_DELTA_DAYS));

        doReturn(8).when(mArchivedTabModel).getCount();
        verify(mMessageObserver, times(1))
                .messageReady(eq(MessageType.ARCHIVED_TABS_MESSAGE), any());
        mTabCountSupplier.set(8);
        assertEquals(8, customCardPropertyModel.get(NUMBER_OF_ARCHIVED_TABS));
        assertEquals(10, customCardPropertyModel.get(ARCHIVE_TIME_DELTA_DAYS));
        // Sending another message to the queue should exit early without sending a message.
        verify(mMessageObserver, times(1))
                .messageReady(eq(MessageType.ARCHIVED_TABS_MESSAGE), any());
        verify(mAppendMessageRunnable, times(1)).run();

        // After invalidating the previous message, a new message should be sent.
        mArchivedTabsMessageService.maybeInvalidatePreviouslySentMessage();
        mArchivedTabsMessageService.maybeSendMessageToQueue();
        verify(mMessageObserver, times(2))
                .messageReady(eq(MessageType.ARCHIVED_TABS_MESSAGE), any());
        verify(mMessageObserver, times(1)).messageInvalidate(MessageType.ARCHIVED_TABS_MESSAGE);
        verify(mAppendMessageRunnable, times(2)).run();
    }

    @Test
    public void testClickCard() {
        createArchivedTabsMessageService();
        PropertyModel customCardPropertyModel =
                mArchivedTabsMessageService.getCustomCardModelForTesting();
        customCardPropertyModel.get(CLICK_HANDLER).run();
        verify(mArchivedTabsDialogCoordinator).show(mOnTabSelectingListener);
        verify(mTracker).notifyEvent("android_tab_declutter_button_clicked");
    }

    @Test
    public void testCustomViewDetached() {
        createArchivedTabsMessageService();
        mRootView.addView(mArchivedTabsMessageService.getCustomView());
        assertNotNull(mArchivedTabsMessageService.getCustomView().getParent());

        mArchivedTabsMessageService.onRemoveAllAppendedMessage();
        assertNull(mArchivedTabsMessageService.getCustomView().getParent());
    }

    @Test
    public void testSettingsChangesUpdatesMessage() {
        createArchivedTabsMessageService();
        PropertyModel customCardPropertyModel =
                mArchivedTabsMessageService.getCustomCardModelForTesting();

        doReturn(15).when(mTabArchiveSettings).getArchiveTimeDeltaDays();
        mTabArchiveSettingsObserver.getValue().onSettingChanged();
        assertEquals(15, customCardPropertyModel.get(ARCHIVE_TIME_DELTA_DAYS));
    }

    @Test
    public void testDestroy() {
        createArchivedTabsMessageService();
        mArchivedTabsMessageService.destroy();
        verify(mTabArchiveSettings).removeObserver(mTabArchiveSettingsObserver.getValue());
        verify(mArchivedTabsDialogCoordinator).destroy();
    }

    @Test
    public void testIphShownThisSession() {
        TabArchiveSettings.setIphShownThisSession(true);
        createArchivedTabsMessageService();

        PropertyModel customCardPropertyModel =
                mArchivedTabsMessageService.getCustomCardModelForTesting();

        doReturn(12).when(mArchivedTabModel).getCount();
        mTabCountSupplier.set(12);
        assertEquals(12, customCardPropertyModel.get(NUMBER_OF_ARCHIVED_TABS));
        assertEquals(10, customCardPropertyModel.get(ARCHIVE_TIME_DELTA_DAYS));

        doReturn(8).when(mArchivedTabModel).getCount();
        verify(mMessageObserver).messageReady(eq(MessageType.ARCHIVED_TABS_MESSAGE), any());
        mTabCountSupplier.set(8);
        doReturn(true)
                .when(mTabListCoordinator)
                .specialItemExists(MessageType.ARCHIVED_TABS_MESSAGE);
        mArchivedTabsMessageService.onAppendedMessage();
        ShadowLooper.runUiThreadTasks();

        // The bit should be reset.
        assertFalse(TabArchiveSettings.getIphShownThisSession());
        verify(mTabListCoordinator).setRecyclerViewPosition(any());
    }
}