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

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

/**
 * @fileoverview Test suite for the ChromeVox subpage in ChromeOS Settings.
 */

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

import {SettingsChromeVoxSubpageElement} from 'chrome://os-settings/lazy_load.js';
import {ChromeVoxSubpageBrowserProxyImpl, CrSettingsPrefs, SettingsDropdownMenuElement, SettingsPrefsElement} from 'chrome://os-settings/os_settings.js';
import {assert} from 'chrome://resources/js/assert.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertDeepEquals, assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';

import {TestChromeVoxSubpageBrowserProxy} from './test_chromevox_subpage_browser_proxy.js';

/**
 * Control types for pref-based settings.
 */
const enum ControlType {
  DROPDOWN = 'dropdown',
  TOGGLE = 'toggle',
  INPUT = 'input',
}

suite('<settings-chromevox-subpage>', () => {
  let page: SettingsChromeVoxSubpageElement;
  let browserProxy: TestChromeVoxSubpageBrowserProxy;
  let prefElement: SettingsPrefsElement;

  setup(async () => {
    browserProxy = new TestChromeVoxSubpageBrowserProxy();
    ChromeVoxSubpageBrowserProxyImpl.setInstanceForTesting(browserProxy);

    prefElement = document.createElement('settings-prefs');
    document.body.appendChild(prefElement);

    await CrSettingsPrefs.initialized;
    page = document.createElement('settings-chromevox-subpage');
    page.prefs = prefElement.prefs;
    document.body.appendChild(page);
    flush();
  });

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

  const settingsControls = [
    {
      id: 'voiceDropdown',
      prefKey: 'settings.a11y.chromevox.voice_name',
      defaultValue: 'chromeos_system_voice',
      secondaryValue: 'Chrome OS US English',
      type: ControlType.DROPDOWN,
    },
    {
      id: 'languageSwitchingToggle',
      prefKey: 'settings.a11y.chromevox.language_switching',
      defaultValue: false,
      secondaryValue: true,
      type: ControlType.TOGGLE,
    },
    {
      id: 'enableSpeechLoggingToggle',
      prefKey: 'settings.a11y.chromevox.enable_speech_logging',
      defaultValue: false,
      secondaryValue: true,
      type: ControlType.TOGGLE,
    },
    {
      id: 'brailleTableTypeDropdown',
      prefKey: 'settings.a11y.chromevox.braille_table_type',
      defaultValue: 'brailleTable8',
      secondaryValue: 'brailleTable6',
      type: ControlType.DROPDOWN,
    },
    {
      id: 'brailleWordWrapToggle',
      prefKey: 'settings.a11y.chromevox.braille_word_wrap',
      defaultValue: true,
      secondaryValue: false,
      type: ControlType.TOGGLE,
    },
    {
      id: 'virtualBrailleDisplayRowsInput',
      prefKey: 'settings.a11y.chromevox.virtual_braille_rows',
      defaultValue: 1,
      secondaryValue: 2,
      correctedValues: [
        ['', 1],
        [0, 1],
        [1, 1],
        [2, 2],
        [98, 98],
        [99, 99],
        [100, 99],
        [-4, 99],
        [-999, 99],
        [20, 20],
        ['', 20],
      ],
      type: ControlType.INPUT,
    },
    {
      id: 'virtualBrailleDisplayColumnsInput',
      prefKey: 'settings.a11y.chromevox.virtual_braille_columns',
      defaultValue: 40,
      secondaryValue: 80,
      correctedValues: [
        ['', 40],
        [0, 40],
        [1, 1],
        [2, 2],
        [98, 98],
        [99, 99],
        [100, 99],
        [-4, 99],
        [-999, 99],
        [20, 20],
        ['', 20],
      ],
      type: ControlType.INPUT,
    },
    {
      id: 'menuBrailleCommandsToggle',
      prefKey: 'settings.a11y.chromevox.menu_braille_commands',
      defaultValue: false,
      secondaryValue: true,
      type: ControlType.TOGGLE,
    },
    {
      id: 'enableEarconLoggingToggle',
      prefKey: 'settings.a11y.chromevox.enable_earcon_logging',
      defaultValue: false,
      secondaryValue: true,
      type: ControlType.TOGGLE,
    },
    {
      id: 'enableBrailleLoggingToggle',
      prefKey: 'settings.a11y.chromevox.enable_braille_logging',
      defaultValue: false,
      secondaryValue: true,
      type: ControlType.TOGGLE,
    },
    {
      id: 'enableEventStreamLoggingToggle',
      prefKey: 'settings.a11y.chromevox.enable_event_stream_logging',
      defaultValue: false,
      secondaryValue: true,
      type: ControlType.TOGGLE,
    },
    {
      id: 'virtualBrailleDisplayStyleDropdown',
      prefKey: 'settings.a11y.chromevox.braille_side_by_side',
      defaultValue: true,
      secondaryValue: false,
      type: ControlType.DROPDOWN,
    },
  ];

  settingsControls.forEach(control => {
    const {id, prefKey, defaultValue, secondaryValue, type, correctedValues} =
        control;

    test(`ChromeVox ${type} ${id} syncs to Pref: ${prefKey}`, async () => {
      // Make sure control exists.
      const control = page.shadowRoot!.querySelector<HTMLElement>(`#${id}`);
      assert(control);

      // Make sure pref is set to the default value.
      let pref = page.getPref(prefKey);
      assertEquals(defaultValue, pref.value);

      switch (type) {
        case ControlType.TOGGLE:
          // Make sure toggle control is set to the default value.
          await waitAfterNextRender(control);
          const crToggleElement =
              control.shadowRoot!.querySelector('cr-toggle');
          assertEquals(defaultValue, crToggleElement!.checked);
          // Click toggle control to attempt updating to secondary value.
          control.click();
          break;

        case ControlType.DROPDOWN:
          // Make sure dropdown is set to the default value.
          await waitAfterNextRender(control);
          const selectElement = control.shadowRoot!.querySelector('select');
          assert(selectElement);
          assertEquals(String(defaultValue), selectElement.value);
          // Update dropdown to secondary value.
          selectElement.value = String(secondaryValue);
          selectElement.dispatchEvent(
              new CustomEvent('change', {bubbles: true, composed: true}));
          break;

        case ControlType.INPUT:
          // Make sure input is set to the default value.
          await waitAfterNextRender(control);
          const inputElement = control.shadowRoot!.querySelector('input');
          assert(inputElement);
          assertEquals(String(defaultValue), inputElement.value);
          // Make sure out-of-range values get updated to correct values.
          assert(correctedValues);
          for (const [startValue, correctedValue] of correctedValues) {
            inputElement.value = String(startValue);
            inputElement.dispatchEvent(
                new CustomEvent('input', {bubbles: true, composed: true}));
            inputElement.dispatchEvent(
                new CustomEvent('focusout', {bubbles: true, composed: true}));
            await waitAfterNextRender(control);
            assertEquals(String(correctedValue), inputElement.value);
          }
          // Update input to secondary value.
          inputElement.value = String(secondaryValue);
          inputElement.dispatchEvent(
              new CustomEvent('input', {bubbles: true, composed: true}));
          break;
      }

      // Make sure pref is set to secondary value.
      pref = page.getPref(prefKey);
      assertEquals(secondaryValue, pref.value);
    });
  });

  test('event stream filter toggles sync to prefs', async () => {
    // Enable event stream logging to allow enabling filter toggles.
    const loggingToggle = page.shadowRoot!.querySelector<HTMLElement>(
        '#enableEventStreamLoggingToggle');
    assert(loggingToggle);
    loggingToggle.click();
    await waitAfterNextRender(loggingToggle);

    // Get all event stream filter prefs.
    let pref = page.getPref('settings.a11y.chromevox.event_stream_filters');

    // Toggle each filter, verify each pref is set.
    page.get('eventStreamFilters_').forEach((filter: string) => {
      const toggle = page.shadowRoot!.querySelector<HTMLElement>('#' + filter);

      // Make sure toggle exists.
      assert(toggle);

      // Make sure pref filter state is false or undefined (key is not present).
      assertTrue([false, undefined].includes(pref.value[filter]));

      // Enable event stream filter toggle.
      toggle.click();

      // Make sure event stream filter pref state is true.
      pref = page.getPref('settings.a11y.chromevox.event_stream_filters');
      assertTrue(pref.value[filter]);
    });
  });

  test('voices are ordered', () => {
    // Make sure voices are ordered with the system default voice first, then
    // Google voices, then eSpeak, then local, then remote.
    const voiceDropdown =
        page.shadowRoot!.querySelector<SettingsDropdownMenuElement>(
            '#voiceDropdown');
    assert(voiceDropdown);
    const expectedMenuOptions = [
      {name: 'System Text-to-Speech voice', value: 'chromeos_system_voice'},
      {name: 'Chrome OS US English', value: 'Chrome OS US English'},
      {name: 'Chrome OS हिन्दी', value: 'Chrome OS हिन्दी'},
      {name: 'eSpeak Turkish', value: 'eSpeak Turkish'},
      {name: 'default-coolnet', value: 'default-coolnet'},
      {name: 'bnm', value: 'bnm'},
      {name: 'bnx', value: 'bnx'},
    ];
    assertDeepEquals(expectedMenuOptions, voiceDropdown.menuOptions);
  });

  test('connect button works', async function() {
    // Mock chrome.bluetooth.getDevice using `display` as the backing source.
    const displays = [{name: 'VarioUltra', address: 'abcd1234', paired: true}];
    chrome.bluetooth.getDevice = async address =>
        displays.find(display => display.address === address)!;

    // Get Bluetooth Braille Display UI element.
    const bluetoothBrailleDisplayUi =
        page.shadowRoot!.querySelector('bluetooth-braille-display-ui');
    assert(bluetoothBrailleDisplayUi);

    // Update list of devices.
    await bluetoothBrailleDisplayUi.onDisplayListChanged(displays);

    // Get Bluetooth Braille Display dropdown element.
    const dropdownMenu =
        bluetoothBrailleDisplayUi.shadowRoot!
            .querySelector<SettingsDropdownMenuElement>('#displaySelect');
    assert(dropdownMenu);

    // Get Bluetooth Braille Display select element.
    const selectElement = dropdownMenu.shadowRoot!.querySelector('select');
    assert(selectElement);

    // Select VarioUltra simulated device.
    selectElement.value = 'abcd1234';
    selectElement.dispatchEvent(
        new CustomEvent('change', {bubbles: true, composed: true}));
    await waitAfterNextRender(selectElement);

    // Update list of devices.
    await bluetoothBrailleDisplayUi.onDisplayListChanged(displays);
  });

  test('no custom dropdown item shown', async function() {
    // Get Bluetooth Braille Display UI element.
    const bluetoothBrailleDisplayUi =
        page.shadowRoot!.querySelector('bluetooth-braille-display-ui');
    assert(bluetoothBrailleDisplayUi);

    // Get Bluetooth Braille Display dropdown element.
    const dropdownMenu =
        bluetoothBrailleDisplayUi.shadowRoot!
            .querySelector<SettingsDropdownMenuElement>('#displaySelect');
    assert(dropdownMenu);

    // Get Bluetooth Braille Display select element.
    const selectElement = dropdownMenu.shadowRoot!.querySelector('select');
    assert(selectElement);

    // Verify Bluetooth Braille Display select element is blank (not custom).
    assertEquals('', selectElement.value);
  });

  test('braille display shown', async function() {
    // Mock chrome.bluetooth.getDevice using `display` as the backing source.
    const displays = [{name: 'VarioUltra', address: 'abcd1234', paired: true}];
    chrome.bluetooth.getDevice = async address =>
        displays.find(display => display.address === address)!;

    // Get Bluetooth Braille Display UI element.
    const bluetoothBrailleDisplayUi =
        page.shadowRoot!.querySelector('bluetooth-braille-display-ui');
    assert(bluetoothBrailleDisplayUi);

    // Update list of devices.
    await bluetoothBrailleDisplayUi.onDisplayListChanged(displays);

    // Get Bluetooth Braille Display dropdown element.
    const dropdownMenu =
        bluetoothBrailleDisplayUi.shadowRoot!
            .querySelector<SettingsDropdownMenuElement>('#displaySelect');
    assert(dropdownMenu);

    // Get Bluetooth Braille Display select element.
    const selectElement = dropdownMenu.shadowRoot!.querySelector('select');
    assert(selectElement);

    // Verify VarioUltra Bluetooth Braille Display is selected.
    assertEquals('abcd1234', selectElement.value);
  });
});