chromium/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/settings/ThemeSettingsFragmentTest.java

// Copyright 2019 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.night_mode.settings;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;

import static org.chromium.chrome.browser.flags.ChromeFeatureList.DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING;
import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.UI_THEME_SETTING;

import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
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.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.night_mode.NightModeMetrics.ThemeSettingsEntry;
import org.chromium.chrome.browser.night_mode.NightModeUtils;
import org.chromium.chrome.browser.night_mode.R;
import org.chromium.chrome.browser.night_mode.ThemeType;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.settings.BlankUiTestActivitySettingsTestRule;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridgeJni;
import org.chromium.components.browser_ui.widget.RadioButtonWithDescription;
import org.chromium.components.browser_ui.widget.RadioButtonWithDescriptionLayout;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.feature_engagement.Tracker;

/** Tests for ThemeSettingsFragment. */
@RunWith(BaseJUnit4ClassRunner.class)
@DisableFeatures(DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING)
public class ThemeSettingsFragmentTest {

    @Rule
    public BlankUiTestActivitySettingsTestRule mSettingsTestRule =
            new BlankUiTestActivitySettingsTestRule();

    @Rule public JniMocker mMocker = new JniMocker();

    @Mock public WebsitePreferenceBridge.Natives mMockWebsitePreferenceBridgeJni;
    @Mock public Profile mProfile;
    @Mock public Tracker mTracker;

    private ThemeSettingsFragment mFragment;
    private RadioButtonGroupThemePreference mPreference;

    // Boolean used for web content auto dark mode.
    private boolean mForceDarkModeEnabled;

    @Before
    public void setUp() {
        // For some reason MockitoRule does not work with JniMocker (seems like an order issue), and
        // RuleChain cannot be applied to MockitoRule since it is not a TestRule.
        MockitoAnnotations.initMocks(this);
        ChromeSharedPreferences.getInstance().removeKey(UI_THEME_SETTING);

        mMocker.mock(WebsitePreferenceBridgeJni.TEST_HOOKS, mMockWebsitePreferenceBridgeJni);

        TrackerFactory.setTrackerForTests(mTracker);

        // Default value for feature DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING.
        mForceDarkModeEnabled = true;
        Mockito.doAnswer(invocation -> mForceDarkModeEnabled)
                .when(mMockWebsitePreferenceBridgeJni)
                .isContentSettingEnabled(any(), eq(ContentSettingsType.AUTO_DARK_WEB_CONTENT));
        Mockito.doAnswer(
                        invocation -> {
                            mForceDarkModeEnabled = (boolean) invocation.getArguments()[2];
                            return null;
                        })
                .when(mMockWebsitePreferenceBridgeJni)
                .setContentSettingEnabled(
                        any(), eq(ContentSettingsType.AUTO_DARK_WEB_CONTENT), anyBoolean());
    }

    @Test
    @SmallTest
    @Feature({"Themes"})
    public void testSelectThemes() {
        launchThemeSettings(ThemeSettingsEntry.SETTINGS);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    int expectedDefaultTheme = ThemeType.LIGHT;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        Assert.assertFalse(
                                "Q should not default to light.",
                                NightModeUtils.isNightModeDefaultToLight());
                        expectedDefaultTheme = ThemeType.SYSTEM_DEFAULT;
                    }

                    Assert.assertEquals(
                            "Incorrect default theme setting.",
                            expectedDefaultTheme,
                            NightModeUtils.getThemeSetting());
                    assertButtonCheckedCorrectly("Light", expectedDefaultTheme);

                    // Select System default
                    Assert.assertEquals(R.id.system_default, getButton(0).getId());
                    selectButton(0);
                    assertButtonCheckedCorrectly("System default", 0);
                    Assert.assertEquals(ThemeType.SYSTEM_DEFAULT, mPreference.getSetting());
                    Assert.assertEquals(
                            mPreference.getSetting(),
                            ChromeSharedPreferences.getInstance().readInt(UI_THEME_SETTING));

                    // Select Light
                    Assert.assertEquals(R.id.light, getButton(1).getId());
                    selectButton(1);
                    assertButtonCheckedCorrectly("Light", 1);
                    Assert.assertEquals(ThemeType.LIGHT, mPreference.getSetting());
                    Assert.assertEquals(
                            mPreference.getSetting(),
                            ChromeSharedPreferences.getInstance().readInt(UI_THEME_SETTING));

                    // Select Dark
                    Assert.assertEquals(R.id.dark, getButton(2).getId());
                    selectButton(2);
                    assertButtonCheckedCorrectly("Dark", 2);
                    Assert.assertEquals(ThemeType.DARK, mPreference.getSetting());
                    Assert.assertEquals(
                            mPreference.getSetting(),
                            ChromeSharedPreferences.getInstance().readInt(UI_THEME_SETTING));
                });
    }

    @Test
    @SmallTest
    @Feature({"Themes"})
    @EnableFeatures(DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING)
    public void testDarkenWebsiteButton() {
        launchThemeSettings(ThemeSettingsEntry.SETTINGS);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    int expectedDefaultTheme = ThemeType.LIGHT;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        Assert.assertFalse(
                                "Q should not default to light.",
                                NightModeUtils.isNightModeDefaultToLight());
                        expectedDefaultTheme = ThemeType.SYSTEM_DEFAULT;
                    }

                    LinearLayout checkboxContainer = mPreference.getCheckboxContainerForTesting();
                    RadioButtonWithDescriptionLayout group = mPreference.getGroupForTesting();

                    Assert.assertEquals(
                            "Incorrect default theme setting.",
                            expectedDefaultTheme,
                            NightModeUtils.getThemeSetting());
                    assertButtonCheckedCorrectly("Light", expectedDefaultTheme);

                    // Select System default
                    selectButton(0);
                    Assert.assertTrue(
                            "Darken website button should be visible when system default is"
                                    + " checked",
                            checkboxContainer.getVisibility() == View.VISIBLE);
                    Assert.assertEquals(
                            "Darken website button should be below the system default option when"
                                    + " system default is checked",
                            1,
                            group.indexOfChild(checkboxContainer));

                    // Select Light
                    selectButton(1);
                    Assert.assertTrue(
                            "Darken website button should be invisible when light theme is checked",
                            checkboxContainer.getVisibility() != View.VISIBLE);

                    // Select Dark
                    selectButton(2);
                    Assert.assertTrue(
                            "Darken website button should be visible when dark theme is checked",
                            checkboxContainer.getVisibility() == View.VISIBLE);
                    Assert.assertEquals(
                            "Darken website button should be below the dark theme option"
                                    + " when dark theme is checked",
                            3,
                            group.indexOfChild(checkboxContainer));

                    // Checks for Darken website button.
                    Assert.assertTrue(
                            "Darken website check box should be checked by default.",
                            mPreference.isDarkenWebsitesEnabled());

                    // Toggle the check box.
                    checkboxContainer.performClick();
                    Assert.assertFalse(
                            "Darken website check box should be unchecked.",
                            mPreference.isDarkenWebsitesEnabled());
                    Mockito.verify(mMockWebsitePreferenceBridgeJni, Mockito.times(1))
                            .setContentSettingEnabled(
                                    any(),
                                    eq(ContentSettingsType.AUTO_DARK_WEB_CONTENT),
                                    eq(false));

                    checkboxContainer.performClick();
                    Assert.assertTrue(
                            "Darken website check box should be checked again.",
                            mPreference.isDarkenWebsitesEnabled());
                    Mockito.verify(mMockWebsitePreferenceBridgeJni, Mockito.times(1))
                            .setContentSettingEnabled(
                                    any(), eq(ContentSettingsType.AUTO_DARK_WEB_CONTENT), eq(true));

                    // Check system default, darken website button should stay checked.
                    selectButton(1);
                    Assert.assertTrue(
                            "Darken website button should stay its state when changing theme"
                                    + " preference.",
                            mPreference.isDarkenWebsitesEnabled());
                    Mockito.verify(mMockWebsitePreferenceBridgeJni, Mockito.times(1))
                            .setContentSettingEnabled(
                                    any(), eq(ContentSettingsType.AUTO_DARK_WEB_CONTENT), eq(true));
                });
    }

    @Test
    @SmallTest
    public void testStartThemeSettings_FromAutoDarkMessages() {
        launchThemeSettings(ThemeSettingsEntry.AUTO_DARK_MODE_MESSAGE);
    }

    private void launchThemeSettings(@ThemeSettingsEntry Integer settingsEntry) {
        Bundle args = new Bundle();
        args.putInt(ThemeSettingsFragment.KEY_THEME_SETTINGS_ENTRY, settingsEntry);
        mSettingsTestRule.launchPreference(
                ThemeSettingsFragment.class,
                args,
                (fragment) -> ((ThemeSettingsFragment) fragment).setProfile(mProfile));

        mFragment = (ThemeSettingsFragment) mSettingsTestRule.getPreferenceFragment();
        mPreference =
                (RadioButtonGroupThemePreference)
                        mFragment.findPreference(ThemeSettingsFragment.PREF_UI_THEME_PREF);
        assertThemeSettingsEntryRecorded(settingsEntry);
    }

    private RadioButtonWithDescription getButton(int index) {
        return (RadioButtonWithDescription) mPreference.getButtonsForTesting().get(index);
    }

    private void selectButton(int index) {
        getButton(index).onClick(null);
    }

    private boolean isRestUnchecked(int selectedIndex) {
        for (int i = 0; i < ThemeType.NUM_ENTRIES; i++) {
            if (i != selectedIndex && getButton(i).isChecked()) {
                return false;
            }
        }
        return true;
    }

    private void assertButtonCheckedCorrectly(String buttonTitle, int index) {
        Assert.assertTrue(buttonTitle + " button should be checked.", getButton(index).isChecked());
        Assert.assertTrue(
                "Buttons except " + buttonTitle + " should be unchecked.", isRestUnchecked(index));
    }

    private void assertThemeSettingsEntryRecorded(int sample) {
        Assert.assertEquals(
                "<Android.DarkTheme.ThemeSettingsEntry> should be recorded once.",
                1,
                RecordHistogram.getHistogramTotalCountForTesting(
                        "Android.DarkTheme.ThemeSettingsEntry"));
        Assert.assertEquals(
                "<Android.DarkTheme.ThemeSettingsEntry> should be recorded once for sample <"
                        + sample
                        + ">.",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.DarkTheme.ThemeSettingsEntry", sample));
    }
}