chromium/chrome/android/javatests/src/org/chromium/chrome/browser/accessibility/settings/AccessibilitySettingsTest.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.accessibility.settings;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.chromium.chrome.browser.accessibility.settings.AccessibilitySettings.PREF_IMAGE_DESCRIPTIONS;

import android.app.Instrumentation;
import android.content.Intent;
import android.content.IntentFilter;
import android.provider.Settings;
import android.view.View;

import androidx.preference.Preference;
import androidx.test.espresso.action.ViewActions;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.components.browser_ui.accessibility.FontSizePrefs;
import org.chromium.components.browser_ui.accessibility.PageZoomPreference;
import org.chromium.components.browser_ui.accessibility.PageZoomUtils;
import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.test.util.UiUtils;
import org.chromium.ui.accessibility.AccessibilityState;
import org.chromium.ui.test.util.ViewUtils;
import org.chromium.ui.widget.ChromeImageButton;

import java.text.NumberFormat;

/**
 * Tests for the Accessibility Settings menu.
 *
 * <p>TODO(crbug.com/40214849): This tests the class in //components/browser_ui, but we don't have a
 * good way of testing with native code there.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@Features.DisableFeatures({
    ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM_ENHANCEMENTS,
    ContentFeatureList.SMART_ZOOM
})
@Features.EnableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM})
public class AccessibilitySettingsTest {
    private AccessibilitySettings mAccessibilitySettings;
    private PageZoomPreference mPageZoomPref;

    @Rule
    public SettingsActivityTestRule<AccessibilitySettings> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(AccessibilitySettings.class);

    @Before
    public void setUp() {
        // Enable screen reader to display all settings options.
        ThreadUtils.runOnUiThreadBlocking(
                () -> AccessibilityState.setIsScreenReaderEnabledForTesting(true));

        mSettingsActivityTestRule.startSettingsActivity();
        mAccessibilitySettings = mSettingsActivityTestRule.getFragment();
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> AccessibilityState.setIsScreenReaderEnabledForTesting(false));
    }

    // Generic AccessibilitySettings tests (no feature flag dependency).

    /**
     * Tests setting FontScaleFactor and ForceEnableZoom in AccessibilitySettings and ensures that
     * ForceEnableZoom changes corresponding to FontScaleFactor.
     */
    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @DisableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM})
    public void testAccessibilitySettings() throws Exception {
        TextScalePreference textScalePref =
                (TextScalePreference)
                        mAccessibilitySettings.findPreference(
                                AccessibilitySettings.PREF_TEXT_SCALE);
        ChromeSwitchPreference forceEnableZoomPref =
                (ChromeSwitchPreference)
                        mAccessibilitySettings.findPreference(
                                AccessibilitySettings.PREF_FORCE_ENABLE_ZOOM);
        NumberFormat percentFormat = NumberFormat.getPercentInstance();
        // Arbitrary value 0.4f to be larger and smaller than threshold.
        float fontSmallerThanThreshold =
                FontSizePrefs.FORCE_ENABLE_ZOOM_THRESHOLD_MULTIPLIER - 0.4f;
        float fontBiggerThanThreshold = FontSizePrefs.FORCE_ENABLE_ZOOM_THRESHOLD_MULTIPLIER + 0.4f;

        // Set the textScaleFactor above the threshold.
        userSetTextScale(mAccessibilitySettings, textScalePref, fontBiggerThanThreshold);
        UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
        // Since above the threshold, this will check the force enable zoom button.
        Assert.assertEquals(
                percentFormat.format(fontBiggerThanThreshold), textScalePref.getAmountForTesting());
        Assert.assertTrue(forceEnableZoomPref.isChecked());
        assertFontSizePrefs(true, fontBiggerThanThreshold);

        // Set the textScaleFactor below the threshold.
        userSetTextScale(mAccessibilitySettings, textScalePref, fontSmallerThanThreshold);
        UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
        // Since below the threshold and userSetForceEnableZoom is false, this will uncheck
        // the force enable zoom button.
        Assert.assertEquals(
                percentFormat.format(fontSmallerThanThreshold),
                textScalePref.getAmountForTesting());
        Assert.assertFalse(forceEnableZoomPref.isChecked());
        assertFontSizePrefs(false, fontSmallerThanThreshold);

        userSetTextScale(mAccessibilitySettings, textScalePref, fontBiggerThanThreshold);
        // Sets onUserSetForceEnableZoom to be true.
        userSetForceEnableZoom(mAccessibilitySettings, forceEnableZoomPref, true);
        UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
        // Since userSetForceEnableZoom is true, when the text scale is moved below the threshold
        // ForceEnableZoom should remain checked.
        userSetTextScale(mAccessibilitySettings, textScalePref, fontSmallerThanThreshold);
        Assert.assertTrue(forceEnableZoomPref.isChecked());
        assertFontSizePrefs(true, fontSmallerThanThreshold);
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @DisableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM})
    public void testChangedFontPrefSavedOnStop() {
        TextScalePreference textScalePref =
                mAccessibilitySettings.findPreference(AccessibilitySettings.PREF_TEXT_SCALE);

        // Change text scale a couple of times.
        userSetTextScale(mAccessibilitySettings, textScalePref, 0.5f);
        userSetTextScale(mAccessibilitySettings, textScalePref, 1.75f);

        Assert.assertEquals(
                "Histogram should not be recorded yet.",
                0,
                RecordHistogram.getHistogramTotalCountForTesting(
                        FontSizePrefs.FONT_SIZE_CHANGE_HISTOGRAM));

        // Simulate activity stopping.
        ThreadUtils.runOnUiThreadBlocking(() -> mAccessibilitySettings.onStop());

        Assert.assertEquals(
                "Histogram should have been recorded once.",
                1,
                RecordHistogram.getHistogramTotalCountForTesting(
                        FontSizePrefs.FONT_SIZE_CHANGE_HISTOGRAM));
        Assert.assertEquals(
                "Histogram should have recorded final value.",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        FontSizePrefs.FONT_SIZE_CHANGE_HISTOGRAM, 175));
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @DisableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM})
    public void testUnchangedFontPrefNotSavedOnStop() {
        // Simulate activity stopping.
        ThreadUtils.runOnUiThreadBlocking(() -> mAccessibilitySettings.onStop());
        Assert.assertEquals(
                "Histogram should not have been recorded.",
                0,
                RecordHistogram.getHistogramTotalCountForTesting(
                        FontSizePrefs.FONT_SIZE_CHANGE_HISTOGRAM));
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testCaptionPreferences() {
        Preference captionsPref =
                mAccessibilitySettings.findPreference(AccessibilitySettings.PREF_CAPTIONS);
        Assert.assertNotNull(captionsPref);
        Assert.assertNotNull(captionsPref.getOnPreferenceClickListener());

        Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(
                                new IntentFilter(Settings.ACTION_CAPTIONING_SETTINGS), null, false);

        // First scroll to the Captions preference, then click.
        onView(withId(R.id.recycler_view))
                .perform(
                        RecyclerViewActions.scrollTo(
                                hasDescendant(withText(R.string.accessibility_captions_title))));
        onView(withText(R.string.accessibility_captions_title)).perform(click());
        monitor.waitForActivityWithTimeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL);
        Assert.assertEquals("Monitor for has not been called", 1, monitor.getHits());
        InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testImageDescriptionsPreferences_Enabled() {
        Preference imageDescriptionsPref =
                mAccessibilitySettings.findPreference(PREF_IMAGE_DESCRIPTIONS);

        Assert.assertNotNull(imageDescriptionsPref);
        Assert.assertTrue(
                "Image Descriptions option should be visible", imageDescriptionsPref.isVisible());

        Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(new IntentFilter(Intent.ACTION_MAIN), null, true);

        // First scroll to the Image Descriptions preference, then click.
        onView(withId(R.id.recycler_view))
                .perform(
                        RecyclerViewActions.scrollTo(
                                hasDescendant(
                                        withText(R.string.image_descriptions_settings_title))));
        onView(withText(R.string.image_descriptions_settings_title)).perform(click());

        // The activity is blocked, so just wait for the ActivityMonitor to capture an Intent.
        CriteriaHelper.pollInstrumentationThread(
                () -> monitor.getHits() >= 1, "Clicking image descriptions should open subpage");

        InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
    }

    // Tests related to Page Zoom feature.

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @DisableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM})
    public void testPageZoomPreference_hiddenWhenDisabled() {
        mPageZoomPref =
                (PageZoomPreference)
                        mAccessibilitySettings.findPreference(
                                AccessibilitySettings.PREF_PAGE_ZOOM_DEFAULT_ZOOM);
        Assert.assertNotNull(mPageZoomPref);
        Assert.assertFalse(
                "Page Zoom default zoom option should not be visible when disabled",
                mPageZoomPref.isVisible());
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_visibleWhenEnabled() {
        getPageZoomPref();
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_decreaseButtonUpdatesValue() {
        getPageZoomPref();

        int startingVal = mPageZoomPref.getZoomSliderForTesting().getProgress();
        onView(withId(R.id.page_zoom_decrease_zoom_button)).perform(click());
        Assert.assertTrue(startingVal > mPageZoomPref.getZoomSliderForTesting().getProgress());
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_decreaseButtonProperlyDisabled() {
        getPageZoomPref();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPageZoomPref.setZoomValueForTesting(0);
                });
        onView(withId(R.id.page_zoom_decrease_zoom_button)).check(matches(sDisabled));
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_increaseButtonUpdatesValue() {
        getPageZoomPref();

        int startingVal = mPageZoomPref.getZoomSliderForTesting().getProgress();
        onView(withId(R.id.page_zoom_increase_zoom_button)).perform(click());
        Assert.assertTrue(startingVal < mPageZoomPref.getZoomSliderForTesting().getProgress());
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_increaseButtonProperlyDisabled() {
        getPageZoomPref();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPageZoomPref.setZoomValueForTesting(
                            PageZoomUtils.PAGE_ZOOM_MAXIMUM_SEEKBAR_VALUE);
                });
        onView(withId(R.id.page_zoom_increase_zoom_button)).check(matches(sDisabled));
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_zoomSliderUpdatesValue() {
        getPageZoomPref();
        int startingVal = mPageZoomPref.getZoomSliderForTesting().getProgress();
        onView(withId(R.id.page_zoom_slider)).perform(ViewActions.swipeRight());
        Assert.assertNotEquals(startingVal, mPageZoomPref.getZoomSliderForTesting().getProgress());
    }

    // Tests related to Page Zoom Enhancements (v2) feature.

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM_ENHANCEMENTS})
    public void testPageZoomPreference_savedZoomLevelsPreference_visibleWhenEnabled() {
        Preference zoomInfoPref =
                mAccessibilitySettings.findPreference(AccessibilitySettings.PREF_ZOOM_INFO);
        Assert.assertNotNull(zoomInfoPref);
        Assert.assertNotNull(zoomInfoPref.getOnPreferenceClickListener());

        Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(new IntentFilter(), null, false);

        // First scroll to the "Saved zoom levels" preference, then click.
        onView(withId(R.id.recycler_view))
                .perform(
                        RecyclerViewActions.scrollTo(
                                hasDescendant(withText(R.string.zoom_info_preference_title))));
        onView(withText(R.string.zoom_info_preference_title)).perform(click());

        // The activity is blocked, so just wait for the ActivityMonitor to capture an Intent.
        CriteriaHelper.pollInstrumentationThread(
                () -> monitor.getHits() >= 1,
                "Clicking 'Saved zoom levels' should open Site Settings page.");

        InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @DisableFeatures({ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM_ENHANCEMENTS})
    public void testPageZoomPreference_savedZoomLevelsPreference_hiddenWhenDisables() {
        Preference zoomInfoPref =
                mAccessibilitySettings.findPreference(AccessibilitySettings.PREF_ZOOM_INFO);
        Assert.assertNotNull(zoomInfoPref);
        Assert.assertFalse(
                "Saved Zoom Levels link should not be visible when disabled",
                zoomInfoPref.isVisible());
    }

    // Tests related to the Smart Zoom feature.

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    public void testPageZoomPreference_smartZoom_hiddenWhenDisabled() {
        getPageZoomPref();
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_title), ViewUtils.VIEW_GONE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_summary), ViewUtils.VIEW_GONE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_current_value_text), ViewUtils.VIEW_GONE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_decrease_zoom_button), ViewUtils.VIEW_GONE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_slider), ViewUtils.VIEW_GONE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_increase_zoom_button), ViewUtils.VIEW_GONE);
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.SMART_ZOOM})
    public void testPageZoomPreference_smartZoom_visibleWhenEnabled() {
        getPageZoomPref();
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_title), ViewUtils.VIEW_VISIBLE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_summary), ViewUtils.VIEW_VISIBLE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_current_value_text), ViewUtils.VIEW_VISIBLE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_decrease_zoom_button), ViewUtils.VIEW_VISIBLE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_slider), ViewUtils.VIEW_VISIBLE);
        ViewUtils.waitForViewCheckingState(
                withId(R.id.text_size_contrast_increase_zoom_button), ViewUtils.VIEW_VISIBLE);
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.SMART_ZOOM})
    public void testPageZoomPreference_smartZoom_decreaseButtonUpdatesValue() {
        getPageZoomPref();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPageZoomPref.setTextContrastValueForTesting(20);
                });
        int startingVal = mPageZoomPref.getTextSizeContrastSliderForTesting().getProgress();
        onView(withId(R.id.text_size_contrast_decrease_zoom_button)).perform(click());
        Assert.assertTrue(
                startingVal > mPageZoomPref.getTextSizeContrastSliderForTesting().getProgress());
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.SMART_ZOOM})
    public void testPageZoomPreference_smartZoom_decreaseButtonProperlyDisabled() {
        getPageZoomPref();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPageZoomPref.setTextContrastValueForTesting(0);
                });
        onView(withId(R.id.text_size_contrast_decrease_zoom_button)).check(matches(sDisabled));
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.SMART_ZOOM})
    public void testPageZoomPreference_smartZoom_increaseButtonUpdatesValue() {
        getPageZoomPref();

        int startingVal = mPageZoomPref.getTextSizeContrastSliderForTesting().getProgress();
        onView(withId(R.id.text_size_contrast_increase_zoom_button)).perform(click());
        Assert.assertTrue(
                startingVal < mPageZoomPref.getTextSizeContrastSliderForTesting().getProgress());
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.SMART_ZOOM})
    public void testPageZoomPreference_smartZoom_increaseButtonProperlyDisabled() {
        getPageZoomPref();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPageZoomPref.setTextContrastValueForTesting(
                            PageZoomUtils.TEXT_SIZE_CONTRAST_MAX_LEVEL);
                });
        onView(withId(R.id.text_size_contrast_increase_zoom_button)).check(matches(sDisabled));
    }

    @Test
    @SmallTest
    @Feature({"Accessibility"})
    @EnableFeatures({ContentFeatureList.SMART_ZOOM})
    public void testPageZoomPreference_smartZoom_zoomSliderUpdatesValue() {
        getPageZoomPref();
        int startingVal = mPageZoomPref.getTextSizeContrastSliderForTesting().getProgress();
        onView(withId(R.id.text_size_contrast_slider)).perform(ViewActions.swipeRight());
        Assert.assertNotEquals(
                startingVal, mPageZoomPref.getTextSizeContrastSliderForTesting().getProgress());
    }

    // Helper methods.

    private static final BaseMatcher<View> sDisabled =
            new BaseMatcher<View>() {
                @Override
                public boolean matches(Object o) {
                    return !((ChromeImageButton) o).isEnabled();
                }

                @Override
                public void describeTo(Description description) {
                    description.appendText("View was enabled, but should have been disabled.");
                }
            };

    private void getPageZoomPref() {
        mPageZoomPref =
                (PageZoomPreference)
                        mAccessibilitySettings.findPreference(
                                AccessibilitySettings.PREF_PAGE_ZOOM_DEFAULT_ZOOM);
        Assert.assertNotNull(mPageZoomPref);
        Assert.assertTrue(
                "Page Zoom pref should be visible when enabled.", mPageZoomPref.isVisible());
    }

    private void assertFontSizePrefs(
            final boolean expectedForceEnableZoom, final float expectedFontScale) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    FontSizePrefs fontSizePrefs =
                            FontSizePrefs.getInstance(ProfileManager.getLastUsedRegularProfile());
                    Assert.assertEquals(
                            expectedForceEnableZoom, fontSizePrefs.getForceEnableZoom());
                    Assert.assertEquals(
                            expectedFontScale, fontSizePrefs.getFontScaleFactor(), 0.001f);
                });
    }

    private static void userSetTextScale(
            final AccessibilitySettings accessibilitySettings,
            final TextScalePreference textScalePref,
            final float textScale) {
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> accessibilitySettings.onPreferenceChange(textScalePref, textScale));
    }

    private static void userSetForceEnableZoom(
            final AccessibilitySettings accessibilitySettings,
            final ChromeSwitchPreference forceEnableZoomPref,
            final boolean enabled) {
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> accessibilitySettings.onPreferenceChange(forceEnableZoomPref, enabled));
    }
}