chromium/chrome/test/data/webui/chromeos/settings/os_a11y_page/keyboard_and_text_input_page_test.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://os-settings/lazy_load.js';

import {SettingsKeyboardAndTextInputPageElement} from 'chrome://os-settings/lazy_load.js';
import {CrLinkRowElement, CrSettingsPrefs, Router, routes, SettingsPrefsElement, SettingsSliderElement, SettingsToggleButtonElement} from 'chrome://os-settings/os_settings.js';
import {assert} from 'chrome://resources/js/assert.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.js';
import {pressAndReleaseKeyOn} from 'chrome://resources/polymer/v3_0/iron-test-helpers/mock-interactions.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertNotEquals, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

suite('<settings-keyboard-and-text-input-page>', () => {
  let page: SettingsKeyboardAndTextInputPageElement;
  let prefElement: SettingsPrefsElement;

  async function initPage() {
    prefElement = document.createElement('settings-prefs');
    document.body.appendChild(prefElement);
    await CrSettingsPrefs.initialized;

    page = document.createElement('settings-keyboard-and-text-input-page');
    page.prefs = prefElement.prefs;
    document.body.appendChild(page);
    flush();
  }

  setup(() => {
    Router.getInstance().navigateTo(routes.A11Y_KEYBOARD_AND_TEXT_INPUT);
  });

  teardown(() => {
    page.remove();
    prefElement.remove();
    Router.getInstance().resetRouteForTesting();
  });

  test('Dictation labels', async () => {
    // Ensure that the Dictation locale menu is shown by setting the dictation
    // pref to true (done in default prefs) and populating dictation locale
    // options with mock data.
    await initPage();

    page.setPrefValue('settings.a11y.dictation', true);
    page.setPrefValue('settings.a11y.dictation_locale', 'en-US');

    const locales = [{
      name: 'English (United States)',
      worksOffline: true,
      installed: true,
      recommended: true,
      value: 'en-US',
    }];
    webUIListenerCallback('dictation-locales-set', locales);
    flush();

    // Dictation toggle.
    const dictationSetting =
        page.shadowRoot!.querySelector<SettingsToggleButtonElement>(
            '#enableDictation');
    assert(dictationSetting);
    assertTrue(dictationSetting.checked);
    assertEquals('Dictation', dictationSetting.label);
    assertEquals(
        'Type with your voice. Use Search + D, then start speaking.',
        dictationSetting.subLabel);

    // Dictation locale menu.
    const dictationLocaleMenuLabel =
        page.shadowRoot!.querySelector<HTMLElement>(
            '#dictationLocaleMenuLabel');
    const dictationLocaleMenuSubtitle =
        page.shadowRoot!.querySelector<HTMLElement>(
            '#dictationLocaleMenuSubtitle');
    assert(dictationLocaleMenuLabel);
    assert(dictationLocaleMenuSubtitle);
    assertEquals('Language', dictationLocaleMenuLabel.innerText);
    assertEquals(
        'English (United States) is processed locally and works offline',
        dictationLocaleMenuSubtitle.innerText);

    // Fake a request to change the dictation locale menu subtitle.
    webUIListenerCallback('dictation-locale-menu-subtitle-changed', 'Testing');
    flush();

    // Only the dictation locale subtitle should have changed.
    assertEquals('Dictation', dictationSetting.label);
    assertEquals(
        'Type with your voice. Use Search + D, then start speaking.',
        dictationSetting.subLabel);
    assertEquals('Language', dictationLocaleMenuLabel.innerText);
    assertEquals('Testing', dictationLocaleMenuSubtitle.innerText);
  });

  test('Test computeDictationLocaleSubtitle_()', async () => {
    await initPage();
    const locales = [
      {
        name: 'English (United States)',
        worksOffline: true,
        installed: true,
        recommended: true,
        value: 'en-US',
      },
      {
        name: 'Spanish',
        worksOffline: true,
        installed: false,
        recommended: false,
        value: 'es',
      },
      {
        name: 'German',
        // Note: this data should never occur in practice. If a locale isn't
        // supported offline, then it should never be installed. Test this case
        // to verify our code still works given unexpected input.
        worksOffline: false,
        installed: true,
        recommended: false,
        value: 'de',
      },
      {
        name: 'French (France)',
        worksOffline: false,
        installed: false,
        recommended: false,
        value: 'fr-FR',
      },
    ];
    webUIListenerCallback('dictation-locales-set', locales);
    flush();

    page.setPrefValue('settings.a11y.dictation_locale', 'en-US');
    flush();
    assertEquals(
        'English (United States) is processed locally and works offline',
        page.get('dictationLocaleMenuSubtitle_'));

    // Changing the Dictation locale pref should change the subtitle
    // computation.
    page.setPrefValue('settings.a11y.dictation_locale', 'es');
    assertEquals(
        'Couldn’t download Spanish speech files. Download will be attempted ' +
            'later. Speech is sent to Google for processing until download ' +
            'is completed.',
        page.get('dictationLocaleMenuSubtitle_'));

    page.setPrefValue('settings.a11y.dictation_locale', 'de');
    assertEquals(
        'German speech is sent to Google for processing',
        page.get('dictationLocaleMenuSubtitle_'));


    // Only use the subtitle override once.
    page.set('useDictationLocaleSubtitleOverride_', true);
    page.set('dictationLocaleSubtitleOverride_', 'Testing');

    assertEquals('Testing', page.get('dictationLocaleMenuSubtitle_'));
    assertFalse(page.get('useDictationLocaleSubtitleOverride_'));
    page.setPrefValue('settings.a11y.dictation_locale', 'fr-FR');
    assertEquals(
        'French (France) speech is sent to Google for processing',
        page.get('dictationLocaleMenuSubtitle_'));
  });

  test('some parts are hidden in kiosk mode', async () => {
    loadTimeData.overrideValues({
      isKioskModeActive: true,
      showTabletModeShelfNavigationButtonsSettings: true,
    });
    await initPage();
    flush();

    const subpageLinks = page.root!.querySelectorAll('cr-link-row');
    subpageLinks.forEach(subpageLink => assertFalse(isVisible(subpageLink)));
  });

  test('Deep link to switch access', async () => {
    loadTimeData.overrideValues({
      isKioskModeActive: false,
    });
    await initPage();

    const params = new URLSearchParams();
    params.append('settingId', '1522');
    Router.getInstance().navigateTo(
        routes.A11Y_KEYBOARD_AND_TEXT_INPUT, params);

    flush();

    const deepLinkElement =
        page.shadowRoot!.querySelector('#enableSwitchAccess')!.shadowRoot!
            .querySelector('cr-toggle');
    assert(deepLinkElement);
    await waitAfterNextRender(deepLinkElement);
    assertEquals(
        deepLinkElement, getDeepActiveElement(),
        'Switch access toggle should be focused for settingId=1522.');
  });

  test('Caret blink interval setting', async () => {
    await initPage();
    if (!loadTimeData.getBoolean(
            'isAccessibilityCaretBlinkIntervalSettingEnabled')) {
      // Caret blink interval section should not be visible if flag is disabled.
      const caretBlinkIntervalRow =
          page.shadowRoot!.querySelector('#caretBlinkIntervalRow');
      assertNull(caretBlinkIntervalRow);
      return;
    }

    // Caret blink interval section is visible. Test it is connected to
    // the expected preference.
    const caretBlinkIntervalRow =
        page.shadowRoot!.querySelector('#caretBlinkIntervalRow');
    assert(caretBlinkIntervalRow);
    assertTrue(isVisible(caretBlinkIntervalRow));

    const caretBlinkIntervalSlider =
        page.shadowRoot!.querySelector<SettingsSliderElement>(
            '#caretBlinkIntervalSlider');
    assert(caretBlinkIntervalSlider);
    assertTrue(isVisible(caretBlinkIntervalSlider));

    // Check the default synthetic pref value is what is expected.
    assertEquals(100, caretBlinkIntervalSlider.pref.value);
    // Check the actual default interval is what is expected.
    assertEquals(500, page.prefs.settings.a11y.caret.blink_interval.value);

    const slider =
        caretBlinkIntervalSlider.shadowRoot!.querySelector('cr-slider');
    assert(slider);

    // Make the repeat interval shorter (increase the slider value).
    pressAndReleaseKeyOn(slider, 39, [], 'ArrowRight');
    await flushTasks();

    assertEquals(110, caretBlinkIntervalSlider.pref.value);
    assertEquals(455, page.prefs.settings.a11y.caret.blink_interval.value);

    pressAndReleaseKeyOn(slider, 39, [], 'ArrowRight');
    await flushTasks();

    assertEquals(120, caretBlinkIntervalSlider.pref.value);
    assertEquals(417, page.prefs.settings.a11y.caret.blink_interval.value);

    // Now use the left arrow to get the minimum value, which should be "don't
    // blink", aka 0.

    while (caretBlinkIntervalSlider.pref.value > 40) {
      pressAndReleaseKeyOn(slider, 37, [], 'ArrowLeft');
      await flushTasks();
    }

    assertEquals(40, caretBlinkIntervalSlider.pref.value);
    assertEquals(0, page.prefs.settings.a11y.caret.blink_interval.value);
  });

  const selectorRouteList = [
    {selector: '#keyboardSubpageButton', route: routes.KEYBOARD},
  ];

  selectorRouteList.forEach(({selector, route}) => {
    test(
        `should focus ${selector} button when returning from ${
            route.path} subpage`,
        async () => {
          await initPage();
          flush();
          const router = Router.getInstance();

          const subpageButton =
              page.shadowRoot!.querySelector<CrLinkRowElement>(selector);
          assert(subpageButton);

          subpageButton.click();
          assertEquals(route, router.currentRoute);
          assertNotEquals(
              subpageButton, page.shadowRoot!.activeElement,
              `${selector} should not be focused`);

          const popStateEventPromise = eventToPromise('popstate', window);
          router.navigateToPreviousRoute();
          await popStateEventPromise;
          await waitAfterNextRender(page);

          assertEquals(
              routes.A11Y_KEYBOARD_AND_TEXT_INPUT, router.currentRoute);
          assertEquals(
              subpageButton, page.shadowRoot!.activeElement,
              `${selector} should be focused`);
        });
  });

  const settingsToggleButtons = [
    {
      id: 'stickyKeysToggle',
      prefKey: 'settings.a11y.sticky_keys_enabled',
      cvoxTooltipId: 'stickyKeysDisabledTooltip',
    },
    {
      id: 'focusHighlightToggle',
      prefKey: 'settings.a11y.focus_highlight',
      cvoxTooltipId: 'focusHighlightDisabledTooltip',
    },
    {
      id: 'caretHighlightToggle',
      prefKey: 'settings.a11y.caret_highlight',
      cvoxTooltipId: '',
    },
    {
      id: 'caretBrowsingToggle',
      prefKey: 'settings.a11y.caretbrowsing.enabled',
      cvoxTooltipId: '',
    },
  ];

  settingsToggleButtons.forEach(({id, prefKey, cvoxTooltipId}) => {
    test(`Accessibility toggle button syncs to prefs: ${id}`, async () => {
      await initPage();
      // Find the toggle and ensure that it's:
      // 1. Not checked
      // 2. The associated pref is off
      const toggle =
          page.shadowRoot!.querySelector<SettingsToggleButtonElement>(`#${id}`);
      assert(toggle);
      assertFalse(toggle.checked);
      let pref = page.getPref(prefKey);
      assertFalse(pref.value);

      // Click the toggle. Ensure that it's:
      // 1. Checked
      // 2. The associated pref is on
      toggle.click();
      assertTrue(toggle.checked);
      pref = page.getPref(prefKey);
      assertTrue(pref.value);

      if (cvoxTooltipId === '') {
        return;
      }

      const disabledTooltipIcon =
          page.shadowRoot!.querySelector(`#${cvoxTooltipId}`);
      assert(disabledTooltipIcon);
      assertFalse(isVisible(disabledTooltipIcon));

      // Turn on ChromeVox.
      page.setPrefValue('settings.accessibility', true);
      assertTrue(toggle.disabled);
      assertTrue(isVisible(disabledTooltipIcon));
      assertFalse(toggle.checked);

      // Turn off ChromeVox again.
      page.setPrefValue('settings.accessibility', false);
      assertFalse(toggle.disabled);
      assertFalse(isVisible(disabledTooltipIcon));
      assertTrue(toggle.checked);
    });
  });
});