chromium/chrome/test/data/webui/chromeos/settings/os_languages_page/input_page_test.ts

// Copyright 2020 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 {CrCheckboxWithPolicyElement, InputsShortcutReminderState, LanguageHelper, LanguagesBrowserProxyImpl, LanguagesMetricsProxyImpl, LanguagesPageInteraction, OsSettingsAddItemsDialogElement, OsSettingsInputPageElement, SettingsLanguagesElement} from 'chrome://os-settings/lazy_load.js';
import {AcceleratorAction, CrCheckboxElement, CrSettingsPrefs, IronListElement, Router, routes, settingMojom, SettingsPrefsElement, SettingsToggleButtonElement} from 'chrome://os-settings/os_settings.js';
import {StandardAcceleratorProperties} from 'chrome://resources/ash/common/shortcut_input_ui/accelerator_info.mojom-webui.js';
import {VKey} from 'chrome://resources/ash/common/shortcut_input_ui/accelerator_keys.mojom-webui.js';
import {FakeAcceleratorFetcher} from 'chrome://resources/ash/common/shortcut_input_ui/fake_accelerator_fetcher.js';
import {Modifier} from 'chrome://resources/ash/common/shortcut_input_ui/shortcut_utils.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {stringToMojoString16} from 'chrome://resources/js/mojo_type_util.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.js';
import {AcceleratorKeyState} from 'chrome://resources/mojo/ui/base/accelerators/mojom/accelerator.mojom-webui.js';
import {keyDownOn} 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 {assertDeepEquals, assertEquals, assertFalse, assertGE, assertGT, assertNotEquals, assertNull, assertStringContains, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {FakeSettingsPrivate} from 'chrome://webui-test/fake_settings_private.js';
import {fakeDataBind, flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

import {FakeLanguageSettingsPrivate, getFakeLanguagePrefs} from '../fake_language_settings_private.js';
import {clearBody} from '../utils.js';

import {TestLanguagesBrowserProxy} from './test_os_languages_browser_proxy.js';
import {TestLanguagesMetricsProxy} from './test_os_languages_metrics_proxy.js';

suite('<os-settings-input-page>', () => {
  let inputPage: OsSettingsInputPageElement;
  let metricsProxy: TestLanguagesMetricsProxy;
  let browserProxy: TestLanguagesBrowserProxy;
  let languageHelper: LanguageHelper;
  let settingsLanguages: SettingsLanguagesElement;

  async function createInputPage(): Promise<void> {
    const prefElement: SettingsPrefsElement =
        document.createElement('settings-prefs');
    const settingsPrivate = new FakeSettingsPrivate(getFakeLanguagePrefs());

    /**
     * Prefs listener to emulate SpellcheckService listeners.
     * As we use a mocked prefs object in tests, we also need to mock the
     * behavior of SpellcheckService as it relies on a C++ PrefChangeRegistrar
     * to listen to pref changes - which do not work when the prefs are mocked.
     */
    async function spellCheckServiceListener(
        prefs: chrome.settingsPrivate.PrefObject[]): Promise<void> {
      for (const pref of prefs) {
        switch (pref.key) {
          case 'spellcheck.dictionaries':
            // Emulate SpellcheckService::OnSpellCheckDictionariesChanged:
            // If there are no dictionaries, set browser.enable_spellchecking
            // to false.
            if (pref.value.length === 0) {
              settingsPrivate.setPref(
                  'browser.enable_spellchecking', false, '');
            }
            break;
        }
      }
    }

    // Listen to prefs changes using settingsPrivate.onPrefsChanged.
    // While prefElement (<settings-prefs>) is normally a synchronous wrapper
    // around the asynchronous settingsPrivate, the two's prefs are always
    // synchronously kept in sync both ways in tests.
    // However, it's possible that a settingsPrivate.onPrefsChanged listener
    // receives a change before prefElement does if the change is made by
    // settingsPrivate, so prefer to use settingsPrivate getters/setters
    // whenever possible.
    settingsPrivate.onPrefsChanged.addListener(spellCheckServiceListener);
    prefElement.initialize(settingsPrivate);
    document.body.appendChild(prefElement);

    await CrSettingsPrefs.initialized;

    // Set up fake languageSettingsPrivate API.
    const languageSettingsPrivate = browserProxy.getLanguageSettingsPrivate() as
        FakeLanguageSettingsPrivate;
    languageSettingsPrivate.setSettingsPrefsForTesting(prefElement);

    // Instantiate the data model with data bindings for prefs.
    settingsLanguages = document.createElement('settings-languages');
    settingsLanguages.prefs = prefElement.prefs;
    fakeDataBind(prefElement, settingsLanguages, 'prefs');
    document.body.appendChild(settingsLanguages);

    // Create page with data bindings for prefs and data model.
    inputPage = document.createElement('os-settings-input-page');
    inputPage.prefs = prefElement.prefs;
    fakeDataBind(prefElement, inputPage, 'prefs');
    inputPage.languages = settingsLanguages.languages;
    fakeDataBind(settingsLanguages, inputPage, 'languages');
    inputPage.languageHelper = settingsLanguages.languageHelper;
    fakeDataBind(settingsLanguages, inputPage, 'language-helper');
    languageHelper = inputPage.languageHelper;
    document.body.appendChild(inputPage);
    await flushTasks();
  }

  suiteSetup(() => {
    // Set up test browser proxy.
    browserProxy = new TestLanguagesBrowserProxy();
    LanguagesBrowserProxyImpl.setInstanceForTesting(browserProxy);

    // Sets up test metrics proxy.
    metricsProxy = new TestLanguagesMetricsProxy();
    LanguagesMetricsProxyImpl.setInstanceForTesting(metricsProxy);

    CrSettingsPrefs.deferInitialization = true;
  });

  setup(() => {
    clearBody();
    loadTimeData.overrideValues({
      allowEmojiSuggestion: true,
    });
    Router.getInstance().navigateTo(routes.OS_LANGUAGES_INPUT);
  });

  teardown(() => {
    inputPage.remove();
    settingsLanguages.remove();
    Router.getInstance().resetRouteForTesting();
    browserProxy.reset();
    metricsProxy.reset();
  });

  suite('language pack notice', () => {
    test('is shown', async () => {
      await createInputPage();

      assertTrue(isVisible(
          inputPage.shadowRoot!.querySelector('#languagePacksNotice')));
    });
  });

  suite('input method list', () => {
    setup(async () => {
      await createInputPage();
    });

    test('displays correctly', () => {
      const inputMethodsList =
          inputPage.shadowRoot!.querySelector('#inputMethodsList');
      assertTrue(!!inputMethodsList);

      // The test input methods should appear.
      const items = inputMethodsList.querySelectorAll('.list-item');
      // Two items for input methods and one item for add input methods.
      assertEquals(3, items.length);
      let name = items[0]!.querySelector('.display-name');
      assertTrue(!!name);
      assertEquals('US keyboard', name.textContent?.trim());
      assertTrue(!!items[0]!.querySelector('.internal-wrapper'));
      assertNull(items[0]!.querySelector('.external-wrapper'));
      let icon = items[0]!.querySelector<HTMLButtonElement>('.icon-clear');
      assertTrue(!!icon);
      assertFalse(icon.disabled);
      name = items[1]!.querySelector('.display-name');
      assertTrue(!!name);
      assertEquals('US Dvorak keyboard', name.textContent?.trim());
      assertTrue(!!items[1]!.querySelector('.external-wrapper'));
      assertNull(items[1]!.querySelector('.internal-wrapper'));
      icon = items[1]!.querySelector<HTMLButtonElement>('.icon-clear');
      assertTrue(!!icon);
      assertFalse(icon.disabled);
    });

    test('navigates to input method options page', () => {
      const inputMethodsList =
          inputPage.shadowRoot!.querySelector('#inputMethodsList');
      assertTrue(!!inputMethodsList);
      const items = inputMethodsList.querySelectorAll('.list-item');
      const button =
          items[0]!.querySelector<HTMLButtonElement>('.subpage-arrow');
      assertTrue(!!button);
      button.click();
      const router = Router.getInstance();
      assertEquals(
          'chrome://os-settings/osLanguages/inputMethodOptions',
          router.currentRoute.getAbsolutePath());
      assertEquals(
          '_comp_ime_jkghodnilhceideoidjikpgommlajknkxkb:us::eng',
          router.getQueryParameters().get('id'));
    });

    test('removes an input method', () => {
      const inputMethodName = 'US keyboard';

      let inputMethodsList =
          inputPage.shadowRoot!.querySelector('#inputMethodsList');
      assertTrue(!!inputMethodsList);
      let items = inputMethodsList.querySelectorAll('.list-item');
      assertEquals(3, items.length);
      let name = items[0]!.querySelector('.display-name');
      assertTrue(!!name);
      assertEquals(inputMethodName, name.textContent?.trim());

      // clicks remove input method button.
      const icon = items[0]!.querySelector<HTMLButtonElement>('.icon-clear');
      assertTrue(!!icon);
      icon.click();
      flush();

      inputMethodsList =
          inputPage.shadowRoot!.querySelector('#inputMethodsList');
      assertTrue(!!inputMethodsList);
      items = inputMethodsList.querySelectorAll('.list-item');
      assertEquals(2, items.length);
      name = items[0]!.querySelector('.display-name');
      assertTrue(!!name);
      assertNotEquals(inputMethodName, name.textContent?.trim());
    });

    test('disables remove input method option', () => {
      // Add US Swahili keyboard, a third party IME
      languageHelper.addInputMethod(
          'ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:us:sw');
      // Remove US Dvorak keyboard, so there is only 1 component IME left.
      languageHelper.removeInputMethod(
          '_comp_ime_fgoepimhcoialccpbmpnnblemnepkkaoxkb:us:dvorak:eng');
      flush();

      const inputMethodsList =
          inputPage.shadowRoot!.querySelector('#inputMethodsList');
      assertTrue(!!inputMethodsList);
      const items = inputMethodsList.querySelectorAll('.list-item');
      assertEquals(3, items.length);
      let name = items[0]!.querySelector('.display-name');
      assertTrue(!!name);
      assertEquals('US keyboard', name.textContent?.trim());
      let icon = items[0]!.querySelector<HTMLButtonElement>('.icon-clear');
      assertTrue(!!icon);
      assertTrue(icon.disabled);
      name = items[1]!.querySelector('.display-name');
      assertTrue(!!name);
      assertEquals('US Swahili keyboard', name.textContent?.trim());
      icon = items[1]!.querySelector<HTMLButtonElement>('.icon-clear');
      assertTrue(!!icon);
      assertFalse(icon.disabled);
    });

    test('shows managed input methods label', () => {
      const inputMethodsManagedbyPolicy =
          inputPage.shadowRoot!.querySelector('#inputMethodsManagedbyPolicy');
      assertNull(inputMethodsManagedbyPolicy);

      inputPage.setPrefValue(
          'settings.language.allowed_input_methods', ['xkb:us::eng']);
      flush();

      assertTrue(!!inputPage.shadowRoot!.querySelector(
          '#inputMethodsManagedbyPolicy'));
    });
  });

  suite('input page', () => {
    test('Deep link to spell check', async () => {
      await createInputPage();

      const setting = settingMojom.Setting.kSpellCheckOnOff;
      const params = new URLSearchParams();
      params.append('settingId', setting.toString());
      Router.getInstance().navigateTo(routes.OS_LANGUAGES_INPUT, params);
      flush();

      const enableSpellcheckingToggle =
          inputPage.shadowRoot!.querySelector('#enableSpellcheckingToggle');
      assertTrue(!!enableSpellcheckingToggle);
      const deepLinkElement =
          enableSpellcheckingToggle.shadowRoot!.querySelector('cr-toggle');
      assertTrue(!!deepLinkElement);
      await waitAfterNextRender(deepLinkElement);
      assertEquals(
          deepLinkElement, getDeepActiveElement(),
          `Spell check toggle should be focused for settingId=${setting}.`);
    });

    test('Spellcheck row is focused after returning from subpage', async () => {
      await createInputPage();

      const triggerSelector = '#editDictionarySubpageTrigger';
      const subpageTrigger =
          inputPage.shadowRoot!.querySelector<HTMLElement>(triggerSelector);
      assertTrue(!!subpageTrigger);

      // Sub-page trigger navigates to spellcheck subpage
      subpageTrigger.click();
      assertEquals(
          routes.OS_LANGUAGES_EDIT_DICTIONARY,
          Router.getInstance().currentRoute);

      // Navigate back
      const popStateEventPromise = eventToPromise('popstate', window);
      Router.getInstance().navigateToPreviousRoute();
      await popStateEventPromise;
      await waitAfterNextRender(inputPage);

      assertEquals(
          subpageTrigger, inputPage.shadowRoot!.activeElement,
          `${triggerSelector} should be focused.`);
    });
  });

  suite('add input methods dialog', () => {
    let dialog: OsSettingsAddItemsDialogElement;
    let suggestedList: IronListElement;
    let allImesList: IronListElement;
    let cancelButton: HTMLButtonElement;
    let actionButton: HTMLButtonElement;

    setup(async () => {
      await createInputPage();

      let element = inputPage.shadowRoot!.querySelector(
          'os-settings-add-input-methods-dialog');
      assertNull(element);
      const addInputMethod =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#addInputMethod');
      assertTrue(!!addInputMethod);
      addInputMethod.click();
      flush();

      element = inputPage.shadowRoot!.querySelector(
          'os-settings-add-input-methods-dialog');
      assertTrue(!!element);
      const dialogElement =
          element.shadowRoot!.querySelector('os-settings-add-items-dialog');
      assertTrue(!!dialogElement);
      dialog = dialogElement;

      const button =
          dialog.shadowRoot!.querySelector<HTMLButtonElement>('.action-button');
      assertTrue(!!button);
      actionButton = button;
      const cancel =
          dialog.shadowRoot!.querySelector<HTMLButtonElement>('.cancel-button');
      assertTrue(!!cancel);
      cancelButton = cancel;

      const list = dialog.shadowRoot!.querySelector<IronListElement>(
          '#suggested-items-list');
      assertTrue(!!list);
      suggestedList = list;

      const allList = dialog.shadowRoot!.querySelector<IronListElement>(
          '#filtered-items-list');
      assertTrue(!!allList);
      allImesList = allList;

      // No input methods has been selected, so the action button is disabled.
      assertTrue(actionButton.disabled);
      assertFalse(cancelButton.disabled);
    });

    test('has action button working correctly', () => {
      const listItems =
          suggestedList.querySelectorAll<HTMLButtonElement>('.list-item');
      assertTrue(!!listItems);
      // selecting a language enables action button
      listItems[0]!.click();
      assertFalse(actionButton.disabled);

      // selecting the same language again disables action button
      listItems[0]!.click();
      assertTrue(actionButton.disabled);
    });

    test('has correct structure and adds input methods', () => {
      const suggestedItems =
          suggestedList.querySelectorAll<HTMLElement>('.list-item');
      assertTrue(!!suggestedItems);
      // input methods are based on and ordered by enabled languages
      // only allowed input methods are shown.
      assertEquals(2, suggestedItems.length);
      assertEquals(
          'US Swahili keyboard', suggestedItems[0]!.textContent?.trim());
      assertEquals('Swahili keyboard', suggestedItems[1]!.textContent?.trim());
      // selecting Swahili keyboard.
      suggestedItems[1]!.click();

      const allItems = allImesList.querySelectorAll('.list-item');
      // All input methods should appear and ordered based on fake settings
      // data.
      assertEquals(4, allItems.length);

      const expectedItems = [
        {
          name: 'Swahili keyboard',
          checkboxDisabled: false,
          checkboxChecked: true,
          policyIcon: false,
        },
        {
          name: 'US Swahili keyboard',
          checkboxDisabled: false,
          checkboxChecked: false,
          policyIcon: false,
        },
        {
          name: 'US International keyboard',
          checkboxDisabled: true,
          checkboxChecked: false,
          policyIcon: true,
        },
        {
          name: 'Vietnamese keyboard',
          checkboxDisabled: false,
          checkboxChecked: false,
          policyIcon: false,
        },
      ];

      for (let i = 0; i < allItems.length; i++) {
        const checkbox = allItems[i]!.shadowRoot!.querySelector('cr-checkbox');
        assertTrue(!!checkbox);
        assertStringContains(allItems[i]!.textContent!, expectedItems[i]!.name);
        assertEquals(
            expectedItems[i]!.checkboxDisabled, checkbox.disabled,
            `expect ${expectedItems[i]!.name}'s checkbox disabled state to be ${
                expectedItems[i]!.checkboxDisabled}`);
        assertEquals(
            expectedItems[i]!.checkboxChecked, checkbox.checked,
            `expect ${expectedItems[i]!.name}'s checkbox checked state to be ${
                expectedItems[i]!.checkboxChecked}`);
        assertEquals(
            expectedItems[i]!.policyIcon,
            !!allItems[i]!.shadowRoot!.querySelector('iron-icon'),
            `expect ${expectedItems[i]!.name}'s policy icon presence to be ${
                expectedItems[i]!.policyIcon}`);
      }

      // selecting Vietnamese keyboard
      const checkbox = allItems[3]!.shadowRoot!.querySelector('cr-checkbox');
      assertTrue(!!checkbox);
      checkbox.click();

      actionButton.click();

      assertTrue(languageHelper.isInputMethodEnabled(
          '_comp_ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:sw:sw'));
      assertFalse(languageHelper.isInputMethodEnabled(
          'ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:us:sw'));
      assertTrue(languageHelper.isInputMethodEnabled(
          '_comp_ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:vi:vi'));
    });

    test('suggested input methods hidden when no languages is enabled', () => {
      inputPage.setPrefValue('intl.accept_languages', '');
      inputPage.setPrefValue('settings.language.preferred_languages', '');
      flush();

      const suggestedMethods =
          dialog.shadowRoot!.querySelector('#suggestedInputMethods');
      // suggested input methods is rendered previously.
      assertFalse(isVisible(suggestedMethods));
    });

    test('suggested input methods hidden when no input methods left', () => {
      const languageCode = 'sw';
      inputPage.setPrefValue('intl.accept_languages', languageCode);
      inputPage.setPrefValue(
          'settings.language.preferred_languages', languageCode);
      languageHelper.getInputMethodsForLanguage(languageCode)
          .forEach(
              (inputMethod: chrome.languageSettingsPrivate.InputMethod) => {
                languageHelper.addInputMethod(inputMethod.id);
              });
      flush();

      const suggestedMethods =
          dialog.shadowRoot!.querySelector('#suggestedInputMethods');
      assertFalse(isVisible(suggestedMethods));
    });

    test('searches input methods correctly', () => {
      const searchInput = dialog.shadowRoot!.querySelector('cr-search-field');
      assertTrue(!!searchInput);
      const getItems = () => {
        return allImesList.querySelectorAll('.list-item:not([hidden])');
      };

      assertTrue(
          isVisible(dialog.shadowRoot!.querySelector('#filtered-items-label')));
      assertTrue(isVisible(suggestedList));

      // Expecting a few languages to be displayed when no query exists.
      assertGE(getItems().length, 1);

      // Search hides the suggested list and the label for all IMEs.
      searchInput.setValue('v');
      flush();
      assertFalse(
          isVisible(dialog.shadowRoot!.querySelector('#filtered-items-label')));
      assertFalse(isVisible(suggestedList));

      // Search input methods name
      searchInput.setValue('vietnamese');
      flush();
      assertEquals(1, getItems().length);
      assertStringContains(getItems()[0]!.textContent!, 'Vietnamese');

      // Search input methods' language
      searchInput.setValue('Turkmen');
      flush();
      assertEquals(1, getItems().length);
      assertStringContains(getItems()[0]!.textContent!, 'Swahili keyboard');
    });

    test('has escape key behavior working correctly', () => {
      const searchInput = dialog.shadowRoot!.querySelector('cr-search-field');
      assertTrue(!!searchInput);
      searchInput.setValue('dummyquery');

      // Test that dialog is not closed if 'Escape' is pressed on the input
      // and a search query exists.
      keyDownOn(searchInput, 19, [], 'Escape');
      assertTrue(dialog.$.dialog.open);

      // Test that dialog is closed if 'Escape' is pressed on the input and no
      // search query exists.
      searchInput.setValue('');
      keyDownOn(searchInput, 19, [], 'Escape');
      assertFalse(dialog.$.dialog.open);
    });
  });

  suite('records metrics', () => {
    setup(async () => {
      await createInputPage();
    });

    test('when deactivating show ime menu', async () => {
      inputPage.setPrefValue('settings.language.ime_menu_activated', true);
      const showImeMenu =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#showImeMenu');
      assertTrue(!!showImeMenu);
      showImeMenu.click();
      flush();

      assertFalse(
          await metricsProxy.whenCalled('recordToggleShowInputOptionsOnShelf'));
    });

    test('when activating show ime menu', async () => {
      inputPage.setPrefValue('settings.language.ime_menu_activated', false);
      const showImeMenu =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#showImeMenu');
      assertTrue(!!showImeMenu);
      showImeMenu.click();
      flush();

      assertTrue(
          await metricsProxy.whenCalled('recordToggleShowInputOptionsOnShelf'));
    });

    test('when adding input methods', async () => {
      const addInputMethod =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#addInputMethod');
      assertTrue(!!addInputMethod);
      addInputMethod.click();
      flush();

      await metricsProxy.whenCalled('recordAddInputMethod');
    });

    test('when switch input method', async () => {
      const inputMethodsList =
          inputPage.shadowRoot!.querySelector('#inputMethodsList');
      assertTrue(!!inputMethodsList);

      // The test input methods should appear.
      const items =
          inputMethodsList.querySelectorAll<HTMLButtonElement>('.list-item');
      items[0]!.click();
      assertEquals(
          LanguagesPageInteraction.SWITCH_INPUT_METHOD,
          await metricsProxy.whenCalled('recordInteraction'));
    });


    test('dismissing shortcut reminder with accelerator provider', async () => {
      const expectedLastUsedImeAccelerator: StandardAcceleratorProperties = {
        keyDisplay: stringToMojoString16('m'),
        accelerator: {
          modifiers: Modifier.CONTROL,
          keyCode: VKey.kKeyM,
          keyState: AcceleratorKeyState.PRESSED,
          timeStamp: {
            internalValue: 0n,
          },
        },
        originalAccelerator: null,
      };

      const acceleratorProvider = new FakeAcceleratorFetcher();
      inputPage.acceleratorFetcher = acceleratorProvider;
      await flushTasks();

      acceleratorProvider.observeAcceleratorChanges(
          [
            AcceleratorAction.kSwitchToLastUsedIme,
            AcceleratorAction.kSwitchToNextIme,
          ],
          inputPage);
      assertTrue(!!inputPage.acceleratorFetcher);

      // Set an updated lastUsedImeAccelerator, the shortcut reminder should
      // show "ctrl + m" as last used IME.
      acceleratorProvider.mockAcceleratorsUpdated(
          AcceleratorAction.kSwitchToLastUsedIme,
          [expectedLastUsedImeAccelerator]);
      await flushTasks();

      assertTrue(!!inputPage.get('lastUsedImeAccelerator_'));
      assertEquals(
          inputPage.get('lastUsedImeAccelerator_').keyDisplay,
          expectedLastUsedImeAccelerator.keyDisplay);

      const updatedLastUsedImeAccelerator: StandardAcceleratorProperties = {
        keyDisplay: stringToMojoString16('k'),
        accelerator: {
          modifiers: Modifier.CONTROL + Modifier.SHIFT,
          keyCode: VKey.kKeyK,
          keyState: AcceleratorKeyState.PRESSED,
          timeStamp: {
            internalValue: 0n,
          },
        },
        originalAccelerator: null,
      };

      // Update the last used IME with a new accelerator, the shortcut reminder
      // should show "ctrl + shift + k" as last used IME.
      acceleratorProvider.mockAcceleratorsUpdated(
          AcceleratorAction.kSwitchToLastUsedIme,
          [updatedLastUsedImeAccelerator]);
      await flushTasks();
      assertTrue(!!inputPage.get('lastUsedImeAccelerator_'));
      assertEquals(
          (inputPage.get('lastUsedImeAccelerator_'))!.keyDisplay,
          updatedLastUsedImeAccelerator!.keyDisplay);

      let element =
          inputPage.shadowRoot!.querySelector('keyboard-shortcut-banner');
      assertTrue(!!element);

      let dismissButton =
          element.shadowRoot!.querySelector<HTMLButtonElement>('#dismiss');
      assertTrue(!!dismissButton);
      dismissButton.click();
      assertEquals(
          InputsShortcutReminderState.LAST_USED_IME,
          await metricsProxy.whenCalled('recordShortcutReminderDismissed'));
      metricsProxy.resetResolver('recordShortcutReminderDismissed');

      // Add US Swahili keyboard, a third party IME.
      languageHelper.addInputMethod(
          'ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:us:sw');
      flush();

      // Shortcut reminder should show "next IME" shortcut.
      element = inputPage.shadowRoot!.querySelector('keyboard-shortcut-banner');
      assertTrue(!!element);
      dismissButton =
          element.shadowRoot!.querySelector<HTMLButtonElement>('#dismiss');
      assertTrue(!!dismissButton);
      dismissButton.click();
      assertEquals(
          InputsShortcutReminderState.NEXT_IME,
          await metricsProxy.whenCalled('recordShortcutReminderDismissed'));
      metricsProxy.resetResolver('recordShortcutReminderDismissed');

      // Reset shortcut reminder dismissals to display both shortcuts.
      inputPage.setPrefValue(
          'ash.shortcut_reminders.last_used_ime_dismissed', false);
      inputPage.setPrefValue(
          'ash.shortcut_reminders.next_ime_dismissed', false);
      flush();

      // Shortcut reminder should show both shortcuts.
      element = inputPage.shadowRoot!.querySelector('keyboard-shortcut-banner');
      assertTrue(!!element);
      dismissButton =
          element.shadowRoot!.querySelector<HTMLButtonElement>('#dismiss');
      assertTrue(!!dismissButton);
      dismissButton.click();
      assertEquals(
          InputsShortcutReminderState.LAST_USED_IME_AND_NEXT_IME,
          await metricsProxy.whenCalled('recordShortcutReminderDismissed'));
    });

    test(
        'when dismissing shortcut reminder without shortcut provider',
        async () => {
          // Default shortcut reminder with two elements should show "last used
          // IME" reminder.
          let element =
              inputPage.shadowRoot!.querySelector('keyboard-shortcut-banner');
          assertTrue(!!element);
          let dismissButton =
              element.shadowRoot!.querySelector<HTMLButtonElement>('#dismiss');
          assertTrue(!!dismissButton);
          dismissButton.click();
          assertEquals(
              InputsShortcutReminderState.LAST_USED_IME,
              await metricsProxy.whenCalled('recordShortcutReminderDismissed'));
          metricsProxy.resetResolver('recordShortcutReminderDismissed');

          // Add US Swahili keyboard, a third party IME.
          languageHelper.addInputMethod(
              'ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:us:sw');
          flush();

          // Shortcut reminder should show "next IME" shortcut.
          element =
              inputPage.shadowRoot!.querySelector('keyboard-shortcut-banner');
          assertTrue(!!element);
          dismissButton =
              element.shadowRoot!.querySelector<HTMLButtonElement>('#dismiss');
          assertTrue(!!dismissButton);
          dismissButton.click();
          assertEquals(
              InputsShortcutReminderState.NEXT_IME,
              await metricsProxy.whenCalled('recordShortcutReminderDismissed'));
          metricsProxy.resetResolver('recordShortcutReminderDismissed');

          // Reset shortcut reminder dismissals to display both shortcuts.
          inputPage.setPrefValue(
              'ash.shortcut_reminders.last_used_ime_dismissed', false);
          inputPage.setPrefValue(
              'ash.shortcut_reminders.next_ime_dismissed', false);
          flush();

          // Shortcut reminder should show both shortcuts.
          element =
              inputPage.shadowRoot!.querySelector('keyboard-shortcut-banner');
          assertTrue(!!element);
          dismissButton =
              element.shadowRoot!.querySelector<HTMLButtonElement>('#dismiss');
          assertTrue(!!dismissButton);
          dismissButton.click();
          assertEquals(
              InputsShortcutReminderState.LAST_USED_IME_AND_NEXT_IME,
              await metricsProxy.whenCalled('recordShortcutReminderDismissed'));
        });

    test('when clicking on "learn more" about language packs', async () => {
      const languagePacksNotice =
          inputPage.shadowRoot!.querySelector('#languagePacksNotice');
      assertTrue(!!languagePacksNotice);
      const anchor = languagePacksNotice.shadowRoot!.querySelector('a');
      assertTrue(!!anchor);
      // The below would normally create a new window, which would change the
      // focus from this test to the new window.
      // Prevent this from happening by adding an event listener on the anchor
      // element which stops the default behaviour (of opening a new window).
      anchor.addEventListener('click', (e: Event) => e.preventDefault());
      anchor.click();
      assertEquals(
          LanguagesPageInteraction.OPEN_LANGUAGE_PACKS_LEARN_MORE,
          await metricsProxy.whenCalled('recordInteraction'));
    });
  });

  suite('spell check v2', () => {
    let spellCheckToggle: SettingsToggleButtonElement;
    let spellCheckListContainer: HTMLElement;
    // This list is not dynamically updated.
    let spellCheckList: NodeListOf<HTMLElement>;

    setup(async () => {
      // Enable grammar check.
      loadTimeData.overrideValues({
        onDeviceGrammarCheckEnabled: true,
      });
      await createInputPage();

      // Spell check is initially on.
      // Work around b/289955380 by only finding the only button which is not
      // hidden. <dom-if>s use a `display: none;` inline style to hide elements.
      // Because we do not use inline styles, the button which is not hidden
      // does not have a `style` attribute, and the one which is hidden does.
      const toggle =
          inputPage.shadowRoot!.querySelector<SettingsToggleButtonElement>(
              '#enableSpellcheckingToggle:not([style])');
      assertTrue(!!toggle);
      spellCheckToggle = toggle;
      assertTrue(spellCheckToggle.checked);

      const list = inputPage.shadowRoot!.querySelector<HTMLElement>(
          '#spellCheckLanguagesListV2');
      assertTrue(!!list);
      spellCheckListContainer = list;

      // The spell check list should only have en-US (excluding the "add
      // languages" button).
      spellCheckList = spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(1 + 1, spellCheckList.length);
      assertStringContains(
          spellCheckList[0]!.textContent!, 'English (United States)');
      assertStringContains(spellCheckList[1]!.textContent!, 'Add languages');
    });

    test('can remove enabled language from spell check list', () => {
      assertDeepEquals(
          ['en-US'], inputPage.prefs.spellcheck.dictionaries.value);
      // Get remove button for en-US.
      const spellCheckLanguageToggle =
          spellCheckList[0]!.querySelector<HTMLButtonElement>('cr-icon-button');
      assertTrue(!!spellCheckLanguageToggle);

      // Remove the language.
      spellCheckLanguageToggle.click();
      flush();

      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      // The spell check list should just have "add languages".
      assertEquals(0 + 1, newSpellCheckList.length);

      assertDeepEquals([], inputPage.prefs.spellcheck.dictionaries.value);
    });

    test('can remove non-enabled language from spell check list', () => {
      // Add a new non-enabled language to spellcheck.dictionaries.
      inputPage.setPrefValue('spellcheck.dictionaries', ['en-US', 'nb']);
      flush();

      let newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');

      // The spell check list should have en-US, nb and "add languages".
      assertEquals(2 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');
      assertStringContains(
          newSpellCheckList[1]!.textContent!, 'Norwegian Bokmål');

      // Remove nb.
      const icon = newSpellCheckList[1]!.querySelector('cr-icon-button');
      assertTrue(!!icon);
      icon.click();
      flush();
      newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');

      // The spell check list should have en-US and "add languages".
      assertEquals(1 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');

      assertDeepEquals(
          ['en-US'], inputPage.prefs.spellcheck.dictionaries.value);
    });

    test('shows force-on spell check language turned on by user', () => {
      // Force-enable a spell check language originally set by the user.
      inputPage.setPrefValue('spellcheck.forced_dictionaries', ['en-US']);
      flush();

      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');

      // The spell check list should have en-US and "add languages".
      assertEquals(1 + 1, newSpellCheckList.length);

      const forceEnabledEnUSLanguageRow = newSpellCheckList[0];
      assertTrue(!!forceEnabledEnUSLanguageRow);
      assertStringContains(
          forceEnabledEnUSLanguageRow.textContent!, 'English (United States)');
      assertTrue(!!forceEnabledEnUSLanguageRow.querySelector(
          'cr-policy-pref-indicator'));
      // Polymer sometimes hides the old enabled element by using a
      // display: none, so we use the managed-button class to get a reference to
      // the new disabled button.
      const managedButton =
          forceEnabledEnUSLanguageRow.querySelector<HTMLButtonElement>(
              '.managed-button');
      assertTrue(!!managedButton);
      assertTrue(managedButton.disabled);
    });

    test('shows force-on enabled spell check language', () => {
      // Force-enable an enabled language via policy.
      inputPage.setPrefValue('spellcheck.forced_dictionaries', ['sw']);
      flush();

      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');

      // The spell check list should have en-US, sw and "add languages".
      assertEquals(2 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');

      const forceEnabledSwLanguageRow = newSpellCheckList[1];
      assertTrue(!!forceEnabledSwLanguageRow);
      assertStringContains(forceEnabledSwLanguageRow.textContent!, 'Swahili');
      assertTrue(!!forceEnabledSwLanguageRow.querySelector(
          'cr-policy-pref-indicator'));
      const managedButton =
          forceEnabledSwLanguageRow.querySelector<HTMLButtonElement>(
              '.managed-button');
      assertTrue(!!managedButton);
      assertTrue(managedButton.disabled);
    });

    test('shows force-on non-enabled spell check language', () => {
      // Force-enable a non-enabled language via policy.
      inputPage.setPrefValue('spellcheck.forced_dictionaries', ['nb']);
      flush();

      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');

      // The spell check list should have en-US, nb and "add languages".
      assertEquals(2 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');

      const forceEnabledNbLanguageRow = newSpellCheckList[1];
      assertTrue(!!forceEnabledNbLanguageRow);
      assertStringContains(
          forceEnabledNbLanguageRow.textContent!, 'Norwegian Bokmål');
      assertTrue(!!forceEnabledNbLanguageRow.querySelector(
          'cr-policy-pref-indicator'));
      const managedButton =
          forceEnabledNbLanguageRow.querySelector<HTMLButtonElement>(
              '.managed-button');
      assertTrue(!!managedButton);
      assertTrue(managedButton.disabled);
    });

    test('does not show force-off spell check language enabled by user', () => {
      // Force-disable a spell check language originally set by the user.
      inputPage.setPrefValue('spellcheck.blocked_dictionaries', ['en-US']);
      flush();

      // The spell check list should just have "add languages".
      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(0 + 1, newSpellCheckList.length);
    });

    test('does not show force-off enabled spell check language', () => {
      // Force-disable an enabled language via policy.
      inputPage.setPrefValue('spellcheck.blocked_dictionaries', ['sw']);
      flush();

      // The spell check list should be the same (en-US, "add languages").
      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(1 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');
    });

    test('does not show force-off non-enabled spell check language', () => {
      // Force-disable a non-enabled language via policy.
      inputPage.setPrefValue('spellcheck.blocked_dictionaries', ['nb']);
      flush();

      // The spell check list should be the same (en-US, "add languages").
      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(1 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');
    });

    test('toggle off disables buttons', () => {
      assertTrue(spellCheckToggle.checked);
      let iconButton = spellCheckList[0]!.querySelector('cr-icon-button');
      assertTrue(!!iconButton);
      assertFalse(iconButton.disabled);
      // "Add languages" uses a cr-button instead of a cr-icon-button.
      let button = spellCheckList[1]!.querySelector('cr-button');
      assertTrue(!!button);
      assertFalse(button.disabled);

      spellCheckToggle.click();

      assertFalse(spellCheckToggle.checked);
      iconButton = spellCheckList[0]!.querySelector('cr-icon-button');
      assertTrue(!!iconButton);
      assertTrue(iconButton.disabled);
      button = spellCheckList[1]!.querySelector('cr-button');
      assertTrue(!!button);
      assertTrue(button.disabled);
    });

    test('languages are in sorted order', () => {
      inputPage.setPrefValue(
          'spellcheck.dictionaries', ['sw', 'en-US', 'nb', 'en-CA']);
      flush();
      // The spell check list should be sorted by display name:
      // English (Canada), English (United States), Norwegian Bokmål, then
      // Swahili.
      const newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(4 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (Canada)');
      assertStringContains(
          newSpellCheckList[1]!.textContent!, 'English (United States)');
      assertStringContains(
          newSpellCheckList[2]!.textContent!, 'Norwegian Bokmål');
      assertStringContains(newSpellCheckList[3]!.textContent!, 'Swahili');
    });

    test('removing all languages, then adding enabled language works', () => {
      // See https://crbug.com/1197386 for more information.
      // Remove en-US so there are no spell check languages.
      const spellCheckLanguageToggle =
          spellCheckList[0]!.querySelector('cr-icon-button');
      assertTrue(!!spellCheckLanguageToggle);
      spellCheckLanguageToggle.click();
      flush();

      let newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');

      // The spell check list should just have "add languages".
      assertEquals(0 + 1, newSpellCheckList.length);
      // The "enable spellchecking" toggle should be off as well.
      assertFalse(spellCheckToggle.checked);

      // Enable spell checking again.
      spellCheckToggle.click();
      newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      // The spell check list shouldn't have changed...
      assertEquals(0 + 1, newSpellCheckList.length);
      // ...but the "enable spellchecking" toggle should be checked.
      assertTrue(spellCheckToggle.checked);

      // Add an enabled language (en-US).
      languageHelper.toggleSpellCheck('en-US', true);
      flush();

      newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      // The spell check list should now have en-US.
      assertEquals(1 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');
      // Spell check should still be enabled.
      assertTrue(spellCheckToggle.checked);
    });

    test('changing Accept-Language does not change spellcheck', () => {
      // Remove en-US from Accept-Language, which is also an enabled spell check
      // language.
      languageHelper.disableLanguage('en-US');
      flush();

      // en-US should still be there.
      let newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(1 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');

      // Add a spell check language not in Accept-Language.
      languageHelper.toggleSpellCheck('nb', true);
      flush();

      // The spell check list should now have en-US, nb and "add languages".
      newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(2 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');
      assertStringContains(
          newSpellCheckList[1]!.textContent!, 'Norwegian Bokmål');

      // Add an arbitrary language to Accept-Language.
      languageHelper.enableLanguage('tk');
      flush();

      // The spell check list should remain the same.
      newSpellCheckList =
          spellCheckListContainer.querySelectorAll('.list-item');
      assertEquals(2 + 1, newSpellCheckList.length);
      assertStringContains(
          newSpellCheckList[0]!.textContent!, 'English (United States)');
      assertStringContains(
          newSpellCheckList[1]!.textContent!, 'Norwegian Bokmål');
    });

    // TODO(crbug.com/1201540): Add test to ensure that it is impossible to
    //     enable spell check without a spell check language added (i.e. the
    //     "add spell check languages" dialog appears when turning it on).

    // TODO(crbug.com/1201540): Add a test for the "automatically determining
    //     spell check language" behaviour when the user has no spell check
    //     languages.

    // TODO(crbug.com/1201540): Add a test for the shortcut reminder.

    test('error handling', () => {
      // Enable Swahili so we have two languages for testing.
      inputPage.setPrefValue('spellcheck.dictionaries', ['en-US', 'sw']);
      flush();
      const checkAllHidden = (nodes: HTMLElement[]) => {
        assertTrue(nodes.every(node => node.hidden));
      };

      const languageSettingsPrivate =
          browserProxy.getLanguageSettingsPrivate() as unknown as
          FakeLanguageSettingsPrivate;
      const errorDivs =
          Array.from(spellCheckListContainer.querySelectorAll<HTMLElement>(
              '.name-with-error div'));
      assertEquals(2, errorDivs.length);
      checkAllHidden(errorDivs);

      const retryButtons = Array.from(
          spellCheckListContainer.querySelectorAll<HTMLButtonElement>(
              'cr-button:not(#addSpellcheckLanguages)'));
      assertEquals(2, retryButtons.length);

      const languageCode = inputPage.get('languages.enabled.0.language.code');
      languageSettingsPrivate.onSpellcheckDictionariesChanged.callListeners([
        {languageCode, isReady: false, downloadFailed: true},
      ]);

      flush();
      assertFalse(errorDivs[0]!.hidden);
      assertFalse(retryButtons[0]!.hidden);
      assertFalse(retryButtons[0]!.disabled);

      // turns off spell check disable retry button.
      spellCheckToggle.click();
      assertTrue(retryButtons[0]!.disabled);

      // turns spell check back on and enable download.
      spellCheckToggle.click();
      languageSettingsPrivate.onSpellcheckDictionariesChanged.callListeners([
        {languageCode, isReady: true, downloadFailed: false},
      ]);

      flush();
      assertTrue(errorDivs[0]!.hidden);
      assertTrue(retryButtons[0]!.hidden);
    });

    test('toggle off disables edit dictionary', () => {
      const editDictionarySubpageTrigger =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#editDictionarySubpageTrigger');
      assertTrue(!!editDictionarySubpageTrigger);
      assertFalse(editDictionarySubpageTrigger.disabled);
      spellCheckToggle.click();

      assertTrue(editDictionarySubpageTrigger.disabled);
    });

    test('opens edit dictionary page', () => {
      const editDictionarySubpageTrigger =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#editDictionarySubpageTrigger');
      assertTrue(!!editDictionarySubpageTrigger);
      editDictionarySubpageTrigger.click();
      const router = Router.getInstance();
      assertEquals(
          'chrome://os-settings/osLanguages/editDictionary',
          router.currentRoute.getAbsolutePath());
    });
  });

  suite('add spell check languages dialog', () => {
    let dialog: OsSettingsAddItemsDialogElement;
    let suggestedList: IronListElement;
    let allLangsList: IronListElement;
    let cancelButton: HTMLButtonElement;
    let actionButton: HTMLButtonElement;

    /**
     * Returns the list items in the dialog.
     */
    function getAllLanguagesCheckboxWithPolicies():
        CrCheckboxWithPolicyElement[] {
      // If an element (the <iron-list> in this case) is hidden in Polymer,
      // Polymer will intelligently not update the DOM of the hidden element
      // to prevent DOM updates that the user can't see. However, this means
      // that when the <iron-list> is hidden (due to no results), the list
      // items still exist in the DOM.
      // This function should return the *visible* items that the user can
      // select, so if the <iron-list> is hidden we should return an empty
      // list instead.
      if (!isVisible(allLangsList)) {
        return [];
      }
      return [
        ...allLangsList.querySelectorAll('cr-checkbox-with-policy'),
      ].filter(checkbox => isVisible(checkbox));
    }

    /**
     * Returns the internal cr-checkboxes in allLanguages.
     */
    function getAllLanguagesCheckboxes(): CrCheckboxElement[] {
      const checkboxWithPolicies = getAllLanguagesCheckboxWithPolicies();
      return checkboxWithPolicies.map(checkboxWithPolicy => {
        const checkBox =
            checkboxWithPolicy.shadowRoot!.querySelector<CrCheckboxElement>(
                '#checkbox');
        assertTrue(!!checkBox);
        return checkBox;
      });
    }

    setup(async () => {
      await createInputPage();

      let element = inputPage.shadowRoot!.querySelector(
          'os-settings-add-spellcheck-languages-dialog');
      assertNull(element);
      const addSpellcheckLanguages =
          inputPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#addSpellcheckLanguages');
      assertTrue(!!addSpellcheckLanguages);
      addSpellcheckLanguages.click();
      flush();

      element = inputPage.shadowRoot!.querySelector(
          'os-settings-add-spellcheck-languages-dialog');
      assertTrue(!!element);
      const dialogElement =
          element.shadowRoot!.querySelector('os-settings-add-items-dialog');
      assertTrue(!!dialogElement);
      dialog = dialogElement;
      assertTrue(dialog.$.dialog.open);

      const list = dialog.shadowRoot!.querySelector<IronListElement>(
          '#suggested-items-list');
      assertTrue(!!list);
      suggestedList = list;
      const langList = dialog.shadowRoot!.querySelector<IronListElement>(
          '#filtered-items-list');
      assertTrue(!!langList);
      allLangsList = langList;

      const button =
          dialog.shadowRoot!.querySelector<HTMLButtonElement>('.action-button');
      assertTrue(!!button);
      actionButton = button;
      const cancel =
          dialog.shadowRoot!.querySelector<HTMLButtonElement>('.cancel-button');
      assertTrue(!!cancel);
      cancelButton = cancel;
    });

    test('action button is enabled and disabled when necessary', () => {
      // Mimic $$, but with a querySelectorAll instead of querySelector.
      const checkboxes = getAllLanguagesCheckboxes();
      assertGT(checkboxes.length, 0);

      // By default, no languages have been selected so the action button is
      // disabled.
      assertTrue(actionButton.disabled);

      // Selecting a language enables the action button.
      checkboxes[0]!.click();
      assertFalse(actionButton.disabled);

      // Selecting the same language again disables the action button.
      checkboxes[0]!.click();
      assertTrue(actionButton.disabled);
    });

    test('cancel button is never disabled', () => {
      assertFalse(cancelButton.disabled);
    });

    test('initial expected layout', () => {
      // As Swahili is an enabled language, it should be shown as a suggested
      // language.
      const suggestedItems = suggestedList.querySelectorAll('cr-checkbox');
      assertEquals(1, suggestedItems.length);
      assertStringContains(suggestedItems[0]!.textContent!, 'Swahili');

      // There are four languages with spell check enabled in
      // fake_language_settings_private.js: en-US, en-CA, sw, nb.
      // en-US shouldn't be displayed as it is already enabled.
      const allItems = getAllLanguagesCheckboxWithPolicies();
      assertEquals(3, allItems.length);
      assertStringContains(allItems[0]!.textContent!, 'English (Canada)');
      assertStringContains(allItems[1]!.textContent!, 'Swahili');
      assertStringContains(allItems[2]!.textContent!, 'Norwegian Bokmål');

      // By default, all checkboxes should not be disabled, and should not be
      // checked.
      const checkboxes = [...suggestedItems, ...getAllLanguagesCheckboxes()];
      assertTrue(checkboxes.every(checkbox => !checkbox.disabled));
      assertTrue(checkboxes.every(checkbox => !checkbox.checked));

      // There should be a label for both sections.
      const suggestedLabel =
          dialog.shadowRoot!.querySelector('#suggested-items-label');
      assertTrue(isVisible(suggestedLabel));

      const allLangsLabel =
          dialog.shadowRoot!.querySelector('#filtered-items-label');
      assertTrue(isVisible(allLangsLabel));
    });

    test('can add single language and uncheck language', () => {
      const checkboxes = getAllLanguagesCheckboxes();
      const swCheckbox = checkboxes[1];
      const nbCheckbox = checkboxes[2];
      assertTrue(!!swCheckbox);
      assertTrue(!!nbCheckbox);

      // By default, en-US should be the only enabled spell check dictionary.
      assertDeepEquals(
          ['en-US'], inputPage.prefs.spellcheck.dictionaries.value);

      swCheckbox.click();
      assertTrue(swCheckbox.checked);

      // Check and uncheck nb to ensure that it gets "ignored".
      nbCheckbox.click();
      assertTrue(nbCheckbox.checked);

      nbCheckbox.click();
      assertFalse(nbCheckbox.checked);

      actionButton.click();
      assertDeepEquals(
          ['en-US', 'sw'], inputPage.prefs.spellcheck.dictionaries.value);
      assertFalse(dialog.$.dialog.open);
    });

    test('can add multiple languages', () => {
      const checkboxes = getAllLanguagesCheckboxes();

      assertDeepEquals(
          ['en-US'], inputPage.prefs.spellcheck.dictionaries.value);

      // Click en-CA and nb.
      checkboxes[0]!.click();
      assertTrue(checkboxes[0]!.checked);
      checkboxes[2]!.click();
      assertTrue(checkboxes[2]!.checked);

      actionButton.click();
      // The two possible results are en-US, en-CA, nb OR en-US, nb, en-CA.
      // We do not care about the ordering of the last two, but the first one
      // should still be en-US.
      assertEquals('en-US', inputPage.prefs.spellcheck.dictionaries.value[0]);
      // Note that .sort() mutates the array, but as this is the end of the test
      // the prefs will be reset after this anyway.
      assertDeepEquals(
          ['en-CA', 'en-US', 'nb'],
          inputPage.prefs.spellcheck.dictionaries.value.sort());
      assertFalse(dialog.$.dialog.open);
    });

    test('policy disabled languages cannot be selected and show icon', () => {
      // Force-disable sw.
      inputPage.setPrefValue('spellcheck.blocked_dictionaries', ['sw']);
      flush();

      const swCheckboxWithPolicy = getAllLanguagesCheckboxWithPolicies()[1];
      assertTrue(!!swCheckboxWithPolicy);
      const swCheckbox =
          swCheckboxWithPolicy.shadowRoot!.querySelector('cr-checkbox');
      assertTrue(!!swCheckbox);
      const swPolicyIcon =
          swCheckboxWithPolicy.shadowRoot!.querySelector('iron-icon');
      assertTrue(!!swPolicyIcon);

      assertTrue(swCheckbox.disabled);
      assertFalse(swCheckbox.checked);
    });

    test('labels do not appear if there are no suggested languages', () => {
      // Disable sw, the only default suggested language, as a web language.
      languageHelper.disableLanguage('sw');
      flush();

      // Suggested languages should not show up whatsoever.
      assertFalse(isVisible(suggestedList));
      // The label for all languages should not appear either.
      assertFalse(isVisible(allLangsList.querySelector('.label')));
    });

    test('input method languages appear as suggested languages', () => {
      // Remove en-US from the dictionary list AND the enabled languages list.
      inputPage.setPrefValue('spellcheck.dictionaries', []);
      languageHelper.disableLanguage('en-US');
      flush();

      // Both Swahili (as it is an enabled language) and English (US) (as it is
      // enabled as an input method) should appear in the list.
      const suggestedListItems = suggestedList.querySelectorAll('.list-item');
      assertEquals(2, suggestedListItems.length);
      assertStringContains(
          suggestedListItems[0]!.textContent!, 'English (United States)');
      assertStringContains(suggestedListItems[1]!.textContent!, 'Swahili');

      // en-US should also appear in the all languages list now.
      assertEquals(4, allLangsList.querySelectorAll('.list-item').length);
    });

    test('searches languages on display name', () => {
      const searchInput = dialog.shadowRoot!.querySelector('cr-search-field');
      assertTrue(!!searchInput);
      // Expecting a few languages to be displayed when no query exists.
      assertGE(getAllLanguagesCheckboxWithPolicies().length, 1);

      // Issue query that matches the |displayedName| in lowercase.
      searchInput.setValue('norwegian');
      flush();
      assertEquals(1, getAllLanguagesCheckboxWithPolicies().length);
      assertStringContains(
          getAllLanguagesCheckboxWithPolicies()[0]!.textContent!,
          'Norwegian Bokmål');

      // Issue query that matches the |nativeDisplayedName|.
      searchInput.setValue('norsk');
      flush();
      assertEquals(1, getAllLanguagesCheckboxWithPolicies().length);

      // Issue query that does not match any language.
      searchInput.setValue('egaugnal');
      flush();
      assertEquals(0, getAllLanguagesCheckboxWithPolicies().length);
      assertTrue(
          isVisible(dialog.shadowRoot!.querySelector('#no-search-results')));
    });

    test('has escape key behavior working correctly', () => {
      const searchInput = dialog.shadowRoot!.querySelector('cr-search-field');
      assertTrue(!!searchInput);
      searchInput.setValue('dummyquery');

      // Test that dialog is not closed if 'Escape' is pressed on the input
      // and a search query exists.
      keyDownOn(searchInput, 19, [], 'Escape');
      assertTrue(dialog.$.dialog.open);

      // Test that dialog is closed if 'Escape' is pressed on the input and no
      // search query exists.
      searchInput.setValue('');
      keyDownOn(searchInput, 19, [], 'Escape');
      assertFalse(dialog.$.dialog.open);
    });
  });

  suite('Suggestions', () => {
    suite('when emoji suggestions are not available', () => {
      setup(() => {
        loadTimeData.overrideValues({allowEmojiSuggestion: false});
      });
    });

    test('Emoji suggestion toggle is visible', async () => {
      await createInputPage();
      const emojiSuggestionToggle =
          inputPage.shadowRoot!.querySelector('#emojiSuggestionToggle');
      assertTrue(isVisible(emojiSuggestionToggle));
    });

    test('Deep link to emoji suggestion toggle', async () => {
      await createInputPage();

      const params = new URLSearchParams();
      const setting = settingMojom.Setting.kShowEmojiSuggestions;
      params.append('settingId', setting.toString());
      Router.getInstance().navigateTo(routes.OS_LANGUAGES_INPUT, params);
      flush();

      const deepLinkElement = inputPage.shadowRoot!.querySelector<HTMLElement>(
          '#emojiSuggestionToggle');
      assertTrue(!!deepLinkElement);
      await waitAfterNextRender(deepLinkElement);
      assertEquals(
          deepLinkElement, inputPage.shadowRoot!.activeElement,
          `Emoji suggestion toggle should be focused for settingId=${
              setting}.`);
    });

    suite('when allowOrca is false', () => {
      setup(() => {
        loadTimeData.overrideValues({allowOrca: false});
      });

      test('Orca toggle should be hidden', async () => {
        await createInputPage();
        const orcaToggle = inputPage.shadowRoot!.querySelector('#orcaToggle');
        assertFalse(isVisible(orcaToggle));
      });
    });

    suite('when allowOrca is true', () => {
      setup(() => {
        loadTimeData.overrideValues({allowOrca: true});
      });

      test('Orca toggle should be visible', async () => {
        await createInputPage();
        const orcaToggle = inputPage.shadowRoot!.querySelector('#orcaToggle');
        assertTrue(isVisible(orcaToggle));
      });

      test('Deep link to orca suggestion toggle', async () => {
        await createInputPage();

        const params = new URLSearchParams();
        const setting = settingMojom.Setting.kShowOrca;
        params.append('settingId', setting.toString());
        Router.getInstance().navigateTo(routes.OS_LANGUAGES_INPUT, params);
        flush();

        const deepLinkElement =
            inputPage.shadowRoot!.querySelector<HTMLElement>('#orcaToggle');
        assertTrue(!!deepLinkElement);
        await waitAfterNextRender(deepLinkElement);
        assertEquals(
            deepLinkElement, inputPage.shadowRoot!.activeElement,
            `Orca suggestion toggle should be focused for settingId=${
                setting}.`);
      });
    });

    suite('when both the emoji suggestions and orca are not allowed', () => {
      setup(() => {
        loadTimeData.overrideValues(
            {allowEmojiSuggestion: false, allowOrca: false});
      });

      test('Suggestions section is not visible', async () => {
        await createInputPage();
        const suggestionsSection =
            inputPage.shadowRoot!.querySelector('#suggestionsSection');
        assertFalse(isVisible(suggestionsSection));
      });
    });
  });
});