chromium/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedNavigationBarColorControllerUnitTest.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.tabbed_mode;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.graphics.Color;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;

import androidx.test.core.app.ApplicationProvider;

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.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeController;
import org.chromium.chrome.browser.ui.edge_to_edge.NavigationBarColorProvider;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;

@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {TabbedNavigationBarColorControllerUnitTest.ShadowSemanticColorUtils.class},
        sdk = 28)
@EnableFeatures(ChromeFeatureList.NAV_BAR_COLOR_MATCHES_TAB_BACKGROUND)
public class TabbedNavigationBarColorControllerUnitTest {
    @Implements(SemanticColorUtils.class)
    static class ShadowSemanticColorUtils {
        @Implementation
        public static int getBottomSystemNavDividerColor(Context context) {
            return NAV_DIVIDER_COLOR;
        }
    }

    private static final int NAV_DIVIDER_COLOR = Color.LTGRAY;

    private TabbedNavigationBarColorController mNavColorController;
    @Mock private Window mWindow;
    @Mock private View mDecorView;
    @Mock private ViewGroup mRootView;
    private Context mContext;
    @Mock private TabModelSelector mTabModelSelector;
    private ObservableSupplierImpl<LayoutManager> mLayoutManagerSupplier;
    @Mock private LayoutManager mLayoutManager;
    @Mock private FullscreenManager mFullscreenManager;
    private ObservableSupplierImpl<EdgeToEdgeController> mEdgeToEdgeControllerObservableSupplier;
    @Mock private EdgeToEdgeController mEdgeToEdgeController;
    @Mock private BottomAttachedUiObserver mBottomAttachedUiObserver;
    @Mock private Tab mTab;
    @Mock private NavigationBarColorProvider.Observer mObserver;
    @Mock private ObservableSupplierImpl<TabModel> mTabModelSupplier;

    @Captor private ArgumentCaptor<Integer> mWindowColorCaptor;
    @Captor private ArgumentCaptor<Integer> mWindowDividerColorCaptor;
    @Captor private ArgumentCaptor<Integer> mNavigationBarColorChangedCaptor;
    @Captor private ArgumentCaptor<Integer> mNavigationBarDividerColorChangedCaptor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mContext =
                new ContextThemeWrapper(
                        ApplicationProvider.getApplicationContext(),
                        R.style.Theme_BrowserUI_DayNight);
        mLayoutManagerSupplier = new ObservableSupplierImpl<>();
        mEdgeToEdgeControllerObservableSupplier = new ObservableSupplierImpl<>();

        when(mWindow.getContext()).thenReturn(mContext);
        when(mWindow.getDecorView()).thenReturn(mDecorView);
        when(mDecorView.getRootView()).thenReturn(mRootView);
        when(mRootView.getContext()).thenReturn(mContext);
        when(mTabModelSelector.getCurrentTab()).thenReturn(mTab);
        when(mTabModelSelector.getCurrentTabModelSupplier()).thenReturn(mTabModelSupplier);

        mNavColorController =
                new TabbedNavigationBarColorController(
                        mWindow,
                        mTabModelSelector,
                        mLayoutManagerSupplier,
                        mFullscreenManager,
                        mEdgeToEdgeControllerObservableSupplier,
                        mBottomAttachedUiObserver);
        mLayoutManagerSupplier.set(mLayoutManager);
        mEdgeToEdgeControllerObservableSupplier.set(mEdgeToEdgeController);
        mNavColorController.addObserver(mObserver);

        // Setup the capture after TabbedNavigationBarColorController is initialized so it does
        // not capture value during the initializations.
        runColorUpdateAnimation();
        doNothing().when(mWindow).setNavigationBarColor(mWindowColorCaptor.capture());
        doNothing().when(mWindow).setNavigationBarDividerColor(mWindowDividerColorCaptor.capture());
        doNothing()
                .when(mObserver)
                .onNavigationBarColorChanged(mNavigationBarColorChangedCaptor.capture());
        doNothing()
                .when(mObserver)
                .onNavigationBarDividerChanged(mNavigationBarDividerColorChangedCaptor.capture());
    }

    @Test
    public void testMatchBottomAttachedColor() {
        when(mTab.getBackgroundColor()).thenReturn(Color.BLUE);
        when(mLayoutManager.getActiveLayoutType()).thenReturn(LayoutType.BROWSING);
        mNavColorController.updateActiveTabForTesting();
        runColorUpdateAnimation();

        mNavColorController.onBottomAttachedColorChanged(Color.RED, false, false);
        assertTrue(
                "Should be using the bottom attached UI color.",
                mNavColorController.getUseBottomAttachedUiColorForTesting());
        assertNavBarColor(Color.RED);
        assertNavBarDividerColor(Color.RED);

        runColorUpdateAnimation();
        assertWindowNavBarColor(Color.RED);
        assertWindowNavBarDividerColor(Color.RED);

        mNavColorController.onBottomAttachedColorChanged(null, false, false);
        assertFalse(
                "Should no longer be using the bottom attached UI color.",
                mNavColorController.getUseBottomAttachedUiColorForTesting());
        assertNavBarColor(Color.BLUE);
        assertNavBarDividerColor(Color.BLUE);

        runColorUpdateAnimation();
        assertWindowNavBarColor(Color.BLUE);
        assertWindowNavBarDividerColor(Color.BLUE);
    }

    @Test
    public void testMatchBottomAttachedColor_toEdge() {
        when(mTab.getBackgroundColor()).thenReturn(Color.BLUE);
        when(mLayoutManager.getActiveLayoutType()).thenReturn(LayoutType.BROWSING);
        when(mEdgeToEdgeController.isDrawingToEdge()).thenReturn(true);
        when(mWindow.getNavigationBarColor()).thenReturn(Color.RED);
        mNavColorController.updateActiveTabForTesting();
        runColorUpdateAnimation();

        Mockito.clearInvocations(mWindow);
        mNavColorController.onBottomAttachedColorChanged(Color.RED, false, false);
        assertTrue(
                "Should be using the bottom attached UI color.",
                mNavColorController.getUseBottomAttachedUiColorForTesting());
        assertNavBarColor(Color.RED);
        assertNavBarDividerColor(Color.RED);
        assertWindowNavBarColor(Color.TRANSPARENT);
        verify(mWindow, times(0)).setNavigationBarDividerColor(anyInt());

        Mockito.clearInvocations(mWindow);
        mNavColorController.onBottomAttachedColorChanged(null, false, false);
        assertFalse(
                "Should no longer be using the bottom attached UI color.",
                mNavColorController.getUseBottomAttachedUiColorForTesting());
        assertNavBarColor(Color.BLUE);
        assertNavBarDividerColor(Color.BLUE);
        assertWindowNavBarColor(Color.TRANSPARENT);
        verify(mWindow, times(0)).setNavigationBarDividerColor(anyInt());
    }

    @Test
    public void testMatchBottomAttachedColor_forceShowDivider() {
        when(mTab.getBackgroundColor()).thenReturn(Color.BLUE);
        when(mLayoutManager.getActiveLayoutType()).thenReturn(LayoutType.BROWSING);
        mNavColorController.updateActiveTabForTesting();
        runColorUpdateAnimation();

        mNavColorController.onBottomAttachedColorChanged(Color.RED, true, true);
        runColorUpdateAnimation();
        assertWindowNavBarColor(Color.RED);
        assertWindowNavBarDividerColor(NAV_DIVIDER_COLOR);
    }

    @Test
    public void testMatchBottomAttachedColor_forceShowDivider_toEdge() {
        when(mTab.getBackgroundColor()).thenReturn(Color.BLUE);
        when(mLayoutManager.getActiveLayoutType()).thenReturn(LayoutType.BROWSING);
        when(mEdgeToEdgeController.isDrawingToEdge()).thenReturn(true);
        mNavColorController.updateActiveTabForTesting();
        runColorUpdateAnimation();

        Mockito.clearInvocations(mWindow);
        mNavColorController.onBottomAttachedColorChanged(Color.RED, true, false);
        runColorUpdateAnimation();
        assertTrue(
                "Should be using the bottom attached UI color.",
                mNavColorController.getUseBottomAttachedUiColorForTesting());
        assertNavBarColor(Color.RED);
        assertNavBarDividerColor(NAV_DIVIDER_COLOR, true);
        assertWindowNavBarColor(Color.TRANSPARENT);
        verify(mWindow, times(0)).setNavigationBarDividerColor(anyInt());

        Mockito.clearInvocations(mWindow);
        mNavColorController.onBottomAttachedColorChanged(null, false, false);
        runColorUpdateAnimation();
        assertFalse(
                "Should no longer be using the bottom attached UI color.",
                mNavColorController.getUseBottomAttachedUiColorForTesting());
        assertNavBarColor(Color.BLUE);
        assertNavBarDividerColor(Color.BLUE);
        assertWindowNavBarColor(Color.TRANSPARENT);
        verify(mWindow, times(0)).setNavigationBarDividerColor(anyInt());
    }

    @Test
    public void testGetNavigationBarDividerColor() {
        assertEquals(
                "The nav bar divider color should be the bottom attached UI color.",
                NAV_DIVIDER_COLOR,
                mNavColorController.getNavigationBarDividerColor(false, true));
        assertEquals(
                "The nav bar divider color should match the tab background.",
                mContext.getColor(R.color.bottom_system_nav_divider_color_light),
                mNavColorController.getNavigationBarDividerColor(true, true));
    }

    @Test
    public void testMatchTabBackgroundColor() {
        when(mTab.getBackgroundColor()).thenReturn(Color.BLUE);
        when(mLayoutManager.getActiveLayoutType()).thenReturn(LayoutType.BROWSING);
        when(mEdgeToEdgeController.getBottomInset()).thenReturn(100);
        when(mEdgeToEdgeController.isDrawingToEdge()).thenReturn(false);

        mNavColorController.updateActiveTabForTesting();
        runColorUpdateAnimation();

        assertTrue(
                "Should be using tab background color for the navigation bar color.",
                mNavColorController.getUseActiveTabColorForTesting());
        assertNavBarColor(Color.BLUE);
        assertNavBarDividerColor(Color.BLUE);
        assertWindowNavBarColor(Color.BLUE);
        assertWindowNavBarDividerColor(Color.BLUE);
    }

    @Test
    public void testMatchTabBackgroundColor_toEdge() {
        when(mTab.getBackgroundColor()).thenReturn(Color.BLUE);
        when(mLayoutManager.getActiveLayoutType()).thenReturn(LayoutType.BROWSING);
        when(mEdgeToEdgeController.getBottomInset()).thenReturn(100);
        when(mEdgeToEdgeController.isDrawingToEdge()).thenReturn(true);
        when(mWindow.getNavigationBarColor()).thenReturn(Color.RED);

        Mockito.clearInvocations(mWindow);
        mNavColorController.updateActiveTabForTesting();
        runColorUpdateAnimation();

        assertTrue(
                "Should be using tab background color for the navigation bar color.",
                mNavColorController.getUseActiveTabColorForTesting());
        assertNavBarColor(Color.BLUE);
        assertNavBarDividerColor(Color.BLUE);
        assertWindowNavBarColor(Color.TRANSPARENT);
        verify(mWindow, times(0)).setNavigationBarDividerColor(anyInt());
    }

    private void runColorUpdateAnimation() {
        // Run the color  transition animation so color is applied to the window.
        ShadowLooper.idleMainLooper();
    }

    private void assertNavBarColor(int color) {
        assertEquals(
                "The nav bar color should match the active tab.",
                color,
                mNavColorController.getNavigationBarColorForTesting());
        assertEquals(
                "New color is not delivered to the observer.",
                color,
                (int) mNavigationBarColorChangedCaptor.getValue());
    }

    private void assertWindowNavBarColor(int color) {
        assertEquals(
                "The window (OS) nav bar color is different.",
                color,
                (int) mWindowColorCaptor.getValue());
    }

    private void assertNavBarDividerColor(int color) {
        assertNavBarDividerColor(color, /* forceShowDivider= */ false);
    }

    private void assertNavBarDividerColor(int color, boolean forceShowDivider) {
        assertEquals(
                "Incorrect nav bar divider color.",
                color,
                mNavColorController.getNavigationBarDividerColor(false, forceShowDivider));
        assertEquals(
                "New color is not delivered to the observer.",
                color,
                (int) mNavigationBarDividerColorChangedCaptor.getValue());
    }

    private void assertWindowNavBarDividerColor(int color) {
        assertEquals(
                "Incorrect divider color set to the window.",
                color,
                (int) mWindowDividerColorCaptor.getValue());
    }
}